diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 12ca9e461e..518069b359 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -164,6 +164,8 @@
+
+
diff --git a/settings.gradle b/settings.gradle
index e584008cef..c3220102e7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -43,6 +43,7 @@ include 'perftestcordapp'
['test-common', 'test-utils', 'smoke-test-utils', 'node-driver'].each {
project(":$it").projectDir = new File("$settingsDir/testing/$it")
}
+include 'testing:qa:behave:tools:rpc-proxy'
include 'network-management'
include 'network-management:capsule'
include 'network-management:capsule-hsm'
diff --git a/testing/qa/behave/scripts/update-corda-cts.sh b/testing/qa/behave/scripts/update-corda-cts.sh
new file mode 100755
index 0000000000..5189f3ea4e
--- /dev/null
+++ b/testing/qa/behave/scripts/update-corda-cts.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+# Please ensure you run this script using source code (eg. GitHub master, branch or TAG) that reflects the version label defined below
+# For example:
+# corda-master => git clone https://github.com/corda/corda
+# r3corda-master => git clone https://github.com/corda/enterprise
+
+# Please run this script from the corda source code directory
+# eg. $ pwd
+# /myprojects/corda
+# $ testing/qa/behave/scripts/update-corda-cts.sh
+
+VERSION=master
+BUILD_DIR=`pwd`
+STAGING_DIR=~/staging/corda/corda-${VERSION}
+
+# Set up directories
+mkdir -p ${STAGING_DIR}/apps
+
+cd ${BUILD_DIR}
+
+echo "*************************************************************"
+echo "Building and installing $VERSION from $BUILD_DIR"
+echo " to $STAGING_DIR"
+echo "*************************************************************"
+
+# Copy Corda capsule into deps
+./gradlew clean install
+cp -v $(ls node/capsule/build/libs/corda-*.jar | tail -n1) ${STAGING_DIR}/corda.jar
+cp -v $(ls finance/build/libs/corda-finance-*.jar | tail -n1) ${STAGING_DIR}/apps
+
+# Build Network Bootstrapper
+./gradlew buildBootstrapperJar
+cp -v $(ls tools/bootstrapper/build/libs/*.jar | tail -n1) ${STAGING_DIR}/network-bootstrapper.jar
+
+# Build rpcProxy (required by CTS Scenario Driver to call Corda 3.0 which continues to use Kryo for RPC)
+./gradlew testing:qa:behave:tools:rpc-proxy:rpcProxyJar
+cp -v $(ls testing/qa/behave/tools/rpc-proxy/build/libs/corda-rpcProxy*.jar | tail -n1) ${STAGING_DIR}/corda-rpcProxy.jar
+cp -v testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh ${STAGING_DIR}
\ No newline at end of file
diff --git a/testing/qa/behave/scripts/update-cordapps-cts.sh b/testing/qa/behave/scripts/update-cordapps-cts.sh
new file mode 100755
index 0000000000..42ac7c5e74
--- /dev/null
+++ b/testing/qa/behave/scripts/update-cordapps-cts.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+# Please ensure you run this script using source code (eg. GitHub master, branch or TAG) that reflects the version label defined below
+# For example:
+# corda-master => git clone https://github.com/corda/corda
+# r3corda-master => git clone https://github.com/corda/enterprise
+
+# Please run this script from the corda source code directory
+# eg. $ pwd
+# /myprojects/corda
+# $ testing/qa/behave/scripts/update-cordapps-cts.sh
+
+VERSION=master
+BUILD_DIR=`pwd`
+SAMPLES_ROOT=`pwd`/../samples
+STAGING_DIR=~/staging/corda/corda-${VERSION}
+
+# Set up directories
+mkdir -p ${STAGING_DIR}/apps
+mkdir -p ${STAGING_DIR}/proxy
+
+# Corda repository pinned cordapps
+cd ${BUILD_DIR}
+
+./gradlew samples:simm-valuation-demo:jar
+cp -v $(ls samples/simm-valuation-demo/build/libs/simm-valuation-demo-*.jar | tail -n1) ${STAGING_DIR}/apps
+
+# Independent cordapps
+cd ${SAMPLES_ROOT}
+
+# Options sample
+cd cordapp-option
+./gradlew clean jar publishToMavenLocal
+cp -v $(ls build/libs/cordapp-option-*.jar | tail -n1) ${STAGING_DIR}/apps
+cp -v $(ls build/libs/cordapp-option-*.jar | tail -n1) ${STAGING_DIR}/proxy
diff --git a/testing/qa/behave/scripts/update-gradle-plugins.sh b/testing/qa/behave/scripts/update-gradle-plugins.sh
new file mode 100755
index 0000000000..b1dd21203e
--- /dev/null
+++ b/testing/qa/behave/scripts/update-gradle-plugins.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+# Please ensure you run this script using source code (eg. GitHub master, branch or TAG) that reflects the version label defined below
+# For example:
+# corda-master => git clone https://github.com/corda/corda
+# r3corda-master => git clone https://github.com/corda/enterprise
+# from v4.x.x => https://github.com/corda/corda-gradle-plugins
+
+# Please run this script from the corda source code directory
+# eg. $ pwd
+# /myprojects/r3corda
+# $ testing/qa/behave/scripts/update-gradle-plugins.sh
+
+VERSION=master
+BUILD_DIR=`pwd`
+STAGING_DIR=~/staging/corda/corda-${VERSION}
+
+# uses gradle plugins directory
+cd ${BUILD_DIR}
+
+# update Gradle plugins (note this is being moved to a new repo https://github.com/corda/corda-gradle-plugins from v4.x.x )
+echo "*************************************************************"
+echo "Building and installing `cat constants.properties | grep gradlePluginsVersion`"
+echo "*************************************************************"
+
+cd publish-utils
+../gradlew -u install
+cd ../
+./gradlew install
+cd ..
diff --git a/testing/qa/behave/scripts/update-r3corda-cts.sh b/testing/qa/behave/scripts/update-r3corda-cts.sh
new file mode 100755
index 0000000000..c15284b0f0
--- /dev/null
+++ b/testing/qa/behave/scripts/update-r3corda-cts.sh
@@ -0,0 +1,45 @@
+#!/bin/bash
+
+# Please ensure you run this script using source code (eg. GitHub master, branch or TAG) that reflects the version label defined below
+# For example:
+# corda-master => git clone https://github.com/corda/corda
+# r3corda-master => git clone https://github.com/corda/enterprise
+
+# Please run this script from the corda source code directory
+# eg. $ pwd
+# /myprojects/r3corda
+# $ testing/qa/behave/scripts/update-r3corda-cts.sh
+
+VERSION=master
+BUILD_DIR=`pwd`
+STAGING_DIR=~/staging/corda/r3corda-${VERSION}
+
+# Set up directories
+mkdir -p ${STAGING_DIR}/apps
+
+cd ${BUILD_DIR}
+
+echo "*************************************************************"
+echo "Building and installing $VERSION from $BUILD_DIR"
+echo "to $STAGING_DIR"
+echo "*************************************************************"
+
+# Copy Corda capsule into deps
+./gradlew clean install
+cp -v $(ls node/capsule/build/libs/corda-*.jar | tail -n1) ${STAGING_DIR}/corda.jar
+
+# Copy Corda libraries into apps
+cp -v $(ls finance/build/libs/corda-finance-*.jar | tail -n1) ${STAGING_DIR}/apps
+
+# build and distribute Doorman/NMS
+./gradlew network-management:capsule:buildDoormanJAR
+cp -v $(ls network-management/capsule/build/libs/doorman-*.jar | tail -n1) ${STAGING_DIR}/doorman.jar
+
+# build and distribute DB Migration tool
+./gradlew tools:dbmigration:shadowJar
+cp -v $(ls tools/dbmigration/build/libs/*migration-*.jar | tail -n1) ${STAGING_DIR}/dbmigration.jar
+
+# Build rpcProxy (required by CTS Scenario Driver to call Corda 3.0 which continues to use Kryo for RPC)
+./gradlew testing:qa:behave:tools:rpc-proxy:rpcProxyJar
+cp -v $(ls testing/qa/behave/tools/rpc-proxy/build/libs/corda-rpcProxy*.jar | tail -n1) ${STAGING_DIR}/corda-rpcProxy.jar
+cp -v testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh ${STAGING_DIR}
\ No newline at end of file
diff --git a/testing/qa/behave/scripts/update-r3cordapps-cts.sh b/testing/qa/behave/scripts/update-r3cordapps-cts.sh
new file mode 100755
index 0000000000..8f5cd58c6c
--- /dev/null
+++ b/testing/qa/behave/scripts/update-r3cordapps-cts.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# Please ensure you run this script using source code (eg. GitHub master, branch or TAG) that reflects the version label defined below
+# For example:
+# corda-master => git clone https://github.com/corda/corda
+# r3corda-master => git clone https://github.com/corda/enterprise
+
+# Please run this script from the corda source code directory
+# eg. $ pwd
+# /myprojects/r3corda
+# $ testing/qa/behave/scripts/update-r3cordapps-cts.sh
+
+VERSION=master
+BUILD_DIR=`pwd`
+SAMPLES_ROOT=`pwd`/../samples
+STAGING_DIR=~/staging/corda/r3corda-${VERSION}
+
+# Set up directories
+mkdir -p ${STAGING_DIR}/apps
+
+# Corda repository pinned cordapps
+cd ${BUILD_DIR}
+
+./gradlew samples:simm-valuation-demo:jar
+cp -v $(ls samples/simm-valuation-demo/build/libs/simm-valuation-demo-*.jar | tail -n1) ${STAGING_DIR}/apps/simm-valuation-demo.jar
+cp -v $(ls samples/simm-valuation-demo/build/libs/simm-valuation-demo-*.jar | tail -n1) ${STAGING_DIR}/proxy/simm-valuation-demo.jar
+
+# R3 Corda only
+./gradlew tools:notaryhealthcheck:cordaCompileJar
+cp -v $(ls tools/notaryhealthcheck/build/libs/notaryhealthcheck-*.jar | tail -n1) ${STAGING_DIR}/apps/notaryhealthcheck.jar
+cp -v $(ls tools/notaryhealthcheck/build/libs/notaryhealthcheck-*.jar | tail -n1) ${STAGING_DIR}/proxy/notaryhealthcheck.jar
+
+# Independent cordapps
+cd ${SAMPLES_ROOT}
+
+# Options sample
+cd cordapp-option
+./gradlew clean jar publishToMavenLocal
+cp -v $(ls build/libs/cordapp-option-*.jar | tail -n1) ${STAGING_DIR}/apps
+cp -v $(ls build/libs/cordapp-option-*.jar | tail -n1) ${STAGING_DIR}/proxy
diff --git a/testing/qa/behave/scripts/update-rpcProxy-cts.sh b/testing/qa/behave/scripts/update-rpcProxy-cts.sh
new file mode 100755
index 0000000000..77ddce0e8c
--- /dev/null
+++ b/testing/qa/behave/scripts/update-rpcProxy-cts.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# Please ensure you run this script using source code (eg. GitHub master, branch or TAG) that reflects the version label defined below
+# For example:
+# corda-master => git clone https://github.com/corda/corda
+# r3corda-master => git clone https://github.com/corda/enterprise
+
+# Please run this script from the corda source code directory
+# eg. $ pwd
+# /myprojects/r3corda
+# $ testing/qa/behave/scripts/update-rpcProxy-cts.sh
+
+VERSION=master
+BUILD_DIR=`pwd`
+STAGING_DIR=~/staging/corda/corda-${VERSION}
+
+# Set up directories
+mkdir -p ${STAGING_DIR}/apps
+
+cd ${BUILD_DIR}
+
+# Build rpcProxy (required by CTS Scenario Driver to call Corda 3.0 which continues to use Kryo for RPC)
+./gradlew testing:qa:behave:tools:rpc-proxy:rpcProxyJar
+cp -v $(ls testing/qa/behave/tools/rpc-proxy/build/libs/corda-rpcProxy*.jar | tail -n1) ${STAGING_DIR}/corda-rpcProxy.jar
+cp -v testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh ${STAGING_DIR}
diff --git a/testing/qa/behave/tools/rpc-proxy/build.gradle b/testing/qa/behave/tools/rpc-proxy/build.gradle
new file mode 100644
index 0000000000..6e4f9e54bd
--- /dev/null
+++ b/testing/qa/behave/tools/rpc-proxy/build.gradle
@@ -0,0 +1,85 @@
+/*
+ * R3 Proprietary and Confidential
+ *
+ * Copyright (c) 2018 R3 Limited. All rights reserved.
+ *
+ * The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
+ *
+ * Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
+ */
+
+group 'net.corda.behave.tools'
+
+apply plugin: 'java'
+apply plugin: 'kotlin'
+apply plugin: 'maven-publish'
+
+configurations {
+ smokeTestCompile.extendsFrom testCompile
+ smokeTestRuntime.extendsFrom testRuntime
+}
+
+sourceSets {
+ rpcProxy {
+ kotlin {
+ srcDir "src/main/kotlin"
+ }
+ resources {
+ srcDir "config/dev"
+ }
+ }
+ smokeTest {
+ kotlin {
+ compileClasspath += main.output + test.output
+ runtimeClasspath += main.output + test.output
+ srcDir file('src/smoke-test/kotlin')
+ }
+ }
+}
+
+dependencies {
+ compile project(':test-utils')
+ compile "net.corda.plugins:cordform-common:$gradle_plugins_version"
+
+ // Integration test helpers
+ testCompile "org.assertj:assertj-core:$assertj_version"
+ testCompile "junit:junit:$junit_version"
+
+ compile project(':finance')
+ compileOnly project(':node-api')
+ compile project(':core')
+ compile project(':client:rpc')
+
+ // includes jetty/jersey dependencies used by RPCProxyServer
+ compile project(':webserver')
+
+ // Jetty http server
+ rpcProxyCompile "org.eclipse.jetty:jetty-servlet:$jetty_version"
+ rpcProxyCompile "org.eclipse.jetty:jetty-webapp:$jetty_version"
+ rpcProxyCompile "javax.servlet:javax.servlet-api:3.1.0"
+
+ // Jersey for JAX-RS implementation for use in Jetty
+ rpcProxyCompile "org.glassfish.jersey.core:jersey-server:$jersey_version"
+ rpcProxyCompile "org.glassfish.jersey.containers:jersey-container-servlet-core:$jersey_version"
+ rpcProxyCompile "org.glassfish.jersey.containers:jersey-container-jetty-http:$jersey_version"
+
+}
+
+task smokeTest(type: Test) {
+ testClassesDirs = sourceSets.smokeTest.output.classesDirs
+ classpath = sourceSets.smokeTest.runtimeClasspath
+}
+
+task rpcProxyJar(type: Jar) {
+ baseName "corda-rpcProxy"
+ from {
+ configurations.rpcProxyRuntime.collect { it.isDirectory() ? it : zipTree(it) }
+ }
+ with jar
+ exclude("META-INF/*.DSA")
+ exclude("META-INF/*.RSA")
+ exclude("META-INF/*.SF")
+ manifest {
+ attributes 'Main-Class': 'net.corda.behave.service.proxy.RPCProxyServerKt'
+ }
+}
diff --git a/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/CordaRPCProxyClient.kt b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/CordaRPCProxyClient.kt
new file mode 100644
index 0000000000..01456ca21d
--- /dev/null
+++ b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/CordaRPCProxyClient.kt
@@ -0,0 +1,243 @@
+package net.corda.behave.service.proxy
+
+import net.corda.client.rpc.internal.KryoClientSerializationScheme
+import net.corda.core.concurrent.CordaFuture
+import net.corda.core.contracts.ContractState
+import net.corda.core.crypto.SecureHash
+import net.corda.core.flows.FlowLogic
+import net.corda.core.flows.StateMachineRunId
+import net.corda.core.identity.AbstractParty
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.identity.Party
+import net.corda.core.internal.concurrent.doneFuture
+import net.corda.core.internal.openHttpConnection
+import net.corda.core.internal.responseAs
+import net.corda.core.messaging.*
+import net.corda.core.node.NodeInfo
+import net.corda.core.node.services.AttachmentId
+import net.corda.core.node.services.NetworkMapCache
+import net.corda.core.node.services.Vault
+import net.corda.core.node.services.vault.*
+import net.corda.core.serialization.serialize
+import net.corda.core.transactions.SignedTransaction
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.core.utilities.contextLogger
+import java.io.InputStream
+import java.net.URL
+import java.security.PublicKey
+import java.time.Instant
+import javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM
+
+class CordaRPCProxyClient(private val targetHostAndPort: NetworkHostAndPort) : CordaRPCOps {
+
+ companion object {
+ val log = contextLogger()
+ }
+
+ init {
+ try {
+ KryoClientSerializationScheme.initialiseSerialization()
+ } catch (e: Exception) { log.warn("Kryo RPC Client serialization already initialised.")}
+ }
+
+ override fun startFlowDynamic(logicType: Class>, vararg args: Any?): FlowHandle {
+ val flowName = logicType.name
+ val argList = listOf(flowName, *args)
+
+ log.info("Corda RPC Proxy client calling: $flowName with values: $argList")
+ val response = doPost(targetHostAndPort, "start-flow", argList.serialize().bytes)
+ val result = doneFuture(response)
+ return FlowHandleImpl(StateMachineRunId.createRandom(), result) as FlowHandle
+ }
+
+ override fun nodeInfo(): NodeInfo {
+ return doGet(targetHostAndPort, "node-info")
+ }
+
+ override fun notaryIdentities(): List {
+ return doGet(targetHostAndPort, "notary-identities")
+ }
+
+ override fun vaultQuery(contractStateType: Class): Vault.Page {
+ return doPost(targetHostAndPort, "vault-query", contractStateType.name.serialize().bytes)
+ }
+
+ override fun networkMapSnapshot(): List {
+ return doGet(targetHostAndPort, "network-map-snapshot")
+ }
+
+ override fun partiesFromName(query: String, exactMatch: Boolean): Set {
+ return doPost(targetHostAndPort, "parties-from-name", query.serialize().bytes)
+ }
+
+ override fun registeredFlows(): List {
+ return doGet(targetHostAndPort, "registered-flows")
+ }
+
+ override fun stateMachinesSnapshot(): List {
+ TODO("not implemented")
+ }
+
+ override fun stateMachinesFeed(): DataFeed, StateMachineUpdate> {
+ TODO("not implemented")
+ }
+
+ override fun vaultQueryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): Vault.Page {
+ TODO("not implemented")
+ }
+
+ override fun vaultQueryByCriteria(criteria: QueryCriteria, contractStateType: Class): Vault.Page {
+ TODO("not implemented")
+ }
+
+ override fun vaultQueryByWithPagingSpec(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): Vault.Page {
+ TODO("not implemented")
+ }
+
+ override fun vaultQueryByWithSorting(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): Vault.Page {
+ TODO("not implemented")
+ }
+
+ override fun vaultTrackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): DataFeed, Vault.Update> {
+ TODO("not implemented")
+ }
+
+ override fun vaultTrack(contractStateType: Class): DataFeed, Vault.Update> {
+ TODO("not implemented")
+ }
+
+ override fun vaultTrackByCriteria(contractStateType: Class, criteria: QueryCriteria): DataFeed, Vault.Update> {
+ TODO("not implemented")
+ }
+
+ override fun vaultTrackByWithPagingSpec(contractStateType: Class, criteria: QueryCriteria, paging: PageSpecification): DataFeed, Vault.Update> {
+ TODO("not implemented")
+ }
+
+ override fun vaultTrackByWithSorting(contractStateType: Class, criteria: QueryCriteria, sorting: Sort): DataFeed, Vault.Update> {
+ TODO("not implemented")
+ }
+
+ override fun internalVerifiedTransactionsSnapshot(): List {
+ TODO("not implemented")
+ }
+
+ override fun internalVerifiedTransactionsFeed(): DataFeed, SignedTransaction> {
+ TODO("not implemented")
+ }
+
+ override fun stateMachineRecordedTransactionMappingSnapshot(): List {
+ TODO("not implemented")
+ }
+
+ override fun stateMachineRecordedTransactionMappingFeed(): DataFeed, StateMachineTransactionMapping> {
+ TODO("not implemented")
+ }
+
+ override fun networkMapFeed(): DataFeed, NetworkMapCache.MapChange> {
+ TODO("not implemented")
+ }
+
+ override fun networkParametersFeed(): DataFeed {
+ TODO("not implemented")
+ }
+
+ override fun acceptNewNetworkParameters(parametersHash: SecureHash) {
+ TODO("not implemented")
+ }
+
+ override fun startTrackedFlowDynamic(logicType: Class>, vararg args: Any?): FlowProgressHandle {
+ TODO("not implemented")
+ }
+
+ override fun addVaultTransactionNote(txnId: SecureHash, txnNote: String) {
+ TODO("not implemented")
+ }
+
+ override fun getVaultTransactionNotes(txnId: SecureHash): Iterable {
+ TODO("not implemented")
+ }
+
+ override fun attachmentExists(id: SecureHash): Boolean {
+ TODO("not implemented")
+ }
+
+ override fun openAttachment(id: SecureHash): InputStream {
+ TODO("not implemented")
+ }
+
+ override fun uploadAttachment(jar: InputStream): SecureHash {
+ TODO("not implemented")
+ }
+
+ override fun uploadAttachmentWithMetadata(jar: InputStream, uploader: String, filename: String): SecureHash {
+ TODO("not implemented")
+ }
+
+ override fun queryAttachments(query: AttachmentQueryCriteria, sorting: AttachmentSort?): List {
+ TODO("not implemented")
+ }
+
+ override fun currentNodeTime(): Instant {
+ TODO("not implemented")
+ }
+
+ override fun waitUntilNetworkReady(): CordaFuture {
+ TODO("not implemented")
+ }
+
+ override fun wellKnownPartyFromAnonymous(party: AbstractParty): Party? {
+ TODO("not implemented")
+ }
+
+ override fun partyFromKey(key: PublicKey): Party? {
+ TODO("not implemented")
+ }
+
+ override fun wellKnownPartyFromX500Name(x500Name: CordaX500Name): Party? {
+ TODO("not implemented")
+ }
+
+ override fun notaryPartyFromX500Name(x500Name: CordaX500Name): Party? {
+ TODO("not implemented")
+ }
+
+ override fun nodeInfoFromParty(party: AbstractParty): NodeInfo? {
+ TODO("not implemented")
+ }
+
+ override fun clearNetworkMapCache() {
+ TODO("not implemented")
+ }
+
+ override fun setFlowsDrainingModeEnabled(enabled: Boolean) {
+ TODO("not implemented")
+ }
+
+ override fun isFlowsDrainingModeEnabled(): Boolean {
+ TODO("not implemented")
+ }
+
+ override fun shutdown() {
+ TODO("not implemented")
+ }
+
+ override fun killFlow(id: StateMachineRunId): Boolean {
+ TODO("not implemented")
+ }
+
+ private inline fun doPost(hostAndPort: NetworkHostAndPort, path: String, payload: ByteArray) : T {
+ val url = URL("http://$hostAndPort/rpc/$path")
+ val connection = url.openHttpConnection().apply {
+ doOutput = true
+ requestMethod = "POST"
+ setRequestProperty("Content-Type", APPLICATION_OCTET_STREAM)
+ outputStream.write(payload)
+ }
+ return connection.responseAs()
+ }
+
+ private inline fun doGet(hostAndPort: NetworkHostAndPort, path: String): T {
+ return URL("http://$hostAndPort/rpc/$path").openHttpConnection().responseAs()
+ }
+}
\ No newline at end of file
diff --git a/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyServer.kt b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyServer.kt
new file mode 100644
index 0000000000..e5e4370635
--- /dev/null
+++ b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyServer.kt
@@ -0,0 +1,94 @@
+package net.corda.behave.service.proxy
+
+import net.corda.behave.service.proxy.RPCProxyServer.Companion.initialiseSerialization
+import net.corda.behave.service.proxy.RPCProxyServer.Companion.log
+import net.corda.client.rpc.internal.KryoClientSerializationScheme
+import net.corda.core.serialization.internal.SerializationEnvironmentImpl
+import net.corda.core.serialization.internal.nodeSerializationEnv
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.core.utilities.contextLogger
+import net.corda.nodeapi.internal.serialization.AMQP_P2P_CONTEXT
+import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT
+import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
+import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme
+import org.eclipse.jetty.server.Server
+import org.eclipse.jetty.server.ServerConnector
+import org.eclipse.jetty.server.handler.HandlerCollection
+import org.eclipse.jetty.servlet.ServletContextHandler
+import org.eclipse.jetty.servlet.ServletHolder
+import org.glassfish.jersey.server.ResourceConfig
+import org.glassfish.jersey.servlet.ServletContainer
+import java.io.Closeable
+import java.net.InetSocketAddress
+
+class RPCProxyServer(hostAndPort: NetworkHostAndPort, val webService: RPCProxyWebService) : Closeable {
+
+ fun start(): Boolean {
+ log.info("Starting RPC Proxy web services...")
+ try {
+ buildServletContextHandler()
+ server.start()
+ }
+ catch(e: Exception) {
+ log.error("Failed to start RPC Proxy server: ${e.message}", e)
+ return false
+ }
+ log.info("RPC Proxy web services started on $hostAndPort with ${webService.javaClass.simpleName}}")
+
+ return true
+ }
+
+ override fun close() {
+ log.info("Shutting down RPC Proxy web services...")
+ server.stop()
+ server.join()
+ }
+
+ private val server: Server = Server(InetSocketAddress(hostAndPort.host, hostAndPort.port)).apply {
+ handler = HandlerCollection().apply {
+ addHandler(buildServletContextHandler())
+ }
+ }
+
+ private val hostAndPort: NetworkHostAndPort
+ get() = server.connectors.mapNotNull { it as? ServerConnector }
+ .map { NetworkHostAndPort(it.host, it.localPort) }
+ .first()
+
+ private fun buildServletContextHandler(): ServletContextHandler {
+ return ServletContextHandler().apply {
+ contextPath = "/"
+ val resourceConfig = ResourceConfig().apply {
+ // Add your API provider classes (annotated for JAX-RS) here
+ register(webService)
+ }
+ val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { initOrder = 0 }// Initialise at server start
+ addServlet(jerseyServlet, "/*")
+ }
+ }
+
+ companion object {
+ val log = contextLogger()
+ fun initialiseSerialization() {
+ try {
+ nodeSerializationEnv =
+ SerializationEnvironmentImpl(
+ SerializationFactoryImpl().apply {
+ registerScheme(KryoClientSerializationScheme())
+ registerScheme(AMQPClientSerializationScheme(emptyList()))
+ },
+ AMQP_P2P_CONTEXT,
+ rpcClientContext = KRYO_RPC_CLIENT_CONTEXT)
+ }
+ catch(e: Exception) { log.warn("Skipping initialiseSerialization: ${e.message}") }
+ }
+ }
+}
+
+fun main(args: Array) {
+ initialiseSerialization()
+ val portNo = args.singleOrNull() ?: throw IllegalArgumentException("Please specify a port number")
+ val hostAndPort = NetworkHostAndPort("localhost", portNo.toIntOrNull() ?: 13000)
+ log.info("Starting RPC Proxy Server on [$hostAndPort] ...")
+ RPCProxyServer(hostAndPort, webService = RPCProxyWebService(hostAndPort)).start()
+}
\ No newline at end of file
diff --git a/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyWebService.kt b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyWebService.kt
new file mode 100644
index 0000000000..10ba274a76
--- /dev/null
+++ b/testing/qa/behave/tools/rpc-proxy/src/main/kotlin/net/corda/behave/service/proxy/RPCProxyWebService.kt
@@ -0,0 +1,163 @@
+package net.corda.behave.service.proxy
+
+import net.corda.behave.service.proxy.RPCProxyWebService.Companion.RPC_PROXY_PATH
+import net.corda.client.rpc.CordaRPCClient
+import net.corda.client.rpc.CordaRPCClientConfiguration
+import net.corda.client.rpc.internal.KryoClientSerializationScheme
+import net.corda.core.contracts.ContractState
+import net.corda.core.flows.FlowLogic
+import net.corda.core.messaging.CordaRPCOps
+import net.corda.core.serialization.deserialize
+import net.corda.core.serialization.internal.effectiveSerializationEnv
+import net.corda.core.serialization.serialize
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.core.utilities.contextLogger
+import net.corda.core.utilities.getOrThrow
+import net.corda.core.utilities.seconds
+import java.io.InputStream
+import javax.servlet.http.HttpServletRequest
+import javax.ws.rs.Consumes
+import javax.ws.rs.GET
+import javax.ws.rs.POST
+import javax.ws.rs.Path
+import javax.ws.rs.core.Context
+import javax.ws.rs.core.MediaType
+import javax.ws.rs.core.Response
+import javax.ws.rs.core.Response.status
+
+@Path(RPC_PROXY_PATH)
+class RPCProxyWebService(targetHostAndPort: NetworkHostAndPort) {
+
+ // see "NetworkInterface" port allocation definitions
+ private val targetPort = targetHostAndPort.port - 1000
+
+ init {
+ try {
+ effectiveSerializationEnv
+ } catch (e: IllegalStateException) {
+ try {
+ KryoClientSerializationScheme.initialiseSerialization()
+ } catch (e: IllegalStateException) {
+ // Race e.g. two of these constructed in parallel, ignore.
+ }
+ }
+ }
+
+ companion object {
+ private val log = contextLogger()
+ const val DEFAULT_PASSWORD = "S0meS3cretW0rd"
+ const val RPC_PROXY_PATH = "rpc"
+ }
+
+ @GET
+ @Path("my-ip")
+ fun myIp(@Context request: HttpServletRequest): Response {
+ return createResponse("HELLO! My ip is ${request.remoteHost}:${request.remotePort}")
+ }
+
+ @GET
+ @Path("node-info")
+ fun nodeInfo(@Context request: HttpServletRequest): Response {
+ log.info("nodeInfo")
+ return use {
+ it.nodeInfo()
+ }
+ }
+
+ @GET
+ @Path("registered-flows")
+ fun registeredFlows(@Context request: HttpServletRequest): Response {
+ log.info("registeredFlows")
+ return use {
+ it.registeredFlows()
+ }
+ }
+
+ @GET
+ @Path("notary-identities")
+ fun notaryIdentities(@Context request: HttpServletRequest): Response {
+ log.info("networkMapSnapshot")
+ return use {
+ it.notaryIdentities()
+ }
+ }
+
+ @GET
+ @Path("network-map-snapshot")
+ fun networkMapSnapshot(@Context request: HttpServletRequest): Response {
+ log.info("networkMapSnapshot")
+ return use {
+ it.networkMapSnapshot()
+ }
+ }
+
+ @POST
+ @Path("parties-from-name")
+ @Consumes(MediaType.APPLICATION_OCTET_STREAM)
+ fun partiesFromName(input: InputStream): Response {
+ log.info("partiesFromName")
+ val queryName = input.readBytes().deserialize()
+ return use {
+ it.partiesFromName(queryName, false)
+ }
+ }
+
+ @POST
+ @Path("vault-query")
+ @Consumes(MediaType.APPLICATION_OCTET_STREAM)
+ fun vaultQuery(input: InputStream): Response {
+ log.info("vaultQuery")
+ val contractStateType = input.readBytes().deserialize()
+ val clazz = Class.forName(contractStateType) as Class
+ return use {
+ log.info("Calling vaultQuery with: $clazz")
+ it.vaultQuery(clazz)
+ }
+ }
+
+ @POST
+ @Path("start-flow")
+ @Consumes(MediaType.APPLICATION_OCTET_STREAM)
+ fun startFlow(input: InputStream): Response {
+ log.info("startFlow")
+ return use { rpcClient ->
+ val argsList = input.readBytes().deserialize>()
+ for (i in argsList.indices) {
+ log.info("$i: ${argsList[i]}")
+ }
+ val flowClass = Class.forName(argsList[0] as String) as Class>
+ val flowArgs = argsList.drop(1).toTypedArray()
+ log.info("Calling flow: $flowClass with arguments: ${flowArgs.asList()}")
+ rpcClient.startFlowDynamic(flowClass, *flowArgs).returnValue.getOrThrow()
+ }
+ }
+
+ private fun use(action: (CordaRPCOps) -> T): Response {
+ val targetHost = NetworkHostAndPort("localhost", targetPort)
+ val config = object : CordaRPCClientConfiguration {
+ override val connectionMaxRetryInterval = 10.seconds
+ }
+ log.info("Establishing RPC connection to ${targetHost.host} on port ${targetHost.port} ...")
+ return try {
+ CordaRPCClient(targetHost, config).use("corda", DEFAULT_PASSWORD) {
+ log.info("RPC connection to ${targetHost.host}:${targetHost.port} established")
+ val client = it.proxy
+ val result = action(client)
+ log.info("CordaRPCOps result: $result")
+ return createResponse(result)
+ }
+ } catch (e: Exception) {
+ log.warn("RPC Proxy request failed: ", e)
+ e.printStackTrace()
+ status(Response.Status.INTERNAL_SERVER_ERROR).encoding(e.message)
+ }.build()
+ }
+
+ private fun createResponse(payload: Any?): Response {
+ return if (payload != null) {
+ Response.ok(payload.serialize().bytes)
+ } else {
+ status(Response.Status.NOT_FOUND)
+ }.build()
+ }
+}
diff --git a/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyServerTest.kt b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyServerTest.kt
new file mode 100644
index 0000000000..7d93eaa863
--- /dev/null
+++ b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyServerTest.kt
@@ -0,0 +1,26 @@
+package net.corda.behave.service.proxy
+
+import net.corda.core.internal.checkOkResponse
+import net.corda.core.internal.openHttpConnection
+import net.corda.core.utilities.NetworkHostAndPort
+import org.junit.Test
+import java.net.URL
+
+class RPCProxyServerTest {
+
+ private val rpcProxyHostAndPort = NetworkHostAndPort("localhost", 13000)
+ private val nodeHostAndPort = NetworkHostAndPort("localhost", 12000)
+
+ @Test
+ fun `execute RPCOp`() {
+ RPCProxyServer(rpcProxyHostAndPort,
+ webService = RPCProxyWebService(nodeHostAndPort)).use {
+ it.start()
+ it.doGet("my-ip")
+ }
+ }
+
+ private fun RPCProxyServer.doGet(path: String) {
+ return URL("http://$rpcProxyHostAndPort/rpc/$path").openHttpConnection().checkOkResponse()
+ }
+}
\ No newline at end of file
diff --git a/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyWebServiceTest.kt b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyWebServiceTest.kt
new file mode 100644
index 0000000000..73063737ff
--- /dev/null
+++ b/testing/qa/behave/tools/rpc-proxy/src/smoke-test/kotlin/net/corda/behave/service/proxy/RPCProxyWebServiceTest.kt
@@ -0,0 +1,329 @@
+package net.corda.behave.service.proxy
+
+import net.corda.core.internal.openHttpConnection
+import net.corda.core.internal.responseAs
+import net.corda.core.internal.sumByLong
+import net.corda.core.messaging.startFlow
+import net.corda.core.node.services.Vault
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.core.utilities.OpaqueBytes
+import net.corda.core.utilities.getOrThrow
+import net.corda.finance.DOLLARS
+import net.corda.finance.POUNDS
+import net.corda.finance.SWISS_FRANCS
+import net.corda.finance.contracts.asset.Cash
+import net.corda.finance.flows.CashExitFlow
+import net.corda.finance.flows.CashIssueFlow
+import net.corda.finance.flows.CashPaymentFlow
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Ignore
+import org.junit.Test
+import java.net.URL
+
+class RPCProxyWebServiceTest {
+
+ /**
+ * client -> HTTPtoRPCProxy -> Corda Node
+ *
+ * Please note these tests require a running network with at 3 Nodes
+ * (listening on ports 12002, 12007, and 12012) and a Notary
+ */
+ private val hostAndPort = NetworkHostAndPort("localhost", 13002)
+ private val rpcProxyClient = CordaRPCProxyClient(hostAndPort)
+
+ private val hostAndPortB = NetworkHostAndPort("localhost", 13007)
+ private val rpcProxyClientB = CordaRPCProxyClient(hostAndPortB)
+
+ private val hostAndPortC = NetworkHostAndPort("localhost", 13012)
+ private val rpcProxyClientC = CordaRPCProxyClient(hostAndPortC)
+
+ @Test
+ fun myIp() {
+ val response = doGet("my-ip")
+ println(response)
+ assertTrue(response.contains("My ip is"))
+ }
+
+ @Test
+ fun nodeInfo() {
+ val response = rpcProxyClient.nodeInfo()
+ println(response)
+ assertThat(response.toString()).matches("NodeInfo\\(addresses=\\[.*\\], legalIdentitiesAndCerts=\\[.*\\], platformVersion=.*, serial=.*\\)")
+ }
+
+ @Test
+ fun registeredFlows() {
+ val response = rpcProxyClient.registeredFlows()
+ println(response)
+ // Node built-in flows
+ assertThat(response).contains("net.corda.core.flows.ContractUpgradeFlow\$Authorise",
+ "net.corda.core.flows.ContractUpgradeFlow\$Deauthorise",
+ "net.corda.core.flows.ContractUpgradeFlow\$Initiate")
+ }
+
+ @Test
+ fun notaryIdentities() {
+ val response = rpcProxyClient.notaryIdentities()
+ println(response)
+ assertThat(response.first().name.toString()).isEqualTo("O=Notary, L=London, C=GB")
+ }
+
+ @Test
+ fun networkMapSnapshot() {
+ val response = rpcProxyClient.networkMapSnapshot()
+ println(response)
+ assertThat(response).contains(rpcProxyClient.nodeInfo())
+ assertThat(response.size).isEqualTo(4)
+ }
+
+ @Test
+ fun startFlowCashIssuePartyA() {
+ val notary = rpcProxyClient.notaryIdentities()[0]
+ val response = rpcProxyClient.startFlow(::CashIssueFlow, POUNDS(500), OpaqueBytes.of(1), notary)
+ val result = response.returnValue.getOrThrow().stx
+ println(result)
+ assertThat(result.toString()).matches("SignedTransaction\\(id=.*\\)")
+ }
+
+ @Test
+ fun startFlowCashIssuePartyB() {
+ val notary = rpcProxyClientB.notaryIdentities()[0]
+ val response = rpcProxyClientB.startFlow(::CashIssueFlow, DOLLARS(1000), OpaqueBytes.of(1), notary)
+ val result = response.returnValue.getOrThrow().stx
+ println(result)
+ assertThat(result.toString()).matches("SignedTransaction\\(id=.*\\)")
+ }
+
+ @Test
+ fun startFlowCashPaymentToPartyC() {
+ val recipient = rpcProxyClientB.partiesFromName("PartyC", false).first()
+ val response = rpcProxyClientB.startFlow(::CashPaymentFlow, DOLLARS(100), recipient)
+ val result = response.returnValue.getOrThrow().stx
+ println(result)
+ assertThat(result.toString()).matches("SignedTransaction\\(id=.*\\)")
+ }
+
+ @Test
+ fun startFlowCashPaymentToPartyB() {
+ val recipient = rpcProxyClient.partiesFromName("PartyB", false).first()
+ val response = rpcProxyClient.startFlow(::CashPaymentFlow, POUNDS(250), recipient)
+ val result = response.returnValue.getOrThrow().stx
+ println(result)
+ assertThat(result.toString()).matches("SignedTransaction\\(id=.*\\)")
+ }
+
+ @Test
+ fun startFlowCashPaymentToPartyA() {
+ val recipient = rpcProxyClientB.partiesFromName("PartyA", false).first()
+ val response = rpcProxyClientB.startFlow(::CashPaymentFlow, DOLLARS(500), recipient)
+ val result = response.returnValue.getOrThrow().stx
+ println(result)
+ assertThat(result.toString()).matches("SignedTransaction\\(id=.*\\)")
+ }
+
+ @Test
+ fun startFlowCashExit() {
+ val response = rpcProxyClient.startFlow(::CashExitFlow, POUNDS(500), OpaqueBytes.of(1))
+ val result = response.returnValue.getOrThrow().stx
+ println(result)
+ assertThat(result.toString()).matches("SignedTransaction\\(id=.*\\)")
+ }
+
+ @Test
+ fun startMultiPartyCashFlows() {
+ val notary = rpcProxyClient.notaryIdentities()[0]
+
+ // Party A issue 500 GBP
+ println("Party A issuing 500 GBP")
+ rpcProxyClient.startFlow(::CashIssueFlow, POUNDS(500), OpaqueBytes.of(1), notary).returnValue.getOrThrow()
+
+ // Party B issue 1000 USD
+ println("Party B issuing 1000 USD")
+ rpcProxyClientB.startFlow(::CashIssueFlow, DOLLARS(1000), OpaqueBytes.of(1), notary).returnValue.getOrThrow()
+
+ // Party A transfers 250 GBP to Party B
+ println("Party A transferring 250 GBP to Party B")
+ val partyB = rpcProxyClient.partiesFromName("PartyB", false).first()
+ rpcProxyClient.startFlow(::CashPaymentFlow, POUNDS(250), partyB).returnValue.getOrThrow()
+
+ // Party B transfers 500 USD to Party A
+ println("Party B transferring 500 USD to Party A")
+ val partyA = rpcProxyClientB.partiesFromName("PartyA", false).first()
+ rpcProxyClientB.startFlow(::CashPaymentFlow, DOLLARS(500), partyA).returnValue.getOrThrow()
+
+ // Party B transfers back 125 GBP to Party A
+ println("Party B transferring back 125 GBP to Party A")
+ rpcProxyClientB.startFlow(::CashPaymentFlow, POUNDS(125), partyA).returnValue.getOrThrow()
+
+ // Party A transfers back 250 USD to Party B
+ println("Party A transferring back 250 USD to Party B")
+ rpcProxyClient.startFlow(::CashPaymentFlow, DOLLARS(250), partyB).returnValue.getOrThrow()
+
+ // Query the Vault of each respective Party
+ val responseA = rpcProxyClient.vaultQuery(Cash.State::class.java)
+ responseA.states.forEach { state ->
+ println("PartyA: ${state.state.data.amount}")
+ }
+
+ val responseB = rpcProxyClientB.vaultQuery(Cash.State::class.java)
+ responseB.states.forEach { state ->
+ println("PartyB: ${state.state.data.amount}")
+ }
+
+ assertVaultHoldsCash(responseA, responseB)
+ }
+
+ @Ignore
+ @Test
+ fun startMultiABCPartyCashFlows() {
+ val notary = rpcProxyClient.notaryIdentities()[0]
+
+ while(true) {
+ // Party A issue 500 GBP
+ println("Party A issuing 500 GBP")
+ rpcProxyClient.startFlow(::CashIssueFlow, POUNDS(500), OpaqueBytes.of(1), notary).returnValue.getOrThrow()
+
+ // Party B issue 500 USD
+ println("Party B issuing 500 USD")
+ rpcProxyClientB.startFlow(::CashIssueFlow, DOLLARS(500), OpaqueBytes.of(1), notary).returnValue.getOrThrow()
+
+ // Party C issue 500 CHF
+ println("Party C issuing 500 CHF")
+ rpcProxyClientC.startFlow(::CashIssueFlow, SWISS_FRANCS(500), OpaqueBytes.of(1), notary).returnValue.getOrThrow()
+
+ // Party A transfers 250 GBP to Party B who transfers to party C
+ println("Party A transferring 250 GBP to Party B")
+ val partyB = rpcProxyClient.partiesFromName("PartyB", false).first()
+ rpcProxyClient.startFlow(::CashPaymentFlow, POUNDS(250), partyB).returnValue.getOrThrow()
+
+ println(" ... and forwarding to Party C")
+ val partyC = rpcProxyClientB.partiesFromName("PartyC", false).first()
+ rpcProxyClientB.startFlow(::CashPaymentFlow, POUNDS(250), partyC).returnValue.getOrThrow()
+
+ // Party B transfers 500 USD to Party C who transfers to party A
+ println("Party B transferring 500 USD to Party C")
+ rpcProxyClientB.startFlow(::CashPaymentFlow, DOLLARS(250), partyC).returnValue.getOrThrow()
+
+ println(" ... and forwarding to Party A")
+ val partyA = rpcProxyClientC.partiesFromName("PartyA", false).first()
+ rpcProxyClientC.startFlow(::CashPaymentFlow, DOLLARS(250), partyA).returnValue.getOrThrow()
+
+ // Party C transfers 550 CHF to Party A who transfers to party B
+ println("Party C transferring 250 CHF to Party A")
+ rpcProxyClientC.startFlow(::CashPaymentFlow, SWISS_FRANCS(250), partyA).returnValue.getOrThrow()
+
+ println(" ... and forwarding to Party B")
+ rpcProxyClient.startFlow(::CashPaymentFlow, SWISS_FRANCS(250), partyB).returnValue.getOrThrow()
+
+ // Query the Vault of each respective Party
+ val responseA = rpcProxyClient.vaultQuery(Cash.State::class.java)
+ responseA.states.forEach { state ->
+ println("PartyA: ${state.state.data.amount}")
+ }
+
+ val responseB = rpcProxyClientB.vaultQuery(Cash.State::class.java)
+ responseB.states.forEach { state ->
+ println("PartyB: ${state.state.data.amount}")
+ }
+
+ val responseC = rpcProxyClientC.vaultQuery(Cash.State::class.java)
+ responseC.states.forEach { state ->
+ println("PartyC: ${state.state.data.amount}")
+ }
+
+ println("============================================================================================")
+ }
+ }
+
+ // enable Flow Draining on Node B
+ @Ignore
+ @Test
+ fun startMultiACPartyCashFlows() {
+ val notary = rpcProxyClient.notaryIdentities()[0]
+
+ while(true) {
+ // Party A issue 500 GBP
+ println("Party A issuing 500 GBP")
+ rpcProxyClient.startFlow(::CashIssueFlow, POUNDS(500), OpaqueBytes.of(1), notary).returnValue.getOrThrow()
+
+ // Party C issue 500 CHF
+ println("Party C issuing 500 CHF")
+ rpcProxyClientC.startFlow(::CashIssueFlow, SWISS_FRANCS(500), OpaqueBytes.of(1), notary).returnValue.getOrThrow()
+
+ // Party A transfers 250 GBP to Party B who transfers to party C
+ println("Party A transferring 250 GBP to Party B")
+ val partyB = rpcProxyClient.partiesFromName("PartyB", false).first()
+ rpcProxyClient.startFlow(::CashPaymentFlow, POUNDS(250), partyB) // BLOCKS!!!!!
+
+ println(" ... and forwarding to Party A")
+ val partyA = rpcProxyClientC.partiesFromName("PartyA", false).first()
+ rpcProxyClientC.startFlow(::CashPaymentFlow, DOLLARS(250), partyA).returnValue.getOrThrow()
+
+ // Party C transfers 550 CHF to Party A who transfers to party B
+ println("Party C transferring 250 CHF to Party A")
+ rpcProxyClientC.startFlow(::CashPaymentFlow, SWISS_FRANCS(250), partyA).returnValue.getOrThrow()
+
+ println(" ... and forwarding to Party B")
+ rpcProxyClient.startFlow(::CashPaymentFlow, SWISS_FRANCS(250), partyB)
+
+ // Query the Vault of each respective Party
+ val responseA = rpcProxyClient.vaultQuery(Cash.State::class.java)
+ responseA.states.forEach { state ->
+ println("PartyA: ${state.state.data.amount}")
+ }
+
+ val responseB = rpcProxyClientB.vaultQuery(Cash.State::class.java)
+ responseB.states.forEach { state ->
+ println("PartyB: ${state.state.data.amount}")
+ }
+
+ val responseC = rpcProxyClientC.vaultQuery(Cash.State::class.java)
+ responseC.states.forEach { state ->
+ println("PartyC: ${state.state.data.amount}")
+ }
+
+ println("============================================================================================")
+ }
+ }
+
+ @Test
+ fun vaultQueryCash() {
+ try {
+ val responseA = rpcProxyClient.vaultQuery(Cash.State::class.java)
+ responseA.states.forEach { state ->
+ println("PartyA: ${state.state.data.amount}")
+ }
+
+ val responseB = rpcProxyClientB.vaultQuery(Cash.State::class.java)
+ responseB.states.forEach { state ->
+ println("PartyB: ${state.state.data.amount}")
+ }
+
+ val responseC = rpcProxyClientC.vaultQuery(Cash.State::class.java)
+ responseC.states.forEach { state ->
+ println("PartyC: ${state.state.data.amount}")
+ }
+
+ assertVaultHoldsCash(responseA, responseB, responseC)
+ }
+ catch (e: Exception) {
+ println("Vault Cash query error: ${e.message}")
+ fail()
+ }
+ }
+
+ private fun assertVaultHoldsCash(vararg vaultPages: Vault.Page) {
+ vaultPages.forEach { vaultPage ->
+ assertThat(vaultPage.states.size).isGreaterThan(0)
+ vaultPage.states.groupBy { it.state.data.amount.token.product.currencyCode }.forEach { _, value ->
+ assertThat(value.sumByLong { it.state.data.amount.quantity }).isGreaterThan(0L)
+ }
+ }
+ }
+
+ private inline fun doGet(path: String): T {
+ return URL("http://$hostAndPort/rpc/$path").openHttpConnection().responseAs()
+ }
+}
\ No newline at end of file
diff --git a/testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh b/testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh
new file mode 100755
index 0000000000..6cf6aa93be
--- /dev/null
+++ b/testing/qa/behave/tools/rpc-proxy/startRPCproxy.sh
@@ -0,0 +1,43 @@
+#!/bin/sh
+
+DISTRO_DIR=$1
+PORT="${2:-13000}"
+TMPDIR="${TMPDIR:-/tmp}"
+
+if [ ! -d "$DISTRO_DIR" ]; then
+ echo "Must specify location of Corda distribution (directory does not exist: $DISTRO_DIR)"
+ exit 1
+fi
+
+if [ ! -f "$DISTRO_DIR/corda.jar" ]; then
+ echo "Distribution corda.jar not found"
+ exit 1
+fi
+
+if [ ! -f "$DISTRO_DIR/corda-rpcProxy.jar" ]; then
+ echo "Distribution corda-rpcProxy.jar not found"
+ exit 1
+fi
+
+# unzip corda jars into proxy directory (if not already there)
+if [ ! -d "$DISTRO_DIR/proxy" ]; then
+ mkdir -p ${DISTRO_DIR}/proxy
+ unzip ${DISTRO_DIR}/corda.jar -d ${DISTRO_DIR}/proxy
+fi
+
+# launch proxy
+echo "Launching RPC proxy ..."
+echo "java -cp $DISTRO_DIR/corda-rpcProxy.jar:\
+\n\t$(ls ${DISTRO_DIR}/proxy/*.jar | tr '\n' ':'):\
+\n\t$(ls ${DISTRO_DIR}/apps/*.jar | tr '\n' ':')
+\
+\n\tnet.corda.behave.service.proxy.RPCProxyServerKt ${PORT}
+"
+
+/usr/bin/java -cp ${DISTRO_DIR}/corda-rpcProxy.jar:\
+$(ls ${DISTRO_DIR}/proxy/*.jar | tr '\n' ':'):\
+$(ls ${DISTRO_DIR}/apps/*.jar | tr '\n' ':') \
+net.corda.behave.service.proxy.RPCProxyServerKt ${PORT} &> rpcproxy-${PORT}.log &
+
+echo $! > ${TMPDIR}/rpcProxy-pid-${PORT}
+echo "RPCProxyServer PID: $(cat ${TMPDIR}/rpcProxy-pid-${PORT})"