mirror of
https://github.com/corda/corda.git
synced 2024-12-21 13:57:54 +00:00
Merge pull request #720 from corda/shams-flow-registration-annotation
Introducing InitiatedBy annotation to be used on initiated flows to s…
This commit is contained in:
commit
37e183652e
@ -147,6 +147,12 @@ operator fun String.div(other: String) = Paths.get(this) / other
|
|||||||
fun Path.createDirectory(vararg attrs: FileAttribute<*>): Path = Files.createDirectory(this, *attrs)
|
fun Path.createDirectory(vararg attrs: FileAttribute<*>): Path = Files.createDirectory(this, *attrs)
|
||||||
fun Path.createDirectories(vararg attrs: FileAttribute<*>): Path = Files.createDirectories(this, *attrs)
|
fun Path.createDirectories(vararg attrs: FileAttribute<*>): Path = Files.createDirectories(this, *attrs)
|
||||||
fun Path.exists(vararg options: LinkOption): Boolean = Files.exists(this, *options)
|
fun Path.exists(vararg options: LinkOption): Boolean = Files.exists(this, *options)
|
||||||
|
fun Path.copyToDirectory(targetDir: Path, vararg options: CopyOption): Path {
|
||||||
|
require(targetDir.isDirectory()) { "$targetDir is not a directory" }
|
||||||
|
val targetFile = targetDir.resolve(fileName)
|
||||||
|
Files.copy(this, targetFile, *options)
|
||||||
|
return targetFile
|
||||||
|
}
|
||||||
fun Path.moveTo(target: Path, vararg options: CopyOption): Path = Files.move(this, target, *options)
|
fun Path.moveTo(target: Path, vararg options: CopyOption): Path = Files.move(this, target, *options)
|
||||||
fun Path.isRegularFile(vararg options: LinkOption): Boolean = Files.isRegularFile(this, *options)
|
fun Path.isRegularFile(vararg options: LinkOption): Boolean = Files.isRegularFile(this, *options)
|
||||||
fun Path.isDirectory(vararg options: LinkOption): Boolean = Files.isDirectory(this, *options)
|
fun Path.isDirectory(vararg options: LinkOption): Boolean = Files.isDirectory(this, *options)
|
||||||
|
16
core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt
Normal file
16
core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package net.corda.core.flows
|
||||||
|
|
||||||
|
import kotlin.annotation.AnnotationTarget.CLASS
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This annotation is required by any [FlowLogic] that is designed to be initiated by a counterparty flow. The flow that
|
||||||
|
* does the initiating is specified by the [value] property and itself must be annotated with [InitiatingFlow].
|
||||||
|
*
|
||||||
|
* The node on startup scans for [FlowLogic]s which are annotated with this and automatically registers the initiating
|
||||||
|
* to initiated flow mapping.
|
||||||
|
*
|
||||||
|
* @see InitiatingFlow
|
||||||
|
*/
|
||||||
|
@Target(CLASS)
|
||||||
|
annotation class InitiatedBy(val value: KClass<out FlowLogic<*>>)
|
@ -5,8 +5,7 @@ import kotlin.annotation.AnnotationTarget.CLASS
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This annotation is required by any [FlowLogic] which has been designated to initiate communication with a counterparty
|
* This annotation is required by any [FlowLogic] which has been designated to initiate communication with a counterparty
|
||||||
* and request they start their side of the flow communication. To ensure that this is correctly applied
|
* and request they start their side of the flow communication.
|
||||||
* [net.corda.core.node.PluginServiceHub.registerServiceFlow] checks the initiating flow class has this annotation.
|
|
||||||
*
|
*
|
||||||
* There is also an optional [version] property, which defaults to 1, to specify the version of the flow protocol. This
|
* There is also an optional [version] property, which defaults to 1, to specify the version of the flow protocol. This
|
||||||
* integer value should be incremented whenever there is a release of this flow which has changes that are not backwards
|
* integer value should be incremented whenever there is a release of this flow which has changes that are not backwards
|
||||||
@ -19,6 +18,11 @@ import kotlin.annotation.AnnotationTarget.CLASS
|
|||||||
*
|
*
|
||||||
* The flow version number is similar in concept to Corda's platform version but they are not the same. A flow's version
|
* The flow version number is similar in concept to Corda's platform version but they are not the same. A flow's version
|
||||||
* number can change independently of the platform version.
|
* number can change independently of the platform version.
|
||||||
|
*
|
||||||
|
* If you are customising an existing initiating flow by sub-classing it then there's no need to specify this annotation
|
||||||
|
* again. In fact doing so is an error and checks are made to make sure this doesn't occur.
|
||||||
|
*
|
||||||
|
* @see InitiatedBy
|
||||||
*/
|
*/
|
||||||
// TODO Add support for multiple versions once CorDapps are loaded in separate class loaders
|
// TODO Add support for multiple versions once CorDapps are loaded in separate class loaders
|
||||||
@Target(CLASS)
|
@Target(CLASS)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.core.flows
|
||||||
|
|
||||||
import java.lang.annotation.Inherited
|
|
||||||
import kotlin.annotation.AnnotationTarget.CLASS
|
import kotlin.annotation.AnnotationTarget.CLASS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -9,7 +8,6 @@ import kotlin.annotation.AnnotationTarget.CLASS
|
|||||||
* flow will not be allowed to start and an exception will be thrown.
|
* flow will not be allowed to start and an exception will be thrown.
|
||||||
*/
|
*/
|
||||||
@Target(CLASS)
|
@Target(CLASS)
|
||||||
@Inherited
|
|
||||||
@MustBeDocumented
|
@MustBeDocumented
|
||||||
// TODO Consider a different name, something along the lines of SchedulableFlow
|
// TODO Consider a different name, something along the lines of SchedulableFlow
|
||||||
annotation class StartableByRPC
|
annotation class StartableByRPC
|
@ -15,7 +15,7 @@ import java.security.cert.X509Certificate
|
|||||||
* This is intended for use as a subflow of another flow.
|
* This is intended for use as a subflow of another flow.
|
||||||
*/
|
*/
|
||||||
object TxKeyFlow {
|
object TxKeyFlow {
|
||||||
abstract class AbstractIdentityFlow(val otherSide: Party, val revocationEnabled: Boolean): FlowLogic<Map<Party, AnonymousIdentity>>() {
|
abstract class AbstractIdentityFlow<out T>(val otherSide: Party): FlowLogic<T>() {
|
||||||
fun validateIdentity(untrustedIdentity: Pair<X509CertificateHolder, CertPath>): AnonymousIdentity {
|
fun validateIdentity(untrustedIdentity: Pair<X509CertificateHolder, CertPath>): AnonymousIdentity {
|
||||||
val (wellKnownCert, certPath) = untrustedIdentity
|
val (wellKnownCert, certPath) = untrustedIdentity
|
||||||
val theirCert = certPath.certificates.last()
|
val theirCert = certPath.certificates.last()
|
||||||
@ -26,20 +26,21 @@ object TxKeyFlow {
|
|||||||
val anonymousParty = AnonymousParty(theirCert.publicKey)
|
val anonymousParty = AnonymousParty(theirCert.publicKey)
|
||||||
serviceHub.identityService.registerPath(wellKnownCert, anonymousParty, certPath)
|
serviceHub.identityService.registerPath(wellKnownCert, anonymousParty, certPath)
|
||||||
AnonymousIdentity(certPath, X509CertificateHolder(theirCert.encoded), anonymousParty)
|
AnonymousIdentity(certPath, X509CertificateHolder(theirCert.encoded), anonymousParty)
|
||||||
} else
|
} else {
|
||||||
throw IllegalStateException("Expected certificate subject to be ${otherSide.name} but found ${certName}")
|
throw IllegalStateException("Expected certificate subject to be ${otherSide.name} but found $certName")
|
||||||
} else
|
}
|
||||||
|
} else {
|
||||||
throw IllegalStateException("Expected an X.509 certificate but received ${theirCert.javaClass.name}")
|
throw IllegalStateException("Expected an X.509 certificate but received ${theirCert.javaClass.name}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
class Requester(otherSide: Party,
|
class Requester(otherSide: Party,
|
||||||
revocationEnabled: Boolean,
|
val revocationEnabled: Boolean,
|
||||||
override val progressTracker: ProgressTracker) : AbstractIdentityFlow(otherSide, revocationEnabled) {
|
override val progressTracker: ProgressTracker) : AbstractIdentityFlow<Map<Party, AnonymousIdentity>>(otherSide) {
|
||||||
constructor(otherSide: Party,
|
constructor(otherSide: Party, revocationEnabled: Boolean) : this(otherSide, revocationEnabled, tracker())
|
||||||
revocationEnabled: Boolean) : this(otherSide, revocationEnabled, tracker())
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
object AWAITING_KEY : ProgressTracker.Step("Awaiting key")
|
object AWAITING_KEY : ProgressTracker.Step("Awaiting key")
|
||||||
@ -62,26 +63,21 @@ object TxKeyFlow {
|
|||||||
* Flow which waits for a key request from a counterparty, generates a new key and then returns it to the
|
* Flow which waits for a key request from a counterparty, generates a new key and then returns it to the
|
||||||
* counterparty and as the result from the flow.
|
* counterparty and as the result from the flow.
|
||||||
*/
|
*/
|
||||||
class Provider(otherSide: Party,
|
@InitiatedBy(Requester::class)
|
||||||
revocationEnabled: Boolean,
|
class Provider(otherSide: Party) : AbstractIdentityFlow<Unit>(otherSide) {
|
||||||
override val progressTracker: ProgressTracker) : AbstractIdentityFlow(otherSide,revocationEnabled) {
|
|
||||||
constructor(otherSide: Party,
|
|
||||||
revocationEnabled: Boolean = false) : this(otherSide, revocationEnabled, tracker())
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
object SENDING_KEY : ProgressTracker.Step("Sending key")
|
object SENDING_KEY : ProgressTracker.Step("Sending key")
|
||||||
|
|
||||||
fun tracker() = ProgressTracker(SENDING_KEY)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val progressTracker: ProgressTracker = ProgressTracker(SENDING_KEY)
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): Map<Party, AnonymousIdentity> {
|
override fun call() {
|
||||||
|
val revocationEnabled = false
|
||||||
progressTracker.currentStep = SENDING_KEY
|
progressTracker.currentStep = SENDING_KEY
|
||||||
val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentity, revocationEnabled)
|
val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentity, revocationEnabled)
|
||||||
send(otherSide, myIdentityFragment)
|
send(otherSide, myIdentityFragment)
|
||||||
val theirIdentity = receive<Pair<X509CertificateHolder, CertPath>>(otherSide).unwrap { validateIdentity(it) }
|
receive<Pair<X509CertificateHolder, CertPath>>(otherSide).unwrap { validateIdentity(it) }
|
||||||
return mapOf(Pair(otherSide, AnonymousIdentity(myIdentityFragment)),
|
|
||||||
Pair(serviceHub.myInfo.legalIdentity, theirIdentity))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,9 @@ abstract class CordaPluginRegistry {
|
|||||||
* The [PluginServiceHub] will be fully constructed before the plugin service is created and will
|
* The [PluginServiceHub] will be fully constructed before the plugin service is created and will
|
||||||
* allow access to the Flow factory and Flow initiation entry points there.
|
* allow access to the Flow factory and Flow initiation entry points there.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
@Deprecated("This is no longer used. If you need to create your own service, such as an oracle, then use the " +
|
||||||
|
"@CordaService annotation. For flow registrations use @InitiatedBy.", level = DeprecationLevel.ERROR)
|
||||||
open val servicePlugins: List<Function<PluginServiceHub, out Any>> get() = emptyList()
|
open val servicePlugins: List<Function<PluginServiceHub, out Any>> get() = emptyList()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,22 +7,7 @@ import net.corda.core.identity.Party
|
|||||||
* A service hub to be used by the [CordaPluginRegistry]
|
* A service hub to be used by the [CordaPluginRegistry]
|
||||||
*/
|
*/
|
||||||
interface PluginServiceHub : ServiceHub {
|
interface PluginServiceHub : ServiceHub {
|
||||||
/**
|
@Deprecated("This is no longer used. Instead annotate the flows produced by your factory with @InitiatedBy and have " +
|
||||||
* Register the service flow factory to use when an initiating party attempts to communicate with us. The registration
|
"them point to the initiating flow class.", level = DeprecationLevel.ERROR)
|
||||||
* is done against the [Class] object of the client flow to the service flow. What this means is if a counterparty
|
fun registerFlowInitiator(initiatingFlowClass: Class<out FlowLogic<*>>, serviceFlowFactory: (Party) -> FlowLogic<*>) = Unit
|
||||||
* starts a [FlowLogic] represented by [initiatingFlowClass] and starts communication with us, we will execute the service
|
|
||||||
* flow produced by [serviceFlowFactory]. This service flow has respond correctly to the sends and receives the client
|
|
||||||
* does.
|
|
||||||
* @param initiatingFlowClass [Class] of the client flow involved in this client-server communication.
|
|
||||||
* @param serviceFlowFactory Lambda which produces a new service flow for each new client flow communication. The
|
|
||||||
* [Party] parameter of the factory is the client's identity.
|
|
||||||
* @throws IllegalArgumentException If [initiatingFlowClass] is not annotated with [net.corda.core.flows.InitiatingFlow].
|
|
||||||
*/
|
|
||||||
fun registerServiceFlow(initiatingFlowClass: Class<out FlowLogic<*>>, serviceFlowFactory: (Party) -> FlowLogic<*>)
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
@Deprecated("This is scheduled to be removed in a future release", ReplaceWith("registerServiceFlow"))
|
|
||||||
fun registerFlowInitiator(markerClass: Class<*>, flowFactory: (Party) -> FlowLogic<*>) {
|
|
||||||
registerServiceFlow(markerClass as Class<out FlowLogic<*>>, flowFactory)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package net.corda.core.node
|
|||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.DigitalSignature
|
import net.corda.core.crypto.DigitalSignature
|
||||||
import net.corda.core.node.services.*
|
import net.corda.core.node.services.*
|
||||||
|
import net.corda.core.serialization.SerializeAsToken
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -44,6 +45,13 @@ interface ServiceHub : ServicesForResolution {
|
|||||||
val clock: Clock
|
val clock: Clock
|
||||||
val myInfo: NodeInfo
|
val myInfo: NodeInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the singleton instance of the given Corda service type. This is a class that is annotated with
|
||||||
|
* [CordaService] and will have automatically been registered by the node.
|
||||||
|
* @throws IllegalArgumentException If [type] is not annotated with [CordaService] or if the instance is not found.
|
||||||
|
*/
|
||||||
|
fun <T : SerializeAsToken> cordaService(type: Class<T>): T
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a [SignedTransaction], writes it to the local storage for validated transactions and then
|
* Given a [SignedTransaction], writes it to the local storage for validated transactions and then
|
||||||
* sends them to the vault for further processing. Expects to be run within a database transaction.
|
* sends them to the vault for further processing. Expects to be run within a database transaction.
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
package net.corda.core.node.services
|
||||||
|
|
||||||
|
import kotlin.annotation.AnnotationTarget.CLASS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotate any class that needs to be a long-lived service within the node, such as an oracle, with this annotation.
|
||||||
|
* Such a class needs to have a constructor with a single parameter of type [net.corda.core.node.PluginServiceHub]. This
|
||||||
|
* construtor will be invoked during node start to initialise the service. The service hub provided can be used to get
|
||||||
|
* information about the node that may be necessary for the service. Corda services are created as singletons within
|
||||||
|
* the node and are available to flows via [net.corda.core.node.ServiceHub.cordaService].
|
||||||
|
*
|
||||||
|
* The service class has to implement [net.corda.core.serialization.SerializeAsToken] to ensure correct usage within flows.
|
||||||
|
* (If possible extend [net.corda.core.serialization.SingletonSerializeAsToken] instead as it removes the boilerplate.)
|
||||||
|
*/
|
||||||
|
// TODO Handle the singleton serialisation of Corda services automatically, removing the need to implement SerializeAsToken
|
||||||
|
// TODO Currently all nodes which load the plugin will attempt to load the service even if it's not revelant to them. The
|
||||||
|
// underlying problem is that the entire CorDapp jar is used as a dependency, when in fact it's just the client-facing
|
||||||
|
// bit of the CorDapp that should be depended on (e.g. the initiating flows).
|
||||||
|
@Target(CLASS)
|
||||||
|
annotation class CordaService
|
@ -2,21 +2,24 @@ package net.corda.core.utilities
|
|||||||
|
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
// TODO This doesn't belong in core and can be moved into node
|
||||||
object ProcessUtilities {
|
object ProcessUtilities {
|
||||||
inline fun <reified C : Any> startJavaProcess(
|
inline fun <reified C : Any> startJavaProcess(
|
||||||
arguments: List<String>,
|
arguments: List<String>,
|
||||||
|
classpath: String = defaultClassPath,
|
||||||
jdwpPort: Int? = null,
|
jdwpPort: Int? = null,
|
||||||
extraJvmArguments: List<String> = emptyList(),
|
extraJvmArguments: List<String> = emptyList(),
|
||||||
inheritIO: Boolean = true,
|
inheritIO: Boolean = true,
|
||||||
errorLogPath: Path? = null,
|
errorLogPath: Path? = null,
|
||||||
workingDirectory: Path? = null
|
workingDirectory: Path? = null
|
||||||
): Process {
|
): Process {
|
||||||
return startJavaProcess(C::class.java.name, arguments, jdwpPort, extraJvmArguments, inheritIO, errorLogPath, workingDirectory)
|
return startJavaProcess(C::class.java.name, arguments, classpath, jdwpPort, extraJvmArguments, inheritIO, errorLogPath, workingDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startJavaProcess(
|
fun startJavaProcess(
|
||||||
className: String,
|
className: String,
|
||||||
arguments: List<String>,
|
arguments: List<String>,
|
||||||
|
classpath: String = defaultClassPath,
|
||||||
jdwpPort: Int? = null,
|
jdwpPort: Int? = null,
|
||||||
extraJvmArguments: List<String> = emptyList(),
|
extraJvmArguments: List<String> = emptyList(),
|
||||||
inheritIO: Boolean = true,
|
inheritIO: Boolean = true,
|
||||||
@ -24,7 +27,6 @@ object ProcessUtilities {
|
|||||||
workingDirectory: Path? = null
|
workingDirectory: Path? = null
|
||||||
): Process {
|
): Process {
|
||||||
val separator = System.getProperty("file.separator")
|
val separator = System.getProperty("file.separator")
|
||||||
val classpath = System.getProperty("java.class.path")
|
|
||||||
val javaPath = System.getProperty("java.home") + separator + "bin" + separator + "java"
|
val javaPath = System.getProperty("java.home") + separator + "bin" + separator + "java"
|
||||||
val debugPortArgument = if (jdwpPort != null) {
|
val debugPortArgument = if (jdwpPort != null) {
|
||||||
listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$jdwpPort")
|
listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$jdwpPort")
|
||||||
@ -44,4 +46,6 @@ object ProcessUtilities {
|
|||||||
if (workingDirectory != null) directory(workingDirectory.toFile())
|
if (workingDirectory != null) directory(workingDirectory.toFile())
|
||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val defaultClassPath: String get() = System.getProperty("java.class.path")
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
package net.corda.core.flows;
|
package net.corda.core.flows;
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.*;
|
import co.paralleluniverse.fibers.Suspendable;
|
||||||
import net.corda.core.identity.Party;
|
import net.corda.core.identity.Party;
|
||||||
import net.corda.testing.node.*;
|
import net.corda.testing.node.MockNetwork;
|
||||||
import org.junit.*;
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
import static org.assertj.core.api.AssertionsForClassTypes.*;
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||||
|
|
||||||
public class FlowsInJavaTest {
|
public class FlowsInJavaTest {
|
||||||
|
|
||||||
@ -30,13 +32,12 @@ public class FlowsInJavaTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void suspendableActionInsideUnwrap() throws Exception {
|
public void suspendableActionInsideUnwrap() throws Exception {
|
||||||
node2.getServices().registerServiceFlow(SendInUnwrapFlow.class, (otherParty) -> new OtherFlow(otherParty, "Hello"));
|
node2.registerInitiatedFlow(SendHelloAndThenReceive.class);
|
||||||
Future<String> result = node1.getServices().startFlow(new SendInUnwrapFlow(node2.getInfo().getLegalIdentity())).getResultFuture();
|
Future<String> result = node1.getServices().startFlow(new SendInUnwrapFlow(node2.getInfo().getLegalIdentity())).getResultFuture();
|
||||||
net.runNetwork();
|
net.runNetwork();
|
||||||
assertThat(result.get()).isEqualTo("Hello");
|
assertThat(result.get()).isEqualTo("Hello");
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
private static class SendInUnwrapFlow extends FlowLogic<String> {
|
private static class SendInUnwrapFlow extends FlowLogic<String> {
|
||||||
private final Party otherParty;
|
private final Party otherParty;
|
||||||
@ -55,19 +56,18 @@ public class FlowsInJavaTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class OtherFlow extends FlowLogic<String> {
|
@InitiatedBy(SendInUnwrapFlow.class)
|
||||||
|
private static class SendHelloAndThenReceive extends FlowLogic<String> {
|
||||||
private final Party otherParty;
|
private final Party otherParty;
|
||||||
private final String payload;
|
|
||||||
|
|
||||||
private OtherFlow(Party otherParty, String payload) {
|
private SendHelloAndThenReceive(Party otherParty) {
|
||||||
this.otherParty = otherParty;
|
this.otherParty = otherParty;
|
||||||
this.payload = payload;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@Override
|
@Override
|
||||||
public String call() throws FlowException {
|
public String call() throws FlowException {
|
||||||
return sendAndReceive(String.class, otherParty, payload).unwrap(data -> data);
|
return sendAndReceive(String.class, otherParty, "Hello").unwrap(data -> data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import net.corda.core.contracts.TransactionType
|
|||||||
import net.corda.core.contracts.requireThat
|
import net.corda.core.contracts.requireThat
|
||||||
import net.corda.core.getOrThrow
|
import net.corda.core.getOrThrow
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.PluginServiceHub
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
import net.corda.flows.CollectSignaturesFlow
|
import net.corda.flows.CollectSignaturesFlow
|
||||||
@ -18,7 +17,7 @@ import net.corda.testing.node.MockNetwork
|
|||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.concurrent.ExecutionException
|
import kotlin.reflect.KClass
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
class CollectSignaturesFlowTests {
|
class CollectSignaturesFlowTests {
|
||||||
@ -37,9 +36,6 @@ class CollectSignaturesFlowTests {
|
|||||||
c = nodes.partyNodes[2]
|
c = nodes.partyNodes[2]
|
||||||
notary = nodes.notaryNode.info.notaryIdentity
|
notary = nodes.notaryNode.info.notaryIdentity
|
||||||
mockNet.runNetwork()
|
mockNet.runNetwork()
|
||||||
CollectSigsTestCorDapp.registerFlows(a.services)
|
|
||||||
CollectSigsTestCorDapp.registerFlows(b.services)
|
|
||||||
CollectSigsTestCorDapp.registerFlows(c.services)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@ -47,11 +43,9 @@ class CollectSignaturesFlowTests {
|
|||||||
mockNet.stopNodes()
|
mockNet.stopNodes()
|
||||||
}
|
}
|
||||||
|
|
||||||
object CollectSigsTestCorDapp {
|
private fun registerFlowOnAllNodes(flowClass: KClass<out FlowLogic<*>>) {
|
||||||
// Would normally be called by custom service init in a CorDapp.
|
listOf(a, b, c).forEach {
|
||||||
fun registerFlows(pluginHub: PluginServiceHub) {
|
it.registerInitiatedFlow(flowClass.java)
|
||||||
pluginHub.registerFlowInitiator(TestFlow.Initiator::class.java) { TestFlow.Responder(it) }
|
|
||||||
pluginHub.registerFlowInitiator(TestFlowTwo.Initiator::class.java) { TestFlowTwo.Responder(it) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +76,7 @@ class CollectSignaturesFlowTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(TestFlow.Initiator::class)
|
||||||
class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
@ -104,7 +99,7 @@ class CollectSignaturesFlowTests {
|
|||||||
// receiving off the wire.
|
// receiving off the wire.
|
||||||
object TestFlowTwo {
|
object TestFlowTwo {
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
class Initiator(val state: DummyContract.MultiOwnerState, val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
class Initiator(val state: DummyContract.MultiOwnerState) : FlowLogic<SignedTransaction>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity
|
val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity
|
||||||
@ -118,6 +113,7 @@ class CollectSignaturesFlowTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(TestFlowTwo.Initiator::class)
|
||||||
class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
||||||
@Suspendable override fun call(): SignedTransaction {
|
@Suspendable override fun call(): SignedTransaction {
|
||||||
val flow = object : SignTransactionFlow(otherParty) {
|
val flow = object : SignTransactionFlow(otherParty) {
|
||||||
@ -137,13 +133,13 @@ class CollectSignaturesFlowTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `successfully collects two signatures`() {
|
fun `successfully collects two signatures`() {
|
||||||
|
registerFlowOnAllNodes(TestFlowTwo.Responder::class)
|
||||||
val magicNumber = 1337
|
val magicNumber = 1337
|
||||||
val parties = listOf(a.info.legalIdentity, b.info.legalIdentity, c.info.legalIdentity)
|
val parties = listOf(a.info.legalIdentity, b.info.legalIdentity, c.info.legalIdentity)
|
||||||
val state = DummyContract.MultiOwnerState(magicNumber, parties)
|
val state = DummyContract.MultiOwnerState(magicNumber, parties)
|
||||||
val flow = a.services.startFlow(TestFlowTwo.Initiator(state, b.info.legalIdentity))
|
val flow = a.services.startFlow(TestFlowTwo.Initiator(state))
|
||||||
mockNet.runNetwork()
|
mockNet.runNetwork()
|
||||||
val result = flow.resultFuture.getOrThrow()
|
val result = flow.resultFuture.getOrThrow()
|
||||||
result.verifySignatures()
|
result.verifySignatures()
|
||||||
@ -169,8 +165,8 @@ class CollectSignaturesFlowTests {
|
|||||||
val ptx = onePartyDummyContract.signWith(MINI_CORP_KEY).toSignedTransaction(false)
|
val ptx = onePartyDummyContract.signWith(MINI_CORP_KEY).toSignedTransaction(false)
|
||||||
val flow = a.services.startFlow(CollectSignaturesFlow(ptx))
|
val flow = a.services.startFlow(CollectSignaturesFlow(ptx))
|
||||||
mockNet.runNetwork()
|
mockNet.runNetwork()
|
||||||
assertFailsWith<ExecutionException>("The Initiator of CollectSignaturesFlow must have signed the transaction.") {
|
assertFailsWith<IllegalArgumentException>("The Initiator of CollectSignaturesFlow must have signed the transaction.") {
|
||||||
flow.resultFuture.get()
|
flow.resultFuture.getOrThrow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ class TxKeyFlowTests {
|
|||||||
bobNode.services.identityService.registerIdentity(notaryNode.info.legalIdentity)
|
bobNode.services.identityService.registerIdentity(notaryNode.info.legalIdentity)
|
||||||
|
|
||||||
// Run the flows
|
// Run the flows
|
||||||
bobNode.registerServiceFlow(TxKeyFlow.Requester::class) { TxKeyFlow.Provider(it) }
|
bobNode.registerInitiatedFlow(TxKeyFlow.Provider::class.java)
|
||||||
val requesterFlow = aliceNode.services.startFlow(TxKeyFlow.Requester(bob, revocationEnabled))
|
val requesterFlow = aliceNode.services.startFlow(TxKeyFlow.Requester(bob, revocationEnabled))
|
||||||
|
|
||||||
// Get the results
|
// Get the results
|
||||||
|
@ -12,10 +12,12 @@ import net.corda.core.messaging.SingleMessageRecipient
|
|||||||
import net.corda.core.node.services.ServiceInfo
|
import net.corda.core.node.services.ServiceInfo
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
import net.corda.flows.FetchAttachmentsFlow
|
import net.corda.flows.FetchAttachmentsFlow
|
||||||
|
import net.corda.node.internal.InitiatedFlowFactory
|
||||||
import net.corda.node.services.config.NodeConfiguration
|
import net.corda.node.services.config.NodeConfiguration
|
||||||
import net.corda.node.services.network.NetworkMapService
|
import net.corda.node.services.network.NetworkMapService
|
||||||
import net.corda.node.services.persistence.NodeAttachmentService
|
import net.corda.node.services.persistence.NodeAttachmentService
|
||||||
import net.corda.node.services.persistence.schemas.AttachmentEntity
|
import net.corda.node.services.persistence.schemas.AttachmentEntity
|
||||||
|
import net.corda.node.services.statemachine.SessionInit
|
||||||
import net.corda.node.utilities.transaction
|
import net.corda.node.utilities.transaction
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
@ -136,7 +138,11 @@ class AttachmentSerializationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun launchFlow(clientLogic: ClientLogic, rounds: Int) {
|
private fun launchFlow(clientLogic: ClientLogic, rounds: Int) {
|
||||||
server.services.registerServiceFlow(clientLogic.javaClass, ::ServerLogic)
|
server.registerFlowFactory(ClientLogic::class.java, object : InitiatedFlowFactory<ServerLogic> {
|
||||||
|
override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): ServerLogic {
|
||||||
|
return ServerLogic(otherParty)
|
||||||
|
}
|
||||||
|
}, ServerLogic::class.java, track = false)
|
||||||
client.services.startFlow(clientLogic)
|
client.services.startFlow(clientLogic)
|
||||||
network.runNetwork(rounds)
|
network.runNetwork(rounds)
|
||||||
}
|
}
|
||||||
|
@ -7,30 +7,26 @@ from the previous milestone release.
|
|||||||
UNRELEASED
|
UNRELEASED
|
||||||
----------
|
----------
|
||||||
|
|
||||||
* API changes:
|
* Quite a few changes have been made to the flow API which should make things simpler when writing CorDapps:
|
||||||
* ``Timestamp`` used for validation/notarization time-range has been renamed to ``TimeWindow``.
|
|
||||||
There are now 4 factory methods ``TimeWindow.fromOnly(fromTime: Instant)``,
|
|
||||||
``TimeWindow.untilOnly(untilTime: Instant)``, ``TimeWindow.between(fromTime: Instant, untilTime: Instant)`` and
|
|
||||||
``TimeWindow.withTolerance(time: Instant, tolerance: Duration)``.
|
|
||||||
Previous constructors ``TimeWindow(fromTime: Instant, untilTime: Instant)`` and
|
|
||||||
``TimeWindow(time: Instant, tolerance: Duration)`` have been removed.
|
|
||||||
|
|
||||||
* ``CordaPluginRegistry.requiredFlows`` is no longer needed. Instead annotate any flows you wish to start via RPC with
|
* ``CordaPluginRegistry.requiredFlows`` is no longer needed. Instead annotate any flows you wish to start via RPC with
|
||||||
``@StartableByRPC`` and any scheduled flows with ``@SchedulableFlow``.
|
``@StartableByRPC`` and any scheduled flows with ``@SchedulableFlow``.
|
||||||
|
|
||||||
* Flows which initiate flows in their counterparties (an example of which is the ``NotaryFlow.Client``) are now
|
* ``CordaPluginRegistry.servicePlugins`` is also no longer used, along with ``PluginServiceHub.registerFlowInitiator``.
|
||||||
required to be annotated with ``@InitiatingFlow``.
|
Instead annotate your initiated flows with ``@InitiatedBy``. This annotation takes a single parameter which is the
|
||||||
|
initiating flow. This initiating flow further has to be annotated with ``@InitiatingFlow``. For any services you
|
||||||
|
may have, such as oracles, annotate them with ``@CordaService``.
|
||||||
|
|
||||||
* ``PluginServiceHub.registerFlowInitiator`` has been deprecated and replaced by ``registerServiceFlow`` with the
|
* Related to ``InitiatingFlow``, the ``shareParentSessions`` boolean parameter of ``FlowLogic.subFlow`` has been
|
||||||
marker Class restricted to ``FlowLogic``. In line with the introduction of ``InitiatingFlow``, it throws an
|
removed. This was an unfortunate parameter that unnecessarily exposed the inner workings of flow sessions. Now, if
|
||||||
``IllegalArgumentException`` if the initiating flow class is not annotated with it.
|
your sub-flow can be started outside the context of the parent flow then annotate it with ``@InitiatingFlow``. If
|
||||||
|
it's meant to be used as a continuation of the existing parent flow, such as ``CollectSignaturesFlow``, then it
|
||||||
|
doesn't need any annotation.
|
||||||
|
|
||||||
* Also related to ``InitiatingFlow``, the ``shareParentSessions`` boolean parameter of ``FlowLogic.subFlow`` has been
|
* The ``InitiatingFlow`` annotation also has an integer ``version`` property which assigns the initiating flow a version
|
||||||
removed. Its purpose was to allow subflows to be inlined with the parent flow - i.e. the subflow does not initiate
|
number, defaulting to 1 if it's not specified. This enables versioning of flows with nodes only accepting communication
|
||||||
new sessions with parties the parent flow has already started. This allowed flows to be used as building blocks. To
|
if the version number matches. At some point we will support the ability for a node to have multiple versions of the
|
||||||
achieve the same effect now simply requires the subflow to be *not* annotated wth ``InitiatingFlow`` (i.e. we've made
|
same flow registered, enabling backwards compatibility of flows.
|
||||||
this the default behaviour). If the subflow is not meant to be inlined, and is supposed to initiate flows on the
|
|
||||||
other side, the annotation is required.
|
|
||||||
|
|
||||||
* ``ContractUpgradeFlow.Instigator`` has been renamed to just ``ContractUpgradeFlow``.
|
* ``ContractUpgradeFlow.Instigator`` has been renamed to just ``ContractUpgradeFlow``.
|
||||||
|
|
||||||
@ -50,10 +46,6 @@ UNRELEASED
|
|||||||
* Names of parties are now stored as a ``X500Name`` rather than a ``String``, to correctly enforce basic structure of the
|
* Names of parties are now stored as a ``X500Name`` rather than a ``String``, to correctly enforce basic structure of the
|
||||||
name. As a result all node legal names must now be structured as X.500 distinguished names.
|
name. As a result all node legal names must now be structured as X.500 distinguished names.
|
||||||
|
|
||||||
* The Bouncy Castle library ``X509CertificateHolder`` class is now used in place of ``X509Certificate`` in order to
|
|
||||||
have a consistent class used internally. Conversions to/from ``X509Certificate`` are done as required, but should
|
|
||||||
be avoided where possible.
|
|
||||||
|
|
||||||
* There are major changes to transaction signing in flows:
|
* There are major changes to transaction signing in flows:
|
||||||
|
|
||||||
* You should use the new ``CollectSignaturesFlow`` and corresponding ``SignTransactionFlow`` which handle most
|
* You should use the new ``CollectSignaturesFlow`` and corresponding ``SignTransactionFlow`` which handle most
|
||||||
@ -78,11 +70,16 @@ UNRELEASED
|
|||||||
* The original ``KeyPair`` signing methods have been left on the ``TransactionBuilder`` and ``SignedTransaction``, but
|
* The original ``KeyPair`` signing methods have been left on the ``TransactionBuilder`` and ``SignedTransaction``, but
|
||||||
should only be used as part of unit testing.
|
should only be used as part of unit testing.
|
||||||
|
|
||||||
* The ``InitiatingFlow`` annotation also has an integer ``version`` property which assigns the initiating flow a version
|
* ``Timestamp`` used for validation/notarization time-range has been renamed to ``TimeWindow``.
|
||||||
number, defaulting to 1 if it's specified. The flow version is included in the flow session request and the counterparty
|
There are now 4 factory methods ``TimeWindow.fromOnly(fromTime: Instant)``,
|
||||||
will only respond and start their own flow if the version number matches to the one they've registered with. At some
|
``TimeWindow.untilOnly(untilTime: Instant)``, ``TimeWindow.between(fromTime: Instant, untilTime: Instant)`` and
|
||||||
point we will support the ability for a node to have multiple versions of the same flow registered, enabling backwards
|
``TimeWindow.withTolerance(time: Instant, tolerance: Duration)``.
|
||||||
compatibility of CorDapp flows.
|
Previous constructors ``TimeWindow(fromTime: Instant, untilTime: Instant)`` and
|
||||||
|
``TimeWindow(time: Instant, tolerance: Duration)`` have been removed.
|
||||||
|
|
||||||
|
* The Bouncy Castle library ``X509CertificateHolder`` class is now used in place of ``X509Certificate`` in order to
|
||||||
|
have a consistent class used internally. Conversions to/from ``X509Certificate`` are done as required, but should
|
||||||
|
be avoided where possible.
|
||||||
|
|
||||||
* The certificate hierarchy has been changed in order to allow corda node to sign keys with proper certificate chain.
|
* The certificate hierarchy has been changed in order to allow corda node to sign keys with proper certificate chain.
|
||||||
* The corda node will now be issued a restricted client CA for identity/transaction key signing.
|
* The corda node will now be issued a restricted client CA for identity/transaction key signing.
|
||||||
|
@ -45,40 +45,7 @@ extensions to be created, or registered at startup. In particular:
|
|||||||
jars. These static serving directories will not be available if the
|
jars. These static serving directories will not be available if the
|
||||||
bundled web server is not started.
|
bundled web server is not started.
|
||||||
|
|
||||||
c. The ``servicePlugins`` property returns a list of classes which will
|
c. The ``customizeSerialization`` function allows classes to be whitelisted
|
||||||
be instantiated once during the ``AbstractNode.start`` call. These
|
|
||||||
classes must provide a single argument constructor which will receive a
|
|
||||||
``PluginServiceHub`` reference. They must also extend the abstract class
|
|
||||||
``SingletonSerializeAsToken`` which ensures that if any reference to your
|
|
||||||
service is captured in a flow checkpoint (i.e. serialized by Kryo as
|
|
||||||
part of Quasar checkpoints, either on the stack or by reference within
|
|
||||||
your flows) it is stored as a simple token representing your service.
|
|
||||||
When checkpoints are restored, after a node restart for example,
|
|
||||||
the latest instance of the service will be substituted back in place of
|
|
||||||
the token stored in the checkpoint.
|
|
||||||
|
|
||||||
i. Firstly, they can call ``PluginServiceHub.registerServiceFlow`` and
|
|
||||||
register flows that will be initiated locally in response to remote flow
|
|
||||||
requests.
|
|
||||||
|
|
||||||
ii. Second, the service can hold a long lived reference to the
|
|
||||||
PluginServiceHub and to other private data, so the service can be used
|
|
||||||
to provide Oracle functionality. This Oracle functionality would
|
|
||||||
typically be exposed to other nodes by flows which are given a reference
|
|
||||||
to the service plugin when initiated (as defined by the
|
|
||||||
``registerServiceFlow`` call). The flow can then call into functions
|
|
||||||
on the plugin service singleton. Note, care should be taken to not allow
|
|
||||||
flows to hold references to fields which are not
|
|
||||||
also ``SingletonSerializeAsToken``, otherwise Quasar suspension in the
|
|
||||||
``StateMachineManager`` will fail with exceptions. An example oracle can
|
|
||||||
be seen in ``NodeInterestRates.kt`` in the irs-demo sample.
|
|
||||||
|
|
||||||
iii. The final use case for service plugins is that they can spawn threads, or register
|
|
||||||
to monitor vault updates. This allows them to provide long lived active
|
|
||||||
functions inside the node, for instance to initiate workflows when
|
|
||||||
certain conditions are met.
|
|
||||||
|
|
||||||
d. The ``customizeSerialization`` function allows classes to be whitelisted
|
|
||||||
for object serialisation, over and above those tagged with the ``@CordaSerializable``
|
for object serialisation, over and above those tagged with the ``@CordaSerializable``
|
||||||
annotation. In general the annotation should be preferred. For
|
annotation. In general the annotation should be preferred. For
|
||||||
instance new state types will need to be explicitly registered. This will be called at
|
instance new state types will need to be explicitly registered. This will be called at
|
||||||
|
@ -12,20 +12,33 @@ App plugins
|
|||||||
To create an app plugin you must extend from `CordaPluginRegistry`_. The JavaDoc contains
|
To create an app plugin you must extend from `CordaPluginRegistry`_. The JavaDoc contains
|
||||||
specific details of the implementation, but you can extend the server in the following ways:
|
specific details of the implementation, but you can extend the server in the following ways:
|
||||||
|
|
||||||
1. Service plugins: Register your services (see below).
|
1. Register your flows and services (see below).
|
||||||
2. Web APIs: You may register your own endpoints under /api/ of the bundled web server.
|
2. Web APIs: You may register your own endpoints under /api/ of the bundled web server.
|
||||||
3. Static web endpoints: You may register your own static serving directories for serving web content from the web server.
|
3. Static web endpoints: You may register your own static serving directories for serving web content from the web server.
|
||||||
4. Whitelisting your additional contract, state and other classes for object serialization. Any class that forms part
|
4. Whitelisting your additional contract, state and other classes for object serialization. Any class that forms part
|
||||||
of a persisted state, that is used in messaging between flows or in RPC needs to be whitelisted.
|
of a persisted state, that is used in messaging between flows or in RPC needs to be whitelisted.
|
||||||
|
|
||||||
Services
|
Flows and services
|
||||||
--------
|
------------------
|
||||||
|
|
||||||
Services are classes which are constructed after the node has started. It is provided a `PluginServiceHub`_ which
|
Flows are of two types: initiating and initiated. Initiating flows need to be annotated with ``@InitiatingFlow`` and can
|
||||||
allows a richer API than the `ServiceHub`_ exposed to contracts. It enables adding flows, registering
|
be started in one of three ways:
|
||||||
message handlers and more. The service does not run in a separate thread, so the only entry point to the service is during
|
|
||||||
construction, where message handlers should be registered and threads started.
|
|
||||||
|
|
||||||
|
1. By a user of your CorDapp via RPC in which the flow also needs to be annotated with ``@StartableByRPC``.
|
||||||
|
2. By another CorDapp executing it as a sub-flow in their own flow.
|
||||||
|
3. By a ``SchedulableState`` activity event, in which the flow also needs to be annotated with ``@SchedulableFlow``
|
||||||
|
|
||||||
|
``InitiatingFlow`` also has a ``version`` property to enable you to version your flows. A node will only accept communication
|
||||||
|
from an initiating party if the version numbers match up.
|
||||||
|
|
||||||
|
Initiated flows are typically private to your CorDapp and need to be annotated with ``@InitiatedBy`` which point to
|
||||||
|
initiating flow Class. The node scans your CorDapps for these annotations and automatically registers the initiating to
|
||||||
|
initiated mapping for you.
|
||||||
|
|
||||||
|
If your CorDapp also needs to have additional services running in the node, such as oracles, then annotate your service
|
||||||
|
class with ``@CordaService``. As with the flows, the node will automatically register it and make it available for use by
|
||||||
|
your flows. The service class has to implement ``SerializeAsToken`` to ensure they work correctly within flows. If possible
|
||||||
|
extend ``SingletonSerializeAsToken`` instead to avoid the boilerplate.
|
||||||
|
|
||||||
Starting nodes
|
Starting nodes
|
||||||
--------------
|
--------------
|
||||||
|
@ -9,9 +9,9 @@ import net.corda.core.contracts.TransactionType
|
|||||||
import net.corda.core.crypto.DigitalSignature
|
import net.corda.core.crypto.DigitalSignature
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.InitiatedBy
|
||||||
import net.corda.core.flows.InitiatingFlow
|
import net.corda.core.flows.InitiatingFlow
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.PluginServiceHub
|
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.services.unconsumedStates
|
import net.corda.core.node.services.unconsumedStates
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
@ -21,13 +21,6 @@ import net.corda.flows.FinalityFlow
|
|||||||
import net.corda.flows.ResolveTransactionsFlow
|
import net.corda.flows.ResolveTransactionsFlow
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object FxTransactionDemoTutorial {
|
|
||||||
// Would normally be called by custom service init in a CorDapp
|
|
||||||
fun registerFxProtocols(pluginHub: PluginServiceHub) {
|
|
||||||
pluginHub.registerServiceFlow(ForeignExchangeFlow::class.java, ::ForeignExchangeRemoteFlow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
private data class FxRequest(val tradeId: String,
|
private data class FxRequest(val tradeId: String,
|
||||||
val amount: Amount<Issued<Currency>>,
|
val amount: Amount<Issued<Currency>>,
|
||||||
@ -212,6 +205,7 @@ class ForeignExchangeFlow(val tradeId: String,
|
|||||||
// DOCEND 3
|
// DOCEND 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(ForeignExchangeFlow::class)
|
||||||
class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() {
|
class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
|
@ -6,10 +6,10 @@ import net.corda.core.crypto.DigitalSignature
|
|||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.containsAny
|
import net.corda.core.crypto.containsAny
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.InitiatedBy
|
||||||
import net.corda.core.flows.InitiatingFlow
|
import net.corda.core.flows.InitiatingFlow
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.PluginServiceHub
|
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.services.linearHeadsOfType
|
import net.corda.core.node.services.linearHeadsOfType
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
@ -19,22 +19,13 @@ import net.corda.flows.FinalityFlow
|
|||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
object WorkflowTransactionBuildTutorial {
|
|
||||||
// Would normally be called by custom service init in a CorDapp
|
|
||||||
fun registerWorkflowProtocols(pluginHub: PluginServiceHub) {
|
|
||||||
pluginHub.registerServiceFlow(SubmitCompletionFlow::class.java, ::RecordCompletionFlow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DOCSTART 1
|
// DOCSTART 1
|
||||||
|
|
||||||
// Helper method to locate the latest Vault version of a LinearState from a possibly out of date StateRef
|
// Helper method to locate the latest Vault version of a LinearState from a possibly out of date StateRef
|
||||||
inline fun <reified T : LinearState> ServiceHub.latest(ref: StateRef): StateAndRef<T> {
|
inline fun <reified T : LinearState> ServiceHub.latest(ref: StateRef): StateAndRef<T> {
|
||||||
val linearHeads = vaultService.linearHeadsOfType<T>()
|
val linearHeads = vaultService.linearHeadsOfType<T>()
|
||||||
val original = toStateAndRef<T>(ref)
|
val original = toStateAndRef<T>(ref)
|
||||||
return linearHeads.get(original.state.data.linearId)!!
|
return linearHeads[original.state.data.linearId]!!
|
||||||
}
|
}
|
||||||
|
|
||||||
// DOCEND 1
|
// DOCEND 1
|
||||||
|
|
||||||
// Minimal state model of a manual approval process
|
// Minimal state model of a manual approval process
|
||||||
@ -87,7 +78,7 @@ data class TradeApprovalContract(override val legalContractReference: SecureHash
|
|||||||
"Issue of new WorkflowContract must not include any inputs" using (tx.inputs.isEmpty())
|
"Issue of new WorkflowContract must not include any inputs" using (tx.inputs.isEmpty())
|
||||||
"Issue of new WorkflowContract must be in a unique transaction" using (tx.outputs.size == 1)
|
"Issue of new WorkflowContract must be in a unique transaction" using (tx.outputs.size == 1)
|
||||||
}
|
}
|
||||||
val issued = tx.outputs.get(0) as TradeApprovalContract.State
|
val issued = tx.outputs[0] as TradeApprovalContract.State
|
||||||
requireThat {
|
requireThat {
|
||||||
"Issue requires the source Party as signer" using (command.signers.contains(issued.source.owningKey))
|
"Issue requires the source Party as signer" using (command.signers.contains(issued.source.owningKey))
|
||||||
"Initial Issue state must be NEW" using (issued.state == WorkflowState.NEW)
|
"Initial Issue state must be NEW" using (issued.state == WorkflowState.NEW)
|
||||||
@ -96,9 +87,9 @@ data class TradeApprovalContract(override val legalContractReference: SecureHash
|
|||||||
is Commands.Completed -> {
|
is Commands.Completed -> {
|
||||||
val stateGroups = tx.groupStates(TradeApprovalContract.State::class.java) { it.linearId }
|
val stateGroups = tx.groupStates(TradeApprovalContract.State::class.java) { it.linearId }
|
||||||
require(stateGroups.size == 1) { "Must be only a single proposal in transaction" }
|
require(stateGroups.size == 1) { "Must be only a single proposal in transaction" }
|
||||||
for (group in stateGroups) {
|
for ((inputs, outputs) in stateGroups) {
|
||||||
val before = group.inputs.single()
|
val before = inputs.single()
|
||||||
val after = group.outputs.single()
|
val after = outputs.single()
|
||||||
requireThat {
|
requireThat {
|
||||||
"Only a non-final trade can be modified" using (before.state == WorkflowState.NEW)
|
"Only a non-final trade can be modified" using (before.state == WorkflowState.NEW)
|
||||||
"Output must be a final state" using (after.state in setOf(WorkflowState.APPROVED, WorkflowState.REJECTED))
|
"Output must be a final state" using (after.state in setOf(WorkflowState.APPROVED, WorkflowState.REJECTED))
|
||||||
@ -227,6 +218,7 @@ class SubmitCompletionFlow(val ref: StateRef, val verdict: WorkflowState) : Flow
|
|||||||
* Then after checking to sign it and eventually store the fully notarised
|
* Then after checking to sign it and eventually store the fully notarised
|
||||||
* transaction to the ledger.
|
* transaction to the ledger.
|
||||||
*/
|
*/
|
||||||
|
@InitiatedBy(SubmitCompletionFlow::class)
|
||||||
class RecordCompletionFlow(val source: Party) : FlowLogic<Unit>() {
|
class RecordCompletionFlow(val source: Party) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): Unit {
|
override fun call(): Unit {
|
||||||
|
@ -29,14 +29,11 @@ class FxTransactionBuildTutorialTest {
|
|||||||
val notaryService = ServiceInfo(ValidatingNotaryService.type)
|
val notaryService = ServiceInfo(ValidatingNotaryService.type)
|
||||||
notaryNode = net.createNode(
|
notaryNode = net.createNode(
|
||||||
legalName = DUMMY_NOTARY.name,
|
legalName = DUMMY_NOTARY.name,
|
||||||
overrideServices = mapOf(Pair(notaryService, DUMMY_NOTARY_KEY)),
|
overrideServices = mapOf(notaryService to DUMMY_NOTARY_KEY),
|
||||||
advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService))
|
advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService))
|
||||||
nodeA = net.createPartyNode(notaryNode.info.address)
|
nodeA = net.createPartyNode(notaryNode.info.address)
|
||||||
nodeB = net.createPartyNode(notaryNode.info.address)
|
nodeB = net.createPartyNode(notaryNode.info.address)
|
||||||
FxTransactionDemoTutorial.registerFxProtocols(nodeA.services)
|
nodeB.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java)
|
||||||
FxTransactionDemoTutorial.registerFxProtocols(nodeB.services)
|
|
||||||
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeA.services)
|
|
||||||
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeB.services)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
@ -4,7 +4,6 @@ import net.corda.core.contracts.LinearState
|
|||||||
import net.corda.core.contracts.StateAndRef
|
import net.corda.core.contracts.StateAndRef
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.getOrThrow
|
import net.corda.core.getOrThrow
|
||||||
import net.corda.core.node.ServiceEntry
|
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.services.ServiceInfo
|
import net.corda.core.node.services.ServiceInfo
|
||||||
import net.corda.core.node.services.linearHeadsOfType
|
import net.corda.core.node.services.linearHeadsOfType
|
||||||
@ -30,7 +29,7 @@ class WorkflowTransactionBuildTutorialTest {
|
|||||||
private inline fun <reified T : LinearState> ServiceHub.latest(ref: StateRef): StateAndRef<T> {
|
private inline fun <reified T : LinearState> ServiceHub.latest(ref: StateRef): StateAndRef<T> {
|
||||||
val linearHeads = vaultService.linearHeadsOfType<T>()
|
val linearHeads = vaultService.linearHeadsOfType<T>()
|
||||||
val original = storageService.validatedTransactions.getTransaction(ref.txhash)!!.tx.outRef<T>(ref.index)
|
val original = storageService.validatedTransactions.getTransaction(ref.txhash)!!.tx.outRef<T>(ref.index)
|
||||||
return linearHeads.get(original.state.data.linearId)!!
|
return linearHeads[original.state.data.linearId]!!
|
||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@ -43,10 +42,7 @@ class WorkflowTransactionBuildTutorialTest {
|
|||||||
advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService))
|
advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService))
|
||||||
nodeA = net.createPartyNode(notaryNode.info.address)
|
nodeA = net.createPartyNode(notaryNode.info.address)
|
||||||
nodeB = net.createPartyNode(notaryNode.info.address)
|
nodeB = net.createPartyNode(notaryNode.info.address)
|
||||||
FxTransactionDemoTutorial.registerFxProtocols(nodeA.services)
|
nodeA.registerInitiatedFlow(RecordCompletionFlow::class.java)
|
||||||
FxTransactionDemoTutorial.registerFxProtocols(nodeB.services)
|
|
||||||
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeA.services)
|
|
||||||
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeB.services)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
@ -419,48 +419,56 @@ sequence of message transfers. Flows end pre-maturely due to exceptions, and as
|
|||||||
|
|
||||||
Taking a step back, we mentioned that the other side has to accept the session request for there to be a communication
|
Taking a step back, we mentioned that the other side has to accept the session request for there to be a communication
|
||||||
channel. A node accepts a session request if it has registered the flow type (the fully-qualified class name) that is
|
channel. A node accepts a session request if it has registered the flow type (the fully-qualified class name) that is
|
||||||
making the request - each session initiation includes the initiating flow type. The registration is done by a CorDapp
|
making the request - each session initiation includes the initiating flow type. This registration is done automatically
|
||||||
which has made available the particular flow communication, using ``PluginServiceHub.registerServiceFlow``. This method
|
by the node at startup by searching for flows which are annotated with ``@InitiatedBy``. This annotation points to the
|
||||||
specifies a flow factory for generating the counter-flow to any given initiating flow. If this registration doesn't exist
|
flow that is doing the initiating, and this flow must be annotated with ``@InitiatingFlow``. The ``InitiatedBy`` flow
|
||||||
then no further communication takes place and the initiating flow ends with an exception. The initiating flow has to be
|
must have a constructor which takes in a single parameter of type ``Party`` - this is the initiating party.
|
||||||
annotated with ``InitiatingFlow``.
|
|
||||||
|
|
||||||
Going back to our buyer and seller flows, we need a way to initiate communication between the two. This is typically done
|
Going back to our buyer and seller flows, we need a way to initiate communication between the two. This is typically done
|
||||||
with one side started manually using the ``startFlowDynamic`` RPC and this initiates the counter-flow on the other side.
|
with one side started manually using the ``startFlowDynamic`` RPC and this initiates the flow on the other side. In our
|
||||||
In this case it doesn't matter which flow is the initiator and which is the initiated, which is why neither ``Buyer`` nor
|
case it doesn't matter which flow is the initiator and which is the initiated, which is why neither ``Buyer`` nor ``Seller``
|
||||||
``Seller`` are annotated with ``InitiatingFlow``. For example, if we choose the seller side as the initiator then we need
|
are annotated with ``InitiatedBy`` or ``InitiatingFlow``. If we, for example, choose the seller side as the initiator then
|
||||||
to create a simple seller starter flow that has the annotation we need:
|
we need to create a simple seller starter flow that has the annotation we need:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
class SellerStarter(val otherParty: Party, val assetToSell: StateAndRef<OwnableState>, val price: Amount<Currency>) : FlowLogic<SignedTransaction>() {
|
class SellerInitiator(val buyer: Party,
|
||||||
|
val notary: NodeInfo,
|
||||||
|
val assetToSell: StateAndRef<OwnableState>,
|
||||||
|
val price: Amount<Currency>) : FlowLogic<SignedTransaction>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
|
send(buyer, Pair(notary.notaryIdentity, price))
|
||||||
val cpOwnerKey: PublicKey = serviceHub.legalIdentityKey
|
return subFlow(Seller(
|
||||||
return subFlow(TwoPartyTradeFlow.Seller(otherParty, notary, assetToSell, price, cpOwnerKey))
|
buyer,
|
||||||
|
notary,
|
||||||
|
assetToSell,
|
||||||
|
price,
|
||||||
|
serviceHub.legalIdentityKey))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
The buyer side would then need to register their flow, perhaps with something like:
|
The buyer side would look something like this. Notice the constructor takes in a single ``Party`` object which represents
|
||||||
|
the seller.
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
val services: PluginServiceHub = TODO()
|
@InitiatedBy(SellerInitiator::class)
|
||||||
services.registerServiceFlow(SellerStarter::class.java) { otherParty ->
|
class BuyerAcceptor(val seller: Party) : FlowLogic<Unit>() {
|
||||||
val notary = services.networkMapCache.notaryNodes[0]
|
@Suspendable
|
||||||
val acceptablePrice = TODO()
|
override fun call() {
|
||||||
val typeToBuy = TODO()
|
val (notary, price) = receive<Pair<Party, Amount<Currency>>>(seller).unwrap {
|
||||||
Buyer(otherParty, notary, acceptablePrice, typeToBuy)
|
require(serviceHub.networkMapCache.isNotary(it.first)) { "${it.first} is not a notary" }
|
||||||
|
it
|
||||||
|
}
|
||||||
|
subFlow(Buyer(seller, notary, price, CommercialPaper.State::class.java))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
This is telling the buyer node to fire up an instance of ``Buyer`` (the code in the lambda) when the initiating flow
|
|
||||||
is a seller (``SellerStarter::class.java``).
|
|
||||||
|
|
||||||
.. _progress-tracking:
|
.. _progress-tracking:
|
||||||
|
|
||||||
|
@ -195,41 +195,37 @@ Here we can see that there are several steps:
|
|||||||
exactly our data source. The final step, assuming we have got this far, is to generate a signature for the
|
exactly our data source. The final step, assuming we have got this far, is to generate a signature for the
|
||||||
transaction and return it.
|
transaction and return it.
|
||||||
|
|
||||||
Binding to the network via a CorDapp plugin
|
Binding to the network
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. note:: Before reading any further, we advise that you understand the concept of flows and how to write them and use
|
.. note:: Before reading any further, we advise that you understand the concept of flows and how to write them and use
|
||||||
them. See :doc:`flow-state-machines`. Likewise some understanding of Cordapps, plugins and services will be helpful.
|
them. See :doc:`flow-state-machines`. Likewise some understanding of Cordapps, plugins and services will be helpful.
|
||||||
See :doc:`creating-a-cordapp`.
|
See :doc:`creating-a-cordapp`.
|
||||||
|
|
||||||
The first step is to create a service to host the oracle on the network. Let's see how that's implemented:
|
The first step is to create the oracle as a service by annotating its class with ``@CordaService``. Let's see how that's
|
||||||
|
done:
|
||||||
|
|
||||||
|
.. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 3
|
||||||
|
:end-before: DOCEND 3
|
||||||
|
|
||||||
|
The Corda node scans for any class with this annotation and initialises them. The only requirement is that the class provide
|
||||||
|
a constructor with a single parameter of type ``PluginServiceHub```. In our example the oracle class has two constructors.
|
||||||
|
The second is used for testing.
|
||||||
|
|
||||||
.. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt
|
.. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: DOCSTART 2
|
:start-after: DOCSTART 2
|
||||||
:end-before: DOCEND 2
|
:end-before: DOCEND 2
|
||||||
|
|
||||||
This may look complicated, but really it's made up of some relatively simple elements (in the order they appear in the code):
|
These two flows leverage the oracle to provide the querying and signing operations. They get reference to the oracle,
|
||||||
|
which will have already been initialised by the node, using ``ServiceHub.cordappService``. Both flows are annotated with
|
||||||
|
``@InitiatedBy``. This tells the node which initiating flow (which are discussed in the next section) they are meant to
|
||||||
|
be executed with.
|
||||||
|
|
||||||
1. Accept a ``PluginServiceHub`` in the constructor. This is your interface to the Corda node.
|
Providing sub-flows for querying and signing
|
||||||
2. Ensure you extend the abstract class ``SingletonSerializeAsToken`` (see :doc:`corda-plugins`).
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
3. Create an instance of your core oracle class that has the ``query`` and ``sign`` methods as discussed above.
|
|
||||||
4. Register your client sub-flows (in this case both in ``RatesFixFlow``. See the next section) for querying and
|
|
||||||
signing as initiating your service flows that actually do the querying and signing using your core oracle class instance.
|
|
||||||
5. Implement your service flows that call your core oracle class instance.
|
|
||||||
|
|
||||||
The final step is to register your service with the node via the plugin mechanism. Do this by
|
|
||||||
implementing a plugin. Don't forget the resources file to register it with the ``ServiceLoader`` framework
|
|
||||||
(see :doc:`corda-plugins`).
|
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
|
||||||
|
|
||||||
class Plugin : CordaPluginRegistry() {
|
|
||||||
override val servicePlugins: List<Class<*>> = listOf(Service::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
Providing client sub-flows for querying and signing
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
We mentioned the client sub-flow briefly above. They are the mechanism that clients, in the form of other flows, will
|
We mentioned the client sub-flow briefly above. They are the mechanism that clients, in the form of other flows, will
|
||||||
interact with your oracle. Typically there will be one for querying and one for signing. Let's take a look at
|
interact with your oracle. Typically there will be one for querying and one for signing. Let's take a look at
|
||||||
|
@ -6,13 +6,15 @@ Here are release notes for each snapshot release from M9 onwards.
|
|||||||
Unreleased
|
Unreleased
|
||||||
----------
|
----------
|
||||||
|
|
||||||
We've added the ability for flows to be versioned by their CorDapp developers. This enables a node to support a particular
|
Writing CorDapps has been made simpler by removing boiler-plate code that was previously required when registering flows.
|
||||||
version of a flow and allows it to reject flow communication with a node which isn't using the same fact. In a future
|
Instead we now make use of classpath scanning to automatically wire-up flows.
|
||||||
release we allow a node to have multiple versions of the same flow running to enable backwards compatibility.
|
|
||||||
|
|
||||||
There are major changes to the ``Party`` class as part of confidential identities, and how parties and keys are stored
|
There are major changes to the ``Party`` class as part of confidential identities, and how parties and keys are stored
|
||||||
in transaction state objects. See :doc:`changelog` for full details.
|
in transaction state objects. See :doc:`changelog` for full details.
|
||||||
|
|
||||||
|
We've added the ability for flows to be versioned by their CorDapp developers. This enables a node to support a particular
|
||||||
|
version of a flow and allows it to reject flow communication with a node which isn't using the same fact. In a future
|
||||||
|
release we allow a node to have multiple versions of the same flow running to enable backwards compatibility.
|
||||||
|
|
||||||
Milestone 11
|
Milestone 11
|
||||||
------------
|
------------
|
||||||
|
@ -2,12 +2,8 @@ package net.corda.flows
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.flows.FlowException
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.InitiatingFlow
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.flows.StartableByRPC
|
|
||||||
import net.corda.core.node.PluginServiceHub
|
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.OpaqueBytes
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
@ -46,6 +42,7 @@ object IssuerFlow {
|
|||||||
* Issuer refers to a Node acting as a Bank Issuer of [FungibleAsset], and processes requests from a [IssuanceRequester] client.
|
* Issuer refers to a Node acting as a Bank Issuer of [FungibleAsset], and processes requests from a [IssuanceRequester] client.
|
||||||
* Returns the generated transaction representing the transfer of the [Issued] [FungibleAsset] to the issue requester.
|
* Returns the generated transaction representing the transfer of the [Issued] [FungibleAsset] to the issue requester.
|
||||||
*/
|
*/
|
||||||
|
@InitiatedBy(IssuanceRequester::class)
|
||||||
class Issuer(val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
class Issuer(val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
||||||
companion object {
|
companion object {
|
||||||
object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request")
|
object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request")
|
||||||
@ -97,11 +94,5 @@ object IssuerFlow {
|
|||||||
// NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger)
|
// NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger)
|
||||||
return moveTx
|
return moveTx
|
||||||
}
|
}
|
||||||
|
|
||||||
class Service(services: PluginServiceHub) {
|
|
||||||
init {
|
|
||||||
services.registerServiceFlow(IssuanceRequester::class.java, ::Issuer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,16 +11,17 @@ import net.corda.core.getOrThrow
|
|||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.map
|
import net.corda.core.map
|
||||||
import net.corda.core.serialization.OpaqueBytes
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
|
import net.corda.core.toFuture
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.DUMMY_NOTARY
|
import net.corda.core.utilities.DUMMY_NOTARY
|
||||||
import net.corda.flows.IssuerFlow.IssuanceRequester
|
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||||
import net.corda.testing.BOC
|
import net.corda.testing.BOC
|
||||||
import net.corda.testing.MEGA_CORP
|
import net.corda.testing.MEGA_CORP
|
||||||
import net.corda.testing.initiateSingleShotFlow
|
|
||||||
import net.corda.testing.ledger
|
import net.corda.testing.ledger
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
import net.corda.testing.node.MockNetwork.MockNode
|
import net.corda.testing.node.MockNetwork.MockNode
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import rx.Observable
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
@ -73,7 +74,6 @@ class IssuerFlowTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test concurrent issuer flow`() {
|
fun `test concurrent issuer flow`() {
|
||||||
|
|
||||||
net = MockNetwork(false, true)
|
net = MockNetwork(false, true)
|
||||||
ledger {
|
ledger {
|
||||||
notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name)
|
notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name)
|
||||||
@ -96,18 +96,19 @@ class IssuerFlowTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runIssuerAndIssueRequester(issuerNode: MockNode, issueToNode: MockNode,
|
private fun runIssuerAndIssueRequester(issuerNode: MockNode,
|
||||||
|
issueToNode: MockNode,
|
||||||
amount: Amount<Currency>,
|
amount: Amount<Currency>,
|
||||||
party: Party, ref: OpaqueBytes): RunResult {
|
party: Party,
|
||||||
|
ref: OpaqueBytes): RunResult {
|
||||||
val issueToPartyAndRef = party.ref(ref)
|
val issueToPartyAndRef = party.ref(ref)
|
||||||
val issuerFuture = issuerNode.initiateSingleShotFlow(IssuerFlow.IssuanceRequester::class) { _ ->
|
val issuerFlows: Observable<IssuerFlow.Issuer> = issuerNode.registerInitiatedFlow(IssuerFlow.Issuer::class.java)
|
||||||
IssuerFlow.Issuer(party)
|
val firstIssuerFiber = issuerFlows.toFuture().map { it.stateMachine }
|
||||||
}.map { it.stateMachine }
|
|
||||||
|
|
||||||
val issueRequest = IssuanceRequester(amount, party, issueToPartyAndRef.reference, issuerNode.info.legalIdentity)
|
val issueRequest = IssuanceRequester(amount, party, issueToPartyAndRef.reference, issuerNode.info.legalIdentity)
|
||||||
val issueRequestResultFuture = issueToNode.services.startFlow(issueRequest).resultFuture
|
val issueRequestResultFuture = issueToNode.services.startFlow(issueRequest).resultFuture
|
||||||
|
|
||||||
return IssuerFlowTest.RunResult(issuerFuture, issueRequestResultFuture)
|
return IssuerFlowTest.RunResult(firstIssuerFiber, issueRequestResultFuture)
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class RunResult(
|
private data class RunResult(
|
||||||
|
@ -46,6 +46,14 @@ processResources {
|
|||||||
from file("$rootDir/config/dev/log4j2.xml")
|
from file("$rootDir/config/dev/log4j2.xml")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
processIntegrationTestResources {
|
||||||
|
// Build one of the demos so that we can test CorDapp scanning in CordappScanningTest. It doesn't matter which demo
|
||||||
|
// we use, just make sure the test is updated accordingly.
|
||||||
|
from(project(':samples:trader-demo').tasks.jar) {
|
||||||
|
rename 'trader-demo-(.*)', 'trader-demo.jar'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
||||||
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
|
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
|
||||||
|
|
||||||
@ -153,7 +161,7 @@ dependencies {
|
|||||||
compile "io.requery:requery-kotlin:$requery_version"
|
compile "io.requery:requery-kotlin:$requery_version"
|
||||||
|
|
||||||
// FastClasspathScanner: classpath scanning
|
// FastClasspathScanner: classpath scanning
|
||||||
compile 'io.github.lukehutch:fast-classpath-scanner:2.0.20'
|
compile 'io.github.lukehutch:fast-classpath-scanner:2.0.21'
|
||||||
|
|
||||||
// Integration test helpers
|
// Integration test helpers
|
||||||
integrationTestCompile "junit:junit:$junit_version"
|
integrationTestCompile "junit:junit:$junit_version"
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
package net.corda.node
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import com.google.common.util.concurrent.Futures
|
||||||
|
import net.corda.core.copyToDirectory
|
||||||
|
import net.corda.core.createDirectories
|
||||||
|
import net.corda.core.div
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.InitiatedBy
|
||||||
|
import net.corda.core.flows.InitiatingFlow
|
||||||
|
import net.corda.core.flows.StartableByRPC
|
||||||
|
import net.corda.core.getOrThrow
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.utilities.ALICE
|
||||||
|
import net.corda.core.utilities.BOB
|
||||||
|
import net.corda.core.utilities.unwrap
|
||||||
|
import net.corda.node.driver.driver
|
||||||
|
import net.corda.node.services.startFlowPermission
|
||||||
|
import net.corda.nodeapi.User
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.Test
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
class CordappScanningTest {
|
||||||
|
@Test
|
||||||
|
fun `CorDapp jar in plugins directory is scanned`() {
|
||||||
|
// If the CorDapp jar does't exist then run the integrationTestClasses gradle task
|
||||||
|
val cordappJar = Paths.get(javaClass.getResource("/trader-demo.jar").toURI())
|
||||||
|
driver {
|
||||||
|
val pluginsDir = (baseDirectory(ALICE.name) / "plugins").createDirectories()
|
||||||
|
cordappJar.copyToDirectory(pluginsDir)
|
||||||
|
|
||||||
|
val user = User("u", "p", emptySet())
|
||||||
|
val alice = startNode(ALICE.name, rpcUsers = listOf(user)).getOrThrow()
|
||||||
|
val rpc = alice.rpcClientToNode().start(user.username, user.password)
|
||||||
|
// If the CorDapp wasn't scanned then SellerFlow won't have been picked up as an RPC flow
|
||||||
|
assertThat(rpc.proxy.registeredFlows()).contains("net.corda.traderdemo.flow.SellerFlow")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty plugins directory`() {
|
||||||
|
driver {
|
||||||
|
val baseDirectory = baseDirectory(ALICE.name)
|
||||||
|
(baseDirectory / "plugins").createDirectories()
|
||||||
|
startNode(ALICE.name).getOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `sub-classed initiated flow pointing to the same initiating flow as its super-class`() {
|
||||||
|
val user = User("u", "p", setOf(startFlowPermission<ReceiveFlow>()))
|
||||||
|
driver(systemProperties = mapOf("net.corda.node.cordapp.scan.package" to "net.corda.node")) {
|
||||||
|
val (alice, bob) = Futures.allAsList(
|
||||||
|
startNode(ALICE.name, rpcUsers = listOf(user)),
|
||||||
|
startNode(BOB.name)).getOrThrow()
|
||||||
|
val initiatedFlowClass = alice.rpcClientToNode()
|
||||||
|
.start(user.username, user.password)
|
||||||
|
.proxy
|
||||||
|
.startFlow(::ReceiveFlow, bob.nodeInfo.legalIdentity)
|
||||||
|
.returnValue
|
||||||
|
assertThat(initiatedFlowClass.getOrThrow()).isEqualTo(SendSubClassFlow::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@StartableByRPC
|
||||||
|
@InitiatingFlow
|
||||||
|
class ReceiveFlow(val otherParty: Party) : FlowLogic<String>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): String = receive<String>(otherParty).unwrap { it }
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(ReceiveFlow::class)
|
||||||
|
open class SendClassFlow(val otherParty: Party) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() = send(otherParty, javaClass.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(ReceiveFlow::class)
|
||||||
|
class SendSubClassFlow(otherParty: Party) : SendClassFlow(otherParty)
|
||||||
|
}
|
@ -6,6 +6,7 @@ import net.corda.client.rpc.CordaRPCClient
|
|||||||
import net.corda.core.crypto.generateKeyPair
|
import net.corda.core.crypto.generateKeyPair
|
||||||
import net.corda.core.crypto.toBase58String
|
import net.corda.core.crypto.toBase58String
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.InitiatedBy
|
||||||
import net.corda.core.flows.InitiatingFlow
|
import net.corda.core.flows.InitiatingFlow
|
||||||
import net.corda.core.getOrThrow
|
import net.corda.core.getOrThrow
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
@ -216,7 +217,7 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
|||||||
|
|
||||||
private fun startBobAndCommunicateWithAlice(): Party {
|
private fun startBobAndCommunicateWithAlice(): Party {
|
||||||
val bob = startNode(BOB.name).getOrThrow()
|
val bob = startNode(BOB.name).getOrThrow()
|
||||||
bob.services.registerServiceFlow(SendFlow::class.java, ::ReceiveFlow)
|
bob.registerInitiatedFlow(ReceiveFlow::class.java)
|
||||||
val bobParty = bob.info.legalIdentity
|
val bobParty = bob.info.legalIdentity
|
||||||
// Perform a protocol exchange to force the peer queue to be created
|
// Perform a protocol exchange to force the peer queue to be created
|
||||||
alice.services.startFlow(SendFlow(bobParty, 0)).resultFuture.getOrThrow()
|
alice.services.startFlow(SendFlow(bobParty, 0)).resultFuture.getOrThrow()
|
||||||
@ -229,6 +230,7 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
|||||||
override fun call() = send(otherParty, payload)
|
override fun call() = send(otherParty, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(SendFlow::class)
|
||||||
private class ReceiveFlow(val otherParty: Party) : FlowLogic<Any>() {
|
private class ReceiveFlow(val otherParty: Party) : FlowLogic<Any>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() = receive<Any>(otherParty).unwrap { it }
|
override fun call() = receive<Any>(otherParty).unwrap { it }
|
||||||
|
@ -7,6 +7,8 @@ import com.google.common.util.concurrent.*
|
|||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
import com.typesafe.config.ConfigRenderOptions
|
import com.typesafe.config.ConfigRenderOptions
|
||||||
import net.corda.client.rpc.CordaRPCClient
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
|
import net.corda.cordform.CordformContext
|
||||||
|
import net.corda.cordform.CordformNode
|
||||||
import net.corda.core.*
|
import net.corda.core.*
|
||||||
import net.corda.core.crypto.X509Utilities
|
import net.corda.core.crypto.X509Utilities
|
||||||
import net.corda.core.crypto.appendToCommonName
|
import net.corda.core.crypto.appendToCommonName
|
||||||
@ -19,10 +21,6 @@ import net.corda.core.node.services.ServiceType
|
|||||||
import net.corda.core.utilities.*
|
import net.corda.core.utilities.*
|
||||||
import net.corda.node.LOGS_DIRECTORY_NAME
|
import net.corda.node.LOGS_DIRECTORY_NAME
|
||||||
import net.corda.node.services.config.*
|
import net.corda.node.services.config.*
|
||||||
import net.corda.node.services.config.ConfigHelper
|
|
||||||
import net.corda.node.services.config.FullNodeConfiguration
|
|
||||||
import net.corda.node.services.config.VerifierType
|
|
||||||
import net.corda.node.services.config.configOf
|
|
||||||
import net.corda.node.services.network.NetworkMapService
|
import net.corda.node.services.network.NetworkMapService
|
||||||
import net.corda.node.services.transactions.RaftValidatingNotaryService
|
import net.corda.node.services.transactions.RaftValidatingNotaryService
|
||||||
import net.corda.node.utilities.ServiceIdentityGenerator
|
import net.corda.node.utilities.ServiceIdentityGenerator
|
||||||
@ -30,8 +28,6 @@ import net.corda.nodeapi.ArtemisMessagingComponent
|
|||||||
import net.corda.nodeapi.User
|
import net.corda.nodeapi.User
|
||||||
import net.corda.nodeapi.config.SSLConfiguration
|
import net.corda.nodeapi.config.SSLConfiguration
|
||||||
import net.corda.nodeapi.config.parseAs
|
import net.corda.nodeapi.config.parseAs
|
||||||
import net.corda.cordform.CordformNode
|
|
||||||
import net.corda.cordform.CordformContext
|
|
||||||
import net.corda.core.internal.ShutdownHook
|
import net.corda.core.internal.ShutdownHook
|
||||||
import net.corda.core.internal.addShutdownHook
|
import net.corda.core.internal.addShutdownHook
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@ -39,6 +35,7 @@ import okhttp3.Request
|
|||||||
import org.bouncycastle.asn1.x500.X500Name
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.File.pathSeparator
|
||||||
import java.net.*
|
import java.net.*
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
@ -614,7 +611,7 @@ class DriverDSL(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun baseDirectory(nodeName: X500Name) = driverDirectory / nodeName.commonName.replace(WHITESPACE, "")
|
override fun baseDirectory(nodeName: X500Name): Path = driverDirectory / nodeName.commonName.replace(WHITESPACE, "")
|
||||||
|
|
||||||
override fun startDedicatedNetworkMapService(): ListenableFuture<Unit> {
|
override fun startDedicatedNetworkMapService(): ListenableFuture<Unit> {
|
||||||
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
|
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
|
||||||
@ -679,6 +676,8 @@ class DriverDSL(
|
|||||||
"-javaagent:$quasarJarPath"
|
"-javaagent:$quasarJarPath"
|
||||||
val loggingLevel = if (debugPort == null) "INFO" else "DEBUG"
|
val loggingLevel = if (debugPort == null) "INFO" else "DEBUG"
|
||||||
|
|
||||||
|
val pluginsDirectory = nodeConf.baseDirectory / "plugins"
|
||||||
|
|
||||||
ProcessUtilities.startJavaProcess(
|
ProcessUtilities.startJavaProcess(
|
||||||
className = "net.corda.node.Corda", // cannot directly get class for this, so just use string
|
className = "net.corda.node.Corda", // cannot directly get class for this, so just use string
|
||||||
arguments = listOf(
|
arguments = listOf(
|
||||||
@ -686,6 +685,8 @@ class DriverDSL(
|
|||||||
"--logging-level=$loggingLevel",
|
"--logging-level=$loggingLevel",
|
||||||
"--no-local-shell"
|
"--no-local-shell"
|
||||||
),
|
),
|
||||||
|
// Like the capsule, include the node's plugin directory
|
||||||
|
classpath = "${ProcessUtilities.defaultClassPath}$pathSeparator$pluginsDirectory/*",
|
||||||
jdwpPort = debugPort,
|
jdwpPort = debugPort,
|
||||||
extraJvmArguments = extraJvmArguments,
|
extraJvmArguments = extraJvmArguments,
|
||||||
errorLogPath = nodeConf.baseDirectory / LOGS_DIRECTORY_NAME / "error.log",
|
errorLogPath = nodeConf.baseDirectory / LOGS_DIRECTORY_NAME / "error.log",
|
||||||
|
@ -2,16 +2,15 @@ package net.corda.node.internal
|
|||||||
|
|
||||||
import com.codahale.metrics.MetricRegistry
|
import com.codahale.metrics.MetricRegistry
|
||||||
import com.google.common.annotations.VisibleForTesting
|
import com.google.common.annotations.VisibleForTesting
|
||||||
|
import com.google.common.collect.MutableClassToInstanceMap
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import com.google.common.util.concurrent.MoreExecutors
|
import com.google.common.util.concurrent.MoreExecutors
|
||||||
import com.google.common.util.concurrent.SettableFuture
|
import com.google.common.util.concurrent.SettableFuture
|
||||||
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
|
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
|
||||||
|
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
|
||||||
import net.corda.core.*
|
import net.corda.core.*
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.flows.FlowInitiator
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.InitiatingFlow
|
|
||||||
import net.corda.core.flows.StartableByRPC
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
import net.corda.core.messaging.RPCOps
|
import net.corda.core.messaging.RPCOps
|
||||||
@ -19,6 +18,7 @@ import net.corda.core.messaging.SingleMessageRecipient
|
|||||||
import net.corda.core.node.*
|
import net.corda.core.node.*
|
||||||
import net.corda.core.node.services.*
|
import net.corda.core.node.services.*
|
||||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||||
|
import net.corda.core.serialization.SerializeAsToken
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
@ -45,7 +45,7 @@ import net.corda.node.services.schema.HibernateObserver
|
|||||||
import net.corda.node.services.schema.NodeSchemaService
|
import net.corda.node.services.schema.NodeSchemaService
|
||||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||||
import net.corda.node.services.statemachine.StateMachineManager
|
import net.corda.node.services.statemachine.StateMachineManager
|
||||||
import net.corda.node.services.statemachine.flowVersion
|
import net.corda.node.services.statemachine.flowVersionAndInitiatingClass
|
||||||
import net.corda.node.services.transactions.*
|
import net.corda.node.services.transactions.*
|
||||||
import net.corda.node.services.vault.CashBalanceAsMetricsObserver
|
import net.corda.node.services.vault.CashBalanceAsMetricsObserver
|
||||||
import net.corda.node.services.vault.NodeVaultService
|
import net.corda.node.services.vault.NodeVaultService
|
||||||
@ -60,9 +60,11 @@ import org.bouncycastle.cert.X509CertificateHolder
|
|||||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
|
import rx.Observable
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.lang.reflect.Modifier.*
|
import java.lang.reflect.Modifier.*
|
||||||
import java.net.URL
|
import java.net.JarURLConnection
|
||||||
|
import java.net.URI
|
||||||
import java.nio.file.FileAlreadyExistsException
|
import java.nio.file.FileAlreadyExistsException
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
@ -73,6 +75,7 @@ import java.util.*
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.TimeUnit.SECONDS
|
import java.util.concurrent.TimeUnit.SECONDS
|
||||||
|
import java.util.stream.Collectors.toList
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
|
import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
|
||||||
@ -106,7 +109,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
|||||||
// low-performance prototyping period.
|
// low-performance prototyping period.
|
||||||
protected abstract val serverThread: AffinityExecutor
|
protected abstract val serverThread: AffinityExecutor
|
||||||
|
|
||||||
protected val serviceFlowFactories = ConcurrentHashMap<Class<*>, ServiceFlowInfo>()
|
private val cordappServices = MutableClassToInstanceMap.create<SerializeAsToken>()
|
||||||
|
private val flowFactories = ConcurrentHashMap<Class<out FlowLogic<*>>, InitiatedFlowFactory<*>>()
|
||||||
protected val partyKeys = mutableSetOf<KeyPair>()
|
protected val partyKeys = mutableSetOf<KeyPair>()
|
||||||
|
|
||||||
val services = object : ServiceHubInternal() {
|
val services = object : ServiceHubInternal() {
|
||||||
@ -122,6 +126,12 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
|||||||
override val schemaService: SchemaService get() = schemas
|
override val schemaService: SchemaService get() = schemas
|
||||||
override val transactionVerifierService: TransactionVerifierService get() = txVerifierService
|
override val transactionVerifierService: TransactionVerifierService get() = txVerifierService
|
||||||
override val auditService: AuditService get() = auditService
|
override val auditService: AuditService get() = auditService
|
||||||
|
|
||||||
|
override fun <T : SerializeAsToken> cordaService(type: Class<T>): T {
|
||||||
|
require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" }
|
||||||
|
return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
override val rpcFlows: List<Class<out FlowLogic<*>>> get() = this@AbstractNode.rpcFlows
|
override val rpcFlows: List<Class<out FlowLogic<*>>> get() = this@AbstractNode.rpcFlows
|
||||||
|
|
||||||
// Internal only
|
// Internal only
|
||||||
@ -131,17 +141,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
|||||||
return serverThread.fetchFrom { smm.add(logic, flowInitiator) }
|
return serverThread.fetchFrom { smm.add(logic, flowInitiator) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun registerServiceFlow(initiatingFlowClass: Class<out FlowLogic<*>>, serviceFlowFactory: (Party) -> FlowLogic<*>) {
|
override fun getFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>? {
|
||||||
require(initiatingFlowClass !in serviceFlowFactories) {
|
return flowFactories[initiatingFlowClass]
|
||||||
"${initiatingFlowClass.name} has already been used to register a service flow"
|
|
||||||
}
|
|
||||||
val info = ServiceFlowInfo.CorDapp(initiatingFlowClass.flowVersion, serviceFlowFactory)
|
|
||||||
log.info("Registering service flow for ${initiatingFlowClass.name}: $info")
|
|
||||||
serviceFlowFactories[initiatingFlowClass] = info
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getServiceFlowFactory(clientFlowClass: Class<out FlowLogic<*>>): ServiceFlowInfo? {
|
|
||||||
return serviceFlowFactories[clientFlowClass]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||||
@ -167,15 +168,11 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
|||||||
lateinit var scheduler: NodeSchedulerService
|
lateinit var scheduler: NodeSchedulerService
|
||||||
lateinit var schemas: SchemaService
|
lateinit var schemas: SchemaService
|
||||||
lateinit var auditService: AuditService
|
lateinit var auditService: AuditService
|
||||||
val customServices: ArrayList<Any> = ArrayList()
|
|
||||||
protected val runOnStop: ArrayList<Runnable> = ArrayList()
|
protected val runOnStop: ArrayList<Runnable> = ArrayList()
|
||||||
lateinit var database: Database
|
lateinit var database: Database
|
||||||
protected var dbCloser: Runnable? = null
|
protected var dbCloser: Runnable? = null
|
||||||
private lateinit var rpcFlows: List<Class<out FlowLogic<*>>>
|
private lateinit var rpcFlows: List<Class<out FlowLogic<*>>>
|
||||||
|
|
||||||
/** Locates and returns a service of the given type if loaded, or throws an exception if not found. */
|
|
||||||
inline fun <reified T : Any> findService() = customServices.filterIsInstance<T>().single()
|
|
||||||
|
|
||||||
var isPreviousCheckpointsPresent = false
|
var isPreviousCheckpointsPresent = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@ -217,7 +214,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
|||||||
val tokenizableServices = makeServices()
|
val tokenizableServices = makeServices()
|
||||||
|
|
||||||
smm = StateMachineManager(services,
|
smm = StateMachineManager(services,
|
||||||
listOf(tokenizableServices),
|
|
||||||
checkpointStorage,
|
checkpointStorage,
|
||||||
serverThread,
|
serverThread,
|
||||||
database,
|
database,
|
||||||
@ -240,22 +236,24 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
|||||||
startMessagingService(rpcOps)
|
startMessagingService(rpcOps)
|
||||||
installCoreFlows()
|
installCoreFlows()
|
||||||
|
|
||||||
fun Class<out FlowLogic<*>>.isUserInvokable(): Boolean {
|
val scanResult = scanCorDapps()
|
||||||
return isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || isStatic(modifiers))
|
if (scanResult != null) {
|
||||||
|
val cordappServices = installCordaServices(scanResult)
|
||||||
|
tokenizableServices.addAll(cordappServices)
|
||||||
|
registerInitiatedFlows(scanResult)
|
||||||
|
rpcFlows = findRPCFlows(scanResult)
|
||||||
|
} else {
|
||||||
|
rpcFlows = emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val flows = scanForFlows()
|
// TODO Remove this once the cash stuff is in its own CorDapp
|
||||||
rpcFlows = flows.filter { it.isUserInvokable() && it.isAnnotationPresent(StartableByRPC::class.java) } +
|
registerInitiatedFlow(IssuerFlow.Issuer::class.java)
|
||||||
// Add any core flows here
|
|
||||||
listOf(ContractUpgradeFlow::class.java,
|
initUploaders()
|
||||||
// TODO Remove all Cash flows from default list once they are split into separate CorDapp.
|
|
||||||
CashIssueFlow::class.java,
|
|
||||||
CashExitFlow::class.java,
|
|
||||||
CashPaymentFlow::class.java)
|
|
||||||
|
|
||||||
runOnStop += Runnable { net.stop() }
|
runOnStop += Runnable { net.stop() }
|
||||||
_networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured())
|
_networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured())
|
||||||
smm.start()
|
smm.start(tokenizableServices)
|
||||||
// Shut down the SMM so no Fibers are scheduled.
|
// Shut down the SMM so no Fibers are scheduled.
|
||||||
runOnStop += Runnable { smm.stop(acceptableLiveFiberCountOnStop()) }
|
runOnStop += Runnable { smm.stop(acceptableLiveFiberCountOnStop()) }
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
@ -264,18 +262,142 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun installCordaServices(scanResult: ScanResult): List<SerializeAsToken> {
|
||||||
|
return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class).mapNotNull {
|
||||||
|
try {
|
||||||
|
installCordaService(it)
|
||||||
|
} catch (e: NoSuchMethodException) {
|
||||||
|
log.error("${it.name}, as a Corda service, must have a constructor with a single parameter " +
|
||||||
|
"of type ${PluginServiceHub::class.java.name}")
|
||||||
|
null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error("Unable to install Corda service ${it.name}", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this method to install your Corda services in your tests. This is automatically done by the node when it
|
||||||
|
* starts up for all classes it finds which are annotated with [CordaService].
|
||||||
|
*/
|
||||||
|
fun <T : SerializeAsToken> installCordaService(clazz: Class<T>): T {
|
||||||
|
clazz.requireAnnotation<CordaService>()
|
||||||
|
val ctor = clazz.getDeclaredConstructor(PluginServiceHub::class.java).apply { isAccessible = true }
|
||||||
|
val service = ctor.newInstance(services)
|
||||||
|
cordappServices.putInstance(clazz, service)
|
||||||
|
log.info("Installed ${clazz.name} Corda service")
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified A : Annotation> Class<*>.requireAnnotation(): A {
|
||||||
|
return requireNotNull(getDeclaredAnnotation(A::class.java)) { "$name needs to be annotated with ${A::class.java.name}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerInitiatedFlows(scanResult: ScanResult) {
|
||||||
|
scanResult
|
||||||
|
.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class)
|
||||||
|
// First group by the initiating flow class in case there are multiple mappings
|
||||||
|
.groupBy { it.requireAnnotation<InitiatedBy>().value.java }
|
||||||
|
.map { (initiatingFlow, initiatedFlows) ->
|
||||||
|
val sorted = initiatedFlows.sortedWith(FlowTypeHierarchyComparator(initiatingFlow))
|
||||||
|
if (sorted.size > 1) {
|
||||||
|
log.warn("${initiatingFlow.name} has been specified as the inititating flow by multiple flows " +
|
||||||
|
"in the same type hierarchy: ${sorted.joinToString { it.name }}. Choosing the most " +
|
||||||
|
"specific sub-type for registration: ${sorted[0].name}.")
|
||||||
|
}
|
||||||
|
sorted[0]
|
||||||
|
}
|
||||||
|
.forEach {
|
||||||
|
try {
|
||||||
|
registerInitiatedFlowInternal(it, track = false)
|
||||||
|
} catch (e: NoSuchMethodException) {
|
||||||
|
log.error("${it.name}, as an initiated flow, must have a constructor with a single parameter " +
|
||||||
|
"of type ${Party::class.java.name}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error("Unable to register initiated flow ${it.name}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FlowTypeHierarchyComparator(val initiatingFlow: Class<out FlowLogic<*>>) : Comparator<Class<out FlowLogic<*>>> {
|
||||||
|
override fun compare(o1: Class<out FlowLogic<*>>, o2: Class<out FlowLogic<*>>): Int {
|
||||||
|
return if (o1 == o2) {
|
||||||
|
0
|
||||||
|
} else if (o1.isAssignableFrom(o2)) {
|
||||||
|
1
|
||||||
|
} else if (o2.isAssignableFrom(o1)) {
|
||||||
|
-1
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("${initiatingFlow.name} has been specified as the initiating flow by " +
|
||||||
|
"both ${o1.name} and ${o2.name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this method to register your initiated flows in your tests. This is automatically done by the node when it
|
||||||
|
* starts up for all [FlowLogic] classes it finds which are annotated with [InitiatedBy].
|
||||||
|
* @return An [Observable] of the initiated flows started by counter-parties.
|
||||||
|
*/
|
||||||
|
fun <T : FlowLogic<*>> registerInitiatedFlow(initiatedFlowClass: Class<T>): Observable<T> {
|
||||||
|
return registerInitiatedFlowInternal(initiatedFlowClass, track = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <F : FlowLogic<*>> registerInitiatedFlowInternal(initiatedFlow: Class<F>, track: Boolean): Observable<F> {
|
||||||
|
val ctor = initiatedFlow.getDeclaredConstructor(Party::class.java).apply { isAccessible = true }
|
||||||
|
val initiatingFlow = initiatedFlow.requireAnnotation<InitiatedBy>().value.java
|
||||||
|
val (version, classWithAnnotation) = initiatingFlow.flowVersionAndInitiatingClass
|
||||||
|
require(classWithAnnotation == initiatingFlow) {
|
||||||
|
"${InitiatingFlow::class.java.name} must be annotated on ${initiatingFlow.name} and not on a super-type"
|
||||||
|
}
|
||||||
|
val flowFactory = InitiatedFlowFactory.CorDapp(version, { ctor.newInstance(it) })
|
||||||
|
val observable = registerFlowFactory(initiatingFlow, flowFactory, initiatedFlow, track)
|
||||||
|
log.info("Registered ${initiatingFlow.name} to initiate ${initiatedFlow.name} (version $version)")
|
||||||
|
return observable
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun <F : FlowLogic<*>> registerFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>,
|
||||||
|
flowFactory: InitiatedFlowFactory<F>,
|
||||||
|
initiatedFlowClass: Class<F>,
|
||||||
|
track: Boolean): Observable<F> {
|
||||||
|
val observable = if (track) {
|
||||||
|
smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass)
|
||||||
|
} else {
|
||||||
|
Observable.empty()
|
||||||
|
}
|
||||||
|
flowFactories[initiatingFlowClass] = flowFactory
|
||||||
|
return observable
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findRPCFlows(scanResult: ScanResult): List<Class<out FlowLogic<*>>> {
|
||||||
|
fun Class<out FlowLogic<*>>.isUserInvokable(): Boolean {
|
||||||
|
return isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || isStatic(modifiers))
|
||||||
|
}
|
||||||
|
|
||||||
|
return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() } +
|
||||||
|
// Add any core flows here
|
||||||
|
listOf(
|
||||||
|
ContractUpgradeFlow::class.java,
|
||||||
|
// TODO Remove all Cash flows from default list once they are split into separate CorDapp.
|
||||||
|
CashIssueFlow::class.java,
|
||||||
|
CashExitFlow::class.java,
|
||||||
|
CashPaymentFlow::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs a flow that's core to the Corda platform. Unlike CorDapp flows which are versioned individually using
|
* Installs a flow that's core to the Corda platform. Unlike CorDapp flows which are versioned individually using
|
||||||
* [InitiatingFlow.version], core flows have the same version as the node's platform version. To cater for backwards
|
* [InitiatingFlow.version], core flows have the same version as the node's platform version. To cater for backwards
|
||||||
* compatibility [serviceFlowFactory] provides a second parameter which is the platform version of the initiating party.
|
* compatibility [flowFactory] provides a second parameter which is the platform version of the initiating party.
|
||||||
* @suppress
|
* @suppress
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
fun installCoreFlow(clientFlowClass: KClass<out FlowLogic<*>>, serviceFlowFactory: (Party, Int) -> FlowLogic<*>) {
|
fun installCoreFlow(clientFlowClass: KClass<out FlowLogic<*>>, flowFactory: (Party, Int) -> FlowLogic<*>) {
|
||||||
require(clientFlowClass.java.flowVersion == 1) {
|
require(clientFlowClass.java.flowVersionAndInitiatingClass.first == 1) {
|
||||||
"${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version"
|
"${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version"
|
||||||
}
|
}
|
||||||
serviceFlowFactories[clientFlowClass.java] = ServiceFlowInfo.Core(serviceFlowFactory)
|
flowFactories[clientFlowClass.java] = InitiatedFlowFactory.Core(flowFactory)
|
||||||
log.debug { "Installed core flow ${clientFlowClass.java.name}" }
|
log.debug { "Installed core flow ${clientFlowClass.java.name}" }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,61 +435,62 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
|||||||
val tokenizableServices = mutableListOf(storage, net, vault, keyManagement, identity, platformClock, scheduler)
|
val tokenizableServices = mutableListOf(storage, net, vault, keyManagement, identity, platformClock, scheduler)
|
||||||
makeAdvertisedServices(tokenizableServices)
|
makeAdvertisedServices(tokenizableServices)
|
||||||
|
|
||||||
customServices.clear()
|
|
||||||
customServices.addAll(makePluginServices(tokenizableServices))
|
|
||||||
|
|
||||||
initUploaders(storageServices)
|
|
||||||
return tokenizableServices
|
return tokenizableServices
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scanForFlows(): List<Class<out FlowLogic<*>>> {
|
private fun scanCorDapps(): ScanResult? {
|
||||||
|
val scanPackage = System.getProperty("net.corda.node.cordapp.scan.package")
|
||||||
|
val paths = if (scanPackage != null) {
|
||||||
|
// This is purely for integration tests so that classes defined in the test can automatically be picked up
|
||||||
|
check(configuration.devMode) { "Package scanning can only occur in dev mode" }
|
||||||
|
val resource = scanPackage.replace('.', '/')
|
||||||
|
javaClass.classLoader.getResources(resource)
|
||||||
|
.asSequence()
|
||||||
|
.map {
|
||||||
|
val uri = if (it.protocol == "jar") {
|
||||||
|
(it.openConnection() as JarURLConnection).jarFileURL.toURI()
|
||||||
|
} else {
|
||||||
|
URI(it.toExternalForm().removeSuffix(resource))
|
||||||
|
}
|
||||||
|
Paths.get(uri)
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
} else {
|
||||||
val pluginsDir = configuration.baseDirectory / "plugins"
|
val pluginsDir = configuration.baseDirectory / "plugins"
|
||||||
log.info("Scanning plugins in $pluginsDir ...")
|
if (!pluginsDir.exists()) return null
|
||||||
if (!pluginsDir.exists()) return emptyList()
|
pluginsDir.list {
|
||||||
|
it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.collect(toList())
|
||||||
val pluginJars = pluginsDir.list {
|
}
|
||||||
it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.toArray()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pluginJars.isEmpty()) return emptyList()
|
log.info("Scanning CorDapps in $paths")
|
||||||
|
|
||||||
val scanResult = FastClasspathScanner().overrideClasspath(*pluginJars).scan() // This will only scan the plugin jars and nothing else
|
// This will only scan the plugin jars and nothing else
|
||||||
|
return if (paths.isNotEmpty()) FastClasspathScanner().overrideClasspath(paths).scan() else null
|
||||||
|
}
|
||||||
|
|
||||||
fun loadFlowClass(className: String): Class<out FlowLogic<*>>? {
|
private fun <T : Any> ScanResult.getClassesWithAnnotation(type: KClass<T>, annotation: KClass<out Annotation>): List<Class<out T>> {
|
||||||
|
fun loadClass(className: String): Class<out T>? {
|
||||||
return try {
|
return try {
|
||||||
// TODO Make sure this is loaded by the correct class loader
|
// TODO Make sure this is loaded by the correct class loader
|
||||||
@Suppress("UNCHECKED_CAST")
|
Class.forName(className, false, javaClass.classLoader).asSubclass(type.java)
|
||||||
Class.forName(className, false, javaClass.classLoader) as Class<out FlowLogic<*>>
|
} catch (e: ClassCastException) {
|
||||||
|
log.warn("As $className is annotated with ${annotation.qualifiedName} it must be a sub-type of ${type.java.name}")
|
||||||
|
null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.warn("Unable to load flow class $className", e)
|
log.warn("Unable to load class $className", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val flowClasses = scanResult.getNamesOfSubclassesOf(FlowLogic::class.java)
|
return getNamesOfClassesWithAnnotation(annotation.java)
|
||||||
.mapNotNull { loadFlowClass(it) }
|
.mapNotNull { loadClass(it) }
|
||||||
.filterNot { isAbstract(it.modifiers) }
|
.filterNot { isAbstract(it.modifiers) }
|
||||||
|
|
||||||
fun URL.pluginName(): String {
|
|
||||||
return try {
|
|
||||||
Paths.get(toURI()).fileName.toString()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
toString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flowClasses.groupBy {
|
private fun initUploaders() {
|
||||||
scanResult.classNameToClassInfo[it.name]!!.classpathElementURLs.first()
|
val uploaders: List<FileUploader> = listOf(storage.attachments as NodeAttachmentService) +
|
||||||
}.forEach { url, classes ->
|
cordappServices.values.filterIsInstance(AcceptsFileUpload::class.java)
|
||||||
log.info("Found flows in plugin ${url.pluginName()}: ${classes.joinToString { it.name }}")
|
|
||||||
}
|
|
||||||
|
|
||||||
return flowClasses
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initUploaders(storageServices: Pair<TxWritableStorageService, CheckpointStorage>) {
|
|
||||||
val uploaders: List<FileUploader> = listOf(storageServices.first.attachments as NodeAttachmentService) +
|
|
||||||
customServices.filterIsInstance(AcceptsFileUpload::class.java)
|
|
||||||
(storage as StorageServiceImpl).initUploaders(uploaders)
|
(storage as StorageServiceImpl).initUploaders(uploaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -433,12 +556,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun makePluginServices(tokenizableServices: MutableList<Any>): List<Any> {
|
|
||||||
val pluginServices = pluginRegistries.flatMap { it.servicePlugins }.map { it.apply(services) }
|
|
||||||
tokenizableServices.addAll(pluginServices)
|
|
||||||
return pluginServices
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run any tasks that are needed to ensure the node is in a correct state before running start().
|
* Run any tasks that are needed to ensure the node is in a correct state before running start().
|
||||||
*/
|
*/
|
||||||
@ -658,11 +775,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class ServiceFlowInfo {
|
|
||||||
data class Core(val factory: (Party, Int) -> FlowLogic<*>) : ServiceFlowInfo()
|
|
||||||
data class CorDapp(val version: Int, val factory: (Party) -> FlowLogic<*>) : ServiceFlowInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
private class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) {
|
private class KeyStoreWrapper(private val storePath: Path, private val storePassword: String) {
|
||||||
private val keyStore = KeyStoreUtilities.loadKeyStore(storePath, storePassword)
|
private val keyStore = KeyStoreUtilities.loadKeyStore(storePath, storePassword)
|
||||||
|
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
package net.corda.node.internal
|
||||||
|
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.node.services.statemachine.SessionInit
|
||||||
|
|
||||||
|
interface InitiatedFlowFactory<out F : FlowLogic<*>> {
|
||||||
|
fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): F
|
||||||
|
|
||||||
|
data class Core<out F : FlowLogic<*>>(val factory: (Party, Int) -> F) : InitiatedFlowFactory<F> {
|
||||||
|
override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): F {
|
||||||
|
return factory(otherParty, platformVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CorDapp<out F : FlowLogic<*>>(val version: Int, val factory: (Party) -> F) : InitiatedFlowFactory<F> {
|
||||||
|
override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): F {
|
||||||
|
// TODO Add support for multiple versions of the same flow when CorDapps are loaded in separate class loaders
|
||||||
|
if (sessionInit.flowVerison == version) return factory(otherParty)
|
||||||
|
throw SessionRejectException(
|
||||||
|
"Version not supported",
|
||||||
|
"Version mismatch - ${sessionInit.initiatingFlowClass} is only registered for version $version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SessionRejectException(val rejectMessage: String, val logMessage: String) : Exception()
|
@ -13,7 +13,7 @@ import net.corda.core.node.services.TxWritableStorageService
|
|||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import net.corda.node.internal.ServiceFlowInfo
|
import net.corda.node.internal.InitiatedFlowFactory
|
||||||
import net.corda.node.services.messaging.MessagingService
|
import net.corda.node.services.messaging.MessagingService
|
||||||
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
||||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||||
@ -47,7 +47,6 @@ interface NetworkMapCacheInternal : NetworkMapCache {
|
|||||||
/** For testing where the network map cache is manipulated marks the service as immediately ready. */
|
/** For testing where the network map cache is manipulated marks the service as immediately ready. */
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
fun runWithoutMapService()
|
fun runWithoutMapService()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
@ -93,7 +92,6 @@ abstract class ServiceHubInternal : PluginServiceHub {
|
|||||||
* Starts an already constructed flow. Note that you must be on the server thread to call this method. [FlowInitiator]
|
* Starts an already constructed flow. Note that you must be on the server thread to call this method. [FlowInitiator]
|
||||||
* defaults to [FlowInitiator.RPC] with username "Only For Testing".
|
* defaults to [FlowInitiator.RPC] with username "Only For Testing".
|
||||||
*/
|
*/
|
||||||
// TODO Move it to test utils.
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
fun <T> startFlow(logic: FlowLogic<T>): FlowStateMachine<T> = startFlow(logic, FlowInitiator.RPC("Only For Testing"))
|
fun <T> startFlow(logic: FlowLogic<T>): FlowStateMachine<T> = startFlow(logic, FlowInitiator.RPC("Only For Testing"))
|
||||||
|
|
||||||
@ -103,7 +101,6 @@ abstract class ServiceHubInternal : PluginServiceHub {
|
|||||||
*/
|
*/
|
||||||
abstract fun <T> startFlow(logic: FlowLogic<T>, flowInitiator: FlowInitiator): FlowStateMachineImpl<T>
|
abstract fun <T> startFlow(logic: FlowLogic<T>, flowInitiator: FlowInitiator): FlowStateMachineImpl<T>
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will check [logicType] and [args] against a whitelist and if acceptable then construct and initiate the flow.
|
* Will check [logicType] and [args] against a whitelist and if acceptable then construct and initiate the flow.
|
||||||
* Note that you must be on the server thread to call this method. [flowInitiator] points how flow was started,
|
* Note that you must be on the server thread to call this method. [flowInitiator] points how flow was started,
|
||||||
@ -122,5 +119,5 @@ abstract class ServiceHubInternal : PluginServiceHub {
|
|||||||
return startFlow(logic, flowInitiator)
|
return startFlow(logic, flowInitiator)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun getServiceFlowFactory(clientFlowClass: Class<out FlowLogic<*>>): ServiceFlowInfo?
|
abstract fun getFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>?
|
||||||
}
|
}
|
@ -8,9 +8,9 @@ import com.google.common.util.concurrent.ListenableFuture
|
|||||||
import com.google.common.util.concurrent.SettableFuture
|
import com.google.common.util.concurrent.SettableFuture
|
||||||
import net.corda.core.ErrorOr
|
import net.corda.core.ErrorOr
|
||||||
import net.corda.core.abbreviate
|
import net.corda.core.abbreviate
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.random63BitValue
|
import net.corda.core.random63BitValue
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
@ -27,7 +27,6 @@ import org.jetbrains.exposed.sql.Transaction
|
|||||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.lang.reflect.Modifier
|
|
||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
import java.sql.SQLException
|
import java.sql.SQLException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -322,9 +321,8 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
|||||||
logger.trace { "Initiating a new session with $otherParty" }
|
logger.trace { "Initiating a new session with $otherParty" }
|
||||||
val session = FlowSession(sessionFlow, random63BitValue(), null, FlowSessionState.Initiating(otherParty), retryable)
|
val session = FlowSession(sessionFlow, random63BitValue(), null, FlowSessionState.Initiating(otherParty), retryable)
|
||||||
openSessions[Pair(sessionFlow, otherParty)] = session
|
openSessions[Pair(sessionFlow, otherParty)] = session
|
||||||
// We get the top-most concrete class object to cater for the case where the client flow is customised via a sub-class
|
val (version, initiatingFlowClass) = sessionFlow.javaClass.flowVersionAndInitiatingClass
|
||||||
val clientFlowClass = sessionFlow.topConcreteFlowClass
|
val sessionInit = SessionInit(session.ourSessionId, initiatingFlowClass, version, firstPayload)
|
||||||
val sessionInit = SessionInit(session.ourSessionId, clientFlowClass, clientFlowClass.flowVersion, firstPayload)
|
|
||||||
sendInternal(session, sessionInit)
|
sendInternal(session, sessionInit)
|
||||||
if (waitForConfirmation) {
|
if (waitForConfirmation) {
|
||||||
session.waitForConfirmation()
|
session.waitForConfirmation()
|
||||||
@ -332,15 +330,6 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
private val FlowLogic<*>.topConcreteFlowClass: Class<out FlowLogic<*>> get() {
|
|
||||||
var current: Class<out FlowLogic<*>> = javaClass
|
|
||||||
while (!Modifier.isAbstract(current.superclass.modifiers)) {
|
|
||||||
current = current.superclass as Class<out FlowLogic<*>>
|
|
||||||
}
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun <M : ExistingSessionMessage> waitForMessage(receiveRequest: ReceiveRequest<M>): ReceivedSessionMessage<M> {
|
private fun <M : ExistingSessionMessage> waitForMessage(receiveRequest: ReceiveRequest<M>): ReceivedSessionMessage<M> {
|
||||||
return receiveRequest.suspendAndExpectReceive().confirmReceiveType(receiveRequest)
|
return receiveRequest.suspendAndExpectReceive().confirmReceiveType(receiveRequest)
|
||||||
@ -460,10 +449,19 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val Class<out FlowLogic<*>>.flowVersion: Int get() {
|
@Suppress("UNCHECKED_CAST")
|
||||||
val annotation = requireNotNull(getAnnotation(InitiatingFlow::class.java)) {
|
val Class<out FlowLogic<*>>.flowVersionAndInitiatingClass: Pair<Int, Class<out FlowLogic<*>>> get() {
|
||||||
"$name as the initiating flow must be annotated with ${InitiatingFlow::class.java.name}"
|
var current: Class<*> = this
|
||||||
}
|
var found: Pair<Int, Class<out FlowLogic<*>>>? = null
|
||||||
|
while (true) {
|
||||||
|
val annotation = current.getDeclaredAnnotation(InitiatingFlow::class.java)
|
||||||
|
if (annotation != null) {
|
||||||
|
if (found != null) throw IllegalArgumentException("${InitiatingFlow::class.java.name} can only be annotated once")
|
||||||
require(annotation.version > 0) { "Flow versions have to be greater or equal to 1" }
|
require(annotation.version > 0) { "Flow versions have to be greater or equal to 1" }
|
||||||
return annotation.version
|
found = annotation.version to (current as Class<out FlowLogic<*>>)
|
||||||
|
}
|
||||||
|
current = current.superclass
|
||||||
|
?: return found
|
||||||
|
?: throw IllegalArgumentException("$name as an initiating flow must be annotated with ${InitiatingFlow::class.java.name}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ import net.corda.core.utilities.UntrustworthyData
|
|||||||
interface SessionMessage
|
interface SessionMessage
|
||||||
|
|
||||||
data class SessionInit(val initiatorSessionId: Long,
|
data class SessionInit(val initiatorSessionId: Long,
|
||||||
val clientFlowClass: Class<out FlowLogic<*>>,
|
val initiatingFlowClass: Class<out FlowLogic<*>>,
|
||||||
val flowVerison: Int,
|
val flowVerison: Int,
|
||||||
val firstPayload: Any?) : SessionMessage
|
val firstPayload: Any?) : SessionMessage
|
||||||
|
|
||||||
|
@ -15,14 +15,14 @@ import com.google.common.collect.HashMultimap
|
|||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import io.requery.util.CloseableIterator
|
import io.requery.util.CloseableIterator
|
||||||
import net.corda.core.*
|
import net.corda.core.*
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.serialization.*
|
import net.corda.core.serialization.*
|
||||||
import net.corda.core.utilities.debug
|
import net.corda.core.utilities.debug
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import net.corda.core.utilities.trace
|
import net.corda.core.utilities.trace
|
||||||
import net.corda.node.internal.ServiceFlowInfo
|
import net.corda.node.internal.SessionRejectException
|
||||||
import net.corda.node.services.api.Checkpoint
|
import net.corda.node.services.api.Checkpoint
|
||||||
import net.corda.node.services.api.CheckpointStorage
|
import net.corda.node.services.api.CheckpointStorage
|
||||||
import net.corda.node.services.api.ServiceHubInternal
|
import net.corda.node.services.api.ServiceHubInternal
|
||||||
@ -61,7 +61,6 @@ import javax.annotation.concurrent.ThreadSafe
|
|||||||
*/
|
*/
|
||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
class StateMachineManager(val serviceHub: ServiceHubInternal,
|
class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||||
tokenizableServices: List<Any>,
|
|
||||||
val checkpointStorage: CheckpointStorage,
|
val checkpointStorage: CheckpointStorage,
|
||||||
val executor: AffinityExecutor,
|
val executor: AffinityExecutor,
|
||||||
val database: Database,
|
val database: Database,
|
||||||
@ -147,7 +146,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
|||||||
private val recentlyClosedSessions = ConcurrentHashMap<Long, Party>()
|
private val recentlyClosedSessions = ConcurrentHashMap<Long, Party>()
|
||||||
|
|
||||||
// Context for tokenized services in checkpoints
|
// Context for tokenized services in checkpoints
|
||||||
private val serializationContext = SerializeAsTokenContext(tokenizableServices, quasarKryoPool, serviceHub)
|
private lateinit var serializationContext: SerializeAsTokenContext
|
||||||
|
|
||||||
/** Returns a list of all state machines executing the given flow logic at the top level (subflows do not count) */
|
/** Returns a list of all state machines executing the given flow logic at the top level (subflows do not count) */
|
||||||
fun <P : FlowLogic<T>, T> findStateMachines(flowClass: Class<P>): List<Pair<P, ListenableFuture<T>>> {
|
fun <P : FlowLogic<T>, T> findStateMachines(flowClass: Class<P>): List<Pair<P, ListenableFuture<T>>> {
|
||||||
@ -171,7 +170,8 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
|||||||
*/
|
*/
|
||||||
val changes: Observable<Change> = mutex.content.changesPublisher.wrapWithDatabaseTransaction()
|
val changes: Observable<Change> = mutex.content.changesPublisher.wrapWithDatabaseTransaction()
|
||||||
|
|
||||||
fun start() {
|
fun start(tokenizableServices: List<Any>) {
|
||||||
|
serializationContext = SerializeAsTokenContext(tokenizableServices, quasarKryoPool, serviceHub)
|
||||||
restoreFibersFromCheckpoints()
|
restoreFibersFromCheckpoints()
|
||||||
listenToLedgerTransactions()
|
listenToLedgerTransactions()
|
||||||
serviceHub.networkMapCache.mapServiceRegistered.then(executor) { resumeRestoredFibers() }
|
serviceHub.networkMapCache.mapServiceRegistered.then(executor) { resumeRestoredFibers() }
|
||||||
@ -345,28 +345,15 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
|||||||
|
|
||||||
fun sendSessionReject(message: String) = sendSessionMessage(sender, SessionReject(otherPartySessionId, message))
|
fun sendSessionReject(message: String) = sendSessionMessage(sender, SessionReject(otherPartySessionId, message))
|
||||||
|
|
||||||
val serviceFlowInfo = serviceHub.getServiceFlowFactory(sessionInit.clientFlowClass)
|
val initiatedFlowFactory = serviceHub.getFlowFactory(sessionInit.initiatingFlowClass)
|
||||||
if (serviceFlowInfo == null) {
|
if (initiatedFlowFactory == null) {
|
||||||
logger.warn("${sessionInit.clientFlowClass} has not been registered with a service flow: $sessionInit")
|
logger.warn("${sessionInit.initiatingFlowClass} has not been registered: $sessionInit")
|
||||||
sendSessionReject("${sessionInit.clientFlowClass.name} has not been registered with a service flow")
|
sendSessionReject("${sessionInit.initiatingFlowClass.name} has not been registered with a service flow")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val session = try {
|
val session = try {
|
||||||
val flow = when (serviceFlowInfo) {
|
val flow = initiatedFlowFactory.createFlow(receivedMessage.platformVersion, sender, sessionInit)
|
||||||
is ServiceFlowInfo.CorDapp -> {
|
|
||||||
// TODO Add support for multiple versions of the same flow when CorDapps are loaded in separate class loaders
|
|
||||||
if (sessionInit.flowVerison != serviceFlowInfo.version) {
|
|
||||||
logger.warn("Version mismatch - ${sessionInit.clientFlowClass} is only registered for version " +
|
|
||||||
"${serviceFlowInfo.version}: $sessionInit")
|
|
||||||
sendSessionReject("Version not supported")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
serviceFlowInfo.factory(sender)
|
|
||||||
}
|
|
||||||
is ServiceFlowInfo.Core -> serviceFlowInfo.factory(sender, receivedMessage.platformVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
val fiber = createFiber(flow, FlowInitiator.Peer(sender))
|
val fiber = createFiber(flow, FlowInitiator.Peer(sender))
|
||||||
val session = FlowSession(flow, random63BitValue(), sender, FlowSessionState.Initiated(sender, otherPartySessionId))
|
val session = FlowSession(flow, random63BitValue(), sender, FlowSessionState.Initiated(sender, otherPartySessionId))
|
||||||
if (sessionInit.firstPayload != null) {
|
if (sessionInit.firstPayload != null) {
|
||||||
@ -376,6 +363,10 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
|||||||
fiber.openSessions[Pair(flow, sender)] = session
|
fiber.openSessions[Pair(flow, sender)] = session
|
||||||
updateCheckpoint(fiber)
|
updateCheckpoint(fiber)
|
||||||
session
|
session
|
||||||
|
} catch (e: SessionRejectException) {
|
||||||
|
logger.warn("${e.logMessage}: $sessionInit")
|
||||||
|
sendSessionReject(e.rejectMessage)
|
||||||
|
return
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn("Couldn't start flow session from $sessionInit", e)
|
logger.warn("Couldn't start flow session from $sessionInit", e)
|
||||||
sendSessionReject("Unable to establish session")
|
sendSessionReject("Unable to establish session")
|
||||||
@ -383,7 +374,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendSessionMessage(sender, SessionConfirm(otherPartySessionId, session.ourSessionId), session.fiber)
|
sendSessionMessage(sender, SessionConfirm(otherPartySessionId, session.ourSessionId), session.fiber)
|
||||||
session.fiber.logger.debug { "Initiated by $sender using ${sessionInit.clientFlowClass.name}" }
|
session.fiber.logger.debug { "Initiated by $sender using ${sessionInit.initiatingFlowClass.name}" }
|
||||||
session.fiber.logger.trace { "Initiated from $sessionInit on $session" }
|
session.fiber.logger.trace { "Initiated from $sessionInit on $session" }
|
||||||
resumeFiber(session.fiber)
|
resumeFiber(session.fiber)
|
||||||
}
|
}
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
package net.corda.node.internal
|
|
||||||
|
|
||||||
import net.corda.core.createDirectories
|
|
||||||
import net.corda.core.crypto.commonName
|
|
||||||
import net.corda.core.div
|
|
||||||
import net.corda.core.getOrThrow
|
|
||||||
import net.corda.core.utilities.ALICE
|
|
||||||
import net.corda.core.utilities.WHITESPACE
|
|
||||||
import net.corda.testing.node.NodeBasedTest
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class NodeTest : NodeBasedTest() {
|
|
||||||
@Test
|
|
||||||
fun `empty plugins directory`() {
|
|
||||||
val baseDirectory = baseDirectory(ALICE.name)
|
|
||||||
(baseDirectory / "plugins").createDirectories()
|
|
||||||
startNode(ALICE.name).getOrThrow()
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,24 +4,18 @@ import co.paralleluniverse.fibers.Suspendable
|
|||||||
import net.corda.contracts.CommercialPaper
|
import net.corda.contracts.CommercialPaper
|
||||||
import net.corda.contracts.asset.*
|
import net.corda.contracts.asset.*
|
||||||
import net.corda.contracts.testing.fillWithSomeTestCash
|
import net.corda.contracts.testing.fillWithSomeTestCash
|
||||||
|
import net.corda.core.*
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.DigitalSignature
|
import net.corda.core.crypto.DigitalSignature
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.sign
|
import net.corda.core.crypto.sign
|
||||||
import net.corda.core.days
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.flows.FlowStateMachine
|
|
||||||
import net.corda.core.flows.InitiatingFlow
|
|
||||||
import net.corda.core.flows.StateMachineRunId
|
|
||||||
import net.corda.core.getOrThrow
|
|
||||||
import net.corda.core.identity.AnonymousParty
|
import net.corda.core.identity.AnonymousParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.identity.AbstractParty
|
|
||||||
import net.corda.core.map
|
|
||||||
import net.corda.core.messaging.SingleMessageRecipient
|
import net.corda.core.messaging.SingleMessageRecipient
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.services.*
|
import net.corda.core.node.services.*
|
||||||
import net.corda.core.rootCause
|
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
@ -86,9 +80,10 @@ class TwoPartyTradeFlowTests {
|
|||||||
net = MockNetwork(false, true)
|
net = MockNetwork(false, true)
|
||||||
|
|
||||||
ledger {
|
ledger {
|
||||||
val notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name)
|
val basketOfNodes = net.createSomeNodes(2)
|
||||||
val aliceNode = net.createPartyNode(notaryNode.info.address, ALICE.name)
|
val notaryNode = basketOfNodes.notaryNode
|
||||||
val bobNode = net.createPartyNode(notaryNode.info.address, BOB.name)
|
val aliceNode = basketOfNodes.partyNodes[0]
|
||||||
|
val bobNode = basketOfNodes.partyNodes[1]
|
||||||
|
|
||||||
aliceNode.disableDBCloseOnStop()
|
aliceNode.disableDBCloseOnStop()
|
||||||
bobNode.disableDBCloseOnStop()
|
bobNode.disableDBCloseOnStop()
|
||||||
@ -137,8 +132,7 @@ class TwoPartyTradeFlowTests {
|
|||||||
aliceNode.disableDBCloseOnStop()
|
aliceNode.disableDBCloseOnStop()
|
||||||
bobNode.disableDBCloseOnStop()
|
bobNode.disableDBCloseOnStop()
|
||||||
|
|
||||||
val cashStates =
|
val cashStates = bobNode.database.transaction {
|
||||||
bobNode.database.transaction {
|
|
||||||
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3)
|
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,7 +233,7 @@ class TwoPartyTradeFlowTests {
|
|||||||
}, true, BOB.name)
|
}, true, BOB.name)
|
||||||
|
|
||||||
// Find the future representing the result of this state machine again.
|
// Find the future representing the result of this state machine again.
|
||||||
val bobFuture = bobNode.smm.findStateMachines(Buyer::class.java).single().second
|
val bobFuture = bobNode.smm.findStateMachines(BuyerAcceptor::class.java).single().second
|
||||||
|
|
||||||
// And off we go again.
|
// And off we go again.
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
@ -489,25 +483,42 @@ class TwoPartyTradeFlowTests {
|
|||||||
sellerNode: MockNetwork.MockNode,
|
sellerNode: MockNetwork.MockNode,
|
||||||
buyerNode: MockNetwork.MockNode,
|
buyerNode: MockNetwork.MockNode,
|
||||||
assetToSell: StateAndRef<OwnableState>): RunResult {
|
assetToSell: StateAndRef<OwnableState>): RunResult {
|
||||||
|
sellerNode.services.identityService.registerIdentity(buyerNode.info.legalIdentity)
|
||||||
|
buyerNode.services.identityService.registerIdentity(sellerNode.info.legalIdentity)
|
||||||
|
val buyerFlows: Observable<BuyerAcceptor> = buyerNode.registerInitiatedFlow(BuyerAcceptor::class.java)
|
||||||
|
val firstBuyerFiber = buyerFlows.toFuture().map { it.stateMachine }
|
||||||
|
val seller = SellerInitiator(buyerNode.info.legalIdentity, notaryNode.info, assetToSell, 1000.DOLLARS)
|
||||||
|
val sellerResult = sellerNode.services.startFlow(seller).resultFuture
|
||||||
|
return RunResult(firstBuyerFiber, sellerResult, seller.stateMachine.id)
|
||||||
|
}
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
class SellerRunnerFlow(val buyer: Party, val notary: NodeInfo) : FlowLogic<SignedTransaction>() {
|
class SellerInitiator(val buyer: Party,
|
||||||
|
val notary: NodeInfo,
|
||||||
|
val assetToSell: StateAndRef<OwnableState>,
|
||||||
|
val price: Amount<Currency>) : FlowLogic<SignedTransaction>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): SignedTransaction = subFlow(Seller(
|
override fun call(): SignedTransaction {
|
||||||
|
send(buyer, Pair(notary.notaryIdentity, price))
|
||||||
|
return subFlow(Seller(
|
||||||
buyer,
|
buyer,
|
||||||
notary,
|
notary,
|
||||||
assetToSell,
|
assetToSell,
|
||||||
1000.DOLLARS,
|
price,
|
||||||
serviceHub.legalIdentityKey))
|
serviceHub.legalIdentityKey))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sellerNode.services.identityService.registerIdentity(buyerNode.info.legalIdentity)
|
@InitiatedBy(SellerInitiator::class)
|
||||||
buyerNode.services.identityService.registerIdentity(sellerNode.info.legalIdentity)
|
class BuyerAcceptor(val seller: Party) : FlowLogic<SignedTransaction>() {
|
||||||
val buyerFuture = buyerNode.initiateSingleShotFlow(SellerRunnerFlow::class) { otherParty ->
|
@Suspendable
|
||||||
Buyer(otherParty, notaryNode.info.notaryIdentity, 1000.DOLLARS, CommercialPaper.State::class.java)
|
override fun call(): SignedTransaction {
|
||||||
}.map { it.stateMachine }
|
val (notary, price) = receive<Pair<Party, Amount<Currency>>>(seller).unwrap {
|
||||||
val seller = SellerRunnerFlow(buyerNode.info.legalIdentity, notaryNode.info)
|
require(serviceHub.networkMapCache.isNotary(it.first)) { "${it.first} is not a notary" }
|
||||||
val sellerResultFuture = sellerNode.services.startFlow(seller).resultFuture
|
it
|
||||||
return RunResult(buyerFuture, sellerResultFuture, seller.stateMachine.id)
|
}
|
||||||
|
return subFlow(Buyer(seller, notary, price, CommercialPaper.State::class.java))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.runWithError(
|
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.runWithError(
|
||||||
|
@ -3,11 +3,11 @@ package net.corda.node.services
|
|||||||
import com.codahale.metrics.MetricRegistry
|
import com.codahale.metrics.MetricRegistry
|
||||||
import net.corda.core.flows.FlowInitiator
|
import net.corda.core.flows.FlowInitiator
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.services.*
|
import net.corda.core.node.services.*
|
||||||
|
import net.corda.core.serialization.SerializeAsToken
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.node.internal.ServiceFlowInfo
|
import net.corda.node.internal.InitiatedFlowFactory
|
||||||
import net.corda.node.serialization.NodeClock
|
import net.corda.node.serialization.NodeClock
|
||||||
import net.corda.node.services.api.*
|
import net.corda.node.services.api.*
|
||||||
import net.corda.node.services.messaging.MessagingService
|
import net.corda.node.services.messaging.MessagingService
|
||||||
@ -67,11 +67,11 @@ open class MockServiceHubInternal(
|
|||||||
|
|
||||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) = recordTransactionsInternal(txStorageService, txs)
|
override fun recordTransactions(txs: Iterable<SignedTransaction>) = recordTransactionsInternal(txStorageService, txs)
|
||||||
|
|
||||||
|
override fun <T : SerializeAsToken> cordaService(type: Class<T>): T = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override fun <T> startFlow(logic: FlowLogic<T>, flowInitiator: FlowInitiator): FlowStateMachineImpl<T> {
|
override fun <T> startFlow(logic: FlowLogic<T>, flowInitiator: FlowInitiator): FlowStateMachineImpl<T> {
|
||||||
return smm.executor.fetchFrom { smm.add(logic, flowInitiator) }
|
return smm.executor.fetchFrom { smm.add(logic, flowInitiator) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun registerServiceFlow(initiatingFlowClass: Class<out FlowLogic<*>>, serviceFlowFactory: (Party) -> FlowLogic<*>) = Unit
|
override fun getFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>? = null
|
||||||
|
|
||||||
override fun getServiceFlowFactory(clientFlowClass: Class<out FlowLogic<*>>): ServiceFlowInfo? = null
|
|
||||||
}
|
}
|
||||||
|
@ -92,13 +92,13 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
|||||||
}
|
}
|
||||||
scheduler = NodeSchedulerService(services, database, schedulerGatedExecutor)
|
scheduler = NodeSchedulerService(services, database, schedulerGatedExecutor)
|
||||||
smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1)
|
smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1)
|
||||||
val mockSMM = StateMachineManager(services, listOf(services, scheduler), DBCheckpointStorage(), smmExecutor, database)
|
val mockSMM = StateMachineManager(services, DBCheckpointStorage(), smmExecutor, database)
|
||||||
mockSMM.changes.subscribe { change ->
|
mockSMM.changes.subscribe { change ->
|
||||||
if (change is StateMachineManager.Change.Removed && mockSMM.allStateMachines.isEmpty()) {
|
if (change is StateMachineManager.Change.Removed && mockSMM.allStateMachines.isEmpty()) {
|
||||||
smmHasRemovedAllFlows.countDown()
|
smmHasRemovedAllFlows.countDown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mockSMM.start()
|
mockSMM.start(listOf(services, scheduler))
|
||||||
services.smm = mockSMM
|
services.smm = mockSMM
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,10 @@ import net.corda.core.contracts.Amount
|
|||||||
import net.corda.core.contracts.Issued
|
import net.corda.core.contracts.Issued
|
||||||
import net.corda.core.contracts.TransactionType
|
import net.corda.core.contracts.TransactionType
|
||||||
import net.corda.core.contracts.USD
|
import net.corda.core.contracts.USD
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.InitiatedBy
|
||||||
import net.corda.core.flows.InitiatingFlow
|
import net.corda.core.flows.InitiatingFlow
|
||||||
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.services.unconsumedStates
|
import net.corda.core.node.services.unconsumedStates
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.DUMMY_NOTARY
|
import net.corda.core.utilities.DUMMY_NOTARY
|
||||||
@ -86,16 +87,20 @@ class DataVendingServiceTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun MockNode.sendNotifyTx(tx: SignedTransaction, walletServiceNode: MockNode) {
|
private fun MockNode.sendNotifyTx(tx: SignedTransaction, walletServiceNode: MockNode) {
|
||||||
walletServiceNode.registerServiceFlow(clientFlowClass = NotifyTxFlow::class, serviceFlowFactory = ::NotifyTransactionHandler)
|
walletServiceNode.registerInitiatedFlow(InitiateNotifyTxFlow::class.java)
|
||||||
services.startFlow(NotifyTxFlow(walletServiceNode.info.legalIdentity, tx))
|
services.startFlow(NotifyTxFlow(walletServiceNode.info.legalIdentity, tx))
|
||||||
network.runNetwork()
|
network.runNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
private class NotifyTxFlow(val otherParty: Party, val stx: SignedTransaction) : FlowLogic<Unit>() {
|
private class NotifyTxFlow(val otherParty: Party, val stx: SignedTransaction) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() = send(otherParty, NotifyTxRequest(stx))
|
override fun call() = send(otherParty, NotifyTxRequest(stx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(NotifyTxFlow::class)
|
||||||
|
private class InitiateNotifyTxFlow(val otherParty: Party) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() = subFlow(NotifyTransactionHandler(otherParty))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,13 @@ import net.corda.contracts.asset.Cash
|
|||||||
import net.corda.core.*
|
import net.corda.core.*
|
||||||
import net.corda.core.contracts.DOLLARS
|
import net.corda.core.contracts.DOLLARS
|
||||||
import net.corda.core.contracts.DummyState
|
import net.corda.core.contracts.DummyState
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.generateKeyPair
|
import net.corda.core.crypto.generateKeyPair
|
||||||
import net.corda.core.flows.FlowException
|
import net.corda.core.flows.FlowException
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.flows.FlowSessionException
|
import net.corda.core.flows.FlowSessionException
|
||||||
import net.corda.core.flows.InitiatingFlow
|
import net.corda.core.flows.InitiatingFlow
|
||||||
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.messaging.MessageRecipients
|
import net.corda.core.messaging.MessageRecipients
|
||||||
import net.corda.core.node.services.PartyInfo
|
import net.corda.core.node.services.PartyInfo
|
||||||
import net.corda.core.node.services.ServiceInfo
|
import net.corda.core.node.services.ServiceInfo
|
||||||
@ -30,15 +30,19 @@ import net.corda.flows.CashIssueFlow
|
|||||||
import net.corda.flows.CashPaymentFlow
|
import net.corda.flows.CashPaymentFlow
|
||||||
import net.corda.flows.FinalityFlow
|
import net.corda.flows.FinalityFlow
|
||||||
import net.corda.flows.NotaryFlow
|
import net.corda.flows.NotaryFlow
|
||||||
|
import net.corda.node.internal.InitiatedFlowFactory
|
||||||
import net.corda.node.services.persistence.checkpoints
|
import net.corda.node.services.persistence.checkpoints
|
||||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||||
import net.corda.node.utilities.transaction
|
import net.corda.node.utilities.transaction
|
||||||
import net.corda.testing.*
|
import net.corda.testing.expect
|
||||||
|
import net.corda.testing.expectEvents
|
||||||
|
import net.corda.testing.getTestX509Name
|
||||||
import net.corda.testing.node.InMemoryMessagingNetwork
|
import net.corda.testing.node.InMemoryMessagingNetwork
|
||||||
import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer
|
import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer
|
||||||
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
import net.corda.testing.node.MockNetwork.MockNode
|
import net.corda.testing.node.MockNetwork.MockNode
|
||||||
|
import net.corda.testing.sequence
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
|
import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
|
||||||
@ -110,7 +114,7 @@ class FlowFrameworkTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `exception while fiber suspended`() {
|
fun `exception while fiber suspended`() {
|
||||||
node2.registerServiceFlow(ReceiveFlow::class) { SendFlow("Hello", it) }
|
node2.registerFlowFactory(ReceiveFlow::class) { SendFlow("Hello", it) }
|
||||||
val flow = ReceiveFlow(node2.info.legalIdentity)
|
val flow = ReceiveFlow(node2.info.legalIdentity)
|
||||||
val fiber = node1.services.startFlow(flow) as FlowStateMachineImpl
|
val fiber = node1.services.startFlow(flow) as FlowStateMachineImpl
|
||||||
// Before the flow runs change the suspend action to throw an exception
|
// Before the flow runs change the suspend action to throw an exception
|
||||||
@ -129,7 +133,7 @@ class FlowFrameworkTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `flow restarted just after receiving payload`() {
|
fun `flow restarted just after receiving payload`() {
|
||||||
node2.registerServiceFlow(SendFlow::class) { ReceiveFlow(it).nonTerminating() }
|
node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() }
|
||||||
node1.services.startFlow(SendFlow("Hello", node2.info.legalIdentity))
|
node1.services.startFlow(SendFlow("Hello", node2.info.legalIdentity))
|
||||||
|
|
||||||
// We push through just enough messages to get only the payload sent
|
// We push through just enough messages to get only the payload sent
|
||||||
@ -179,7 +183,7 @@ class FlowFrameworkTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `flow loaded from checkpoint will respond to messages from before start`() {
|
fun `flow loaded from checkpoint will respond to messages from before start`() {
|
||||||
node1.registerServiceFlow(ReceiveFlow::class) { SendFlow("Hello", it) }
|
node1.registerFlowFactory(ReceiveFlow::class) { SendFlow("Hello", it) }
|
||||||
node2.services.startFlow(ReceiveFlow(node1.info.legalIdentity).nonTerminating()) // Prepare checkpointed receive flow
|
node2.services.startFlow(ReceiveFlow(node1.info.legalIdentity).nonTerminating()) // Prepare checkpointed receive flow
|
||||||
// Make sure the add() has finished initial processing.
|
// Make sure the add() has finished initial processing.
|
||||||
node2.smm.executor.flush()
|
node2.smm.executor.flush()
|
||||||
@ -198,7 +202,7 @@ class FlowFrameworkTests {
|
|||||||
net.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ }
|
net.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ }
|
||||||
|
|
||||||
val node3 = net.createNode(node1.info.address)
|
val node3 = net.createNode(node1.info.address)
|
||||||
val secondFlow = node3.initiateSingleShotFlow(PingPongFlow::class) { PingPongFlow(it, payload2) }
|
val secondFlow = node3.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) }
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
|
|
||||||
// Kick off first send and receive
|
// Kick off first send and receive
|
||||||
@ -243,8 +247,8 @@ class FlowFrameworkTests {
|
|||||||
fun `sending to multiple parties`() {
|
fun `sending to multiple parties`() {
|
||||||
val node3 = net.createNode(node1.info.address)
|
val node3 = net.createNode(node1.info.address)
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
node2.registerServiceFlow(SendFlow::class) { ReceiveFlow(it).nonTerminating() }
|
node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() }
|
||||||
node3.registerServiceFlow(SendFlow::class) { ReceiveFlow(it).nonTerminating() }
|
node3.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() }
|
||||||
val payload = "Hello World"
|
val payload = "Hello World"
|
||||||
node1.services.startFlow(SendFlow(payload, node2.info.legalIdentity, node3.info.legalIdentity))
|
node1.services.startFlow(SendFlow(payload, node2.info.legalIdentity, node3.info.legalIdentity))
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
@ -277,8 +281,8 @@ class FlowFrameworkTests {
|
|||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
val node2Payload = "Test 1"
|
val node2Payload = "Test 1"
|
||||||
val node3Payload = "Test 2"
|
val node3Payload = "Test 2"
|
||||||
node2.registerServiceFlow(ReceiveFlow::class) { SendFlow(node2Payload, it) }
|
node2.registerFlowFactory(ReceiveFlow::class) { SendFlow(node2Payload, it) }
|
||||||
node3.registerServiceFlow(ReceiveFlow::class) { SendFlow(node3Payload, it) }
|
node3.registerFlowFactory(ReceiveFlow::class) { SendFlow(node3Payload, it) }
|
||||||
val multiReceiveFlow = ReceiveFlow(node2.info.legalIdentity, node3.info.legalIdentity).nonTerminating()
|
val multiReceiveFlow = ReceiveFlow(node2.info.legalIdentity, node3.info.legalIdentity).nonTerminating()
|
||||||
node1.services.startFlow(multiReceiveFlow)
|
node1.services.startFlow(multiReceiveFlow)
|
||||||
node1.acceptableLiveFiberCountOnStop = 1
|
node1.acceptableLiveFiberCountOnStop = 1
|
||||||
@ -303,7 +307,7 @@ class FlowFrameworkTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `both sides do a send as their first IO request`() {
|
fun `both sides do a send as their first IO request`() {
|
||||||
node2.registerServiceFlow(PingPongFlow::class) { PingPongFlow(it, 20L) }
|
node2.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, 20L) }
|
||||||
node1.services.startFlow(PingPongFlow(node2.info.legalIdentity, 10L))
|
node1.services.startFlow(PingPongFlow(node2.info.legalIdentity, 10L))
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
|
|
||||||
@ -339,7 +343,7 @@ class FlowFrameworkTests {
|
|||||||
sessionTransfers.expectEvents(isStrict = false) {
|
sessionTransfers.expectEvents(isStrict = false) {
|
||||||
sequence(
|
sequence(
|
||||||
// First Pay
|
// First Pay
|
||||||
expect(match = { it.message is SessionInit && it.message.clientFlowClass == NotaryFlow.Client::class.java }) {
|
expect(match = { it.message is SessionInit && it.message.initiatingFlowClass == NotaryFlow.Client::class.java }) {
|
||||||
it.message as SessionInit
|
it.message as SessionInit
|
||||||
assertEquals(node1.id, it.from)
|
assertEquals(node1.id, it.from)
|
||||||
assertEquals(notary1Address, it.to)
|
assertEquals(notary1Address, it.to)
|
||||||
@ -349,7 +353,7 @@ class FlowFrameworkTests {
|
|||||||
assertEquals(notary1.id, it.from)
|
assertEquals(notary1.id, it.from)
|
||||||
},
|
},
|
||||||
// Second pay
|
// Second pay
|
||||||
expect(match = { it.message is SessionInit && it.message.clientFlowClass == NotaryFlow.Client::class.java }) {
|
expect(match = { it.message is SessionInit && it.message.initiatingFlowClass == NotaryFlow.Client::class.java }) {
|
||||||
it.message as SessionInit
|
it.message as SessionInit
|
||||||
assertEquals(node1.id, it.from)
|
assertEquals(node1.id, it.from)
|
||||||
assertEquals(notary1Address, it.to)
|
assertEquals(notary1Address, it.to)
|
||||||
@ -359,7 +363,7 @@ class FlowFrameworkTests {
|
|||||||
assertEquals(notary2.id, it.from)
|
assertEquals(notary2.id, it.from)
|
||||||
},
|
},
|
||||||
// Third pay
|
// Third pay
|
||||||
expect(match = { it.message is SessionInit && it.message.clientFlowClass == NotaryFlow.Client::class.java }) {
|
expect(match = { it.message is SessionInit && it.message.initiatingFlowClass == NotaryFlow.Client::class.java }) {
|
||||||
it.message as SessionInit
|
it.message as SessionInit
|
||||||
assertEquals(node1.id, it.from)
|
assertEquals(node1.id, it.from)
|
||||||
assertEquals(notary1Address, it.to)
|
assertEquals(notary1Address, it.to)
|
||||||
@ -374,7 +378,7 @@ class FlowFrameworkTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `other side ends before doing expected send`() {
|
fun `other side ends before doing expected send`() {
|
||||||
node2.registerServiceFlow(ReceiveFlow::class) { NoOpFlow() }
|
node2.registerFlowFactory(ReceiveFlow::class) { NoOpFlow() }
|
||||||
val resultFuture = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture
|
val resultFuture = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy {
|
assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy {
|
||||||
@ -384,7 +388,7 @@ class FlowFrameworkTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `non-FlowException thrown on other side`() {
|
fun `non-FlowException thrown on other side`() {
|
||||||
val erroringFlowFuture = node2.initiateSingleShotFlow(ReceiveFlow::class) {
|
val erroringFlowFuture = node2.registerFlowFactory(ReceiveFlow::class) {
|
||||||
ExceptionFlow { Exception("evil bug!") }
|
ExceptionFlow { Exception("evil bug!") }
|
||||||
}
|
}
|
||||||
val erroringFlowSteps = erroringFlowFuture.flatMap { it.progressSteps }
|
val erroringFlowSteps = erroringFlowFuture.flatMap { it.progressSteps }
|
||||||
@ -418,7 +422,7 @@ class FlowFrameworkTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `FlowException thrown on other side`() {
|
fun `FlowException thrown on other side`() {
|
||||||
val erroringFlow = node2.initiateSingleShotFlow(ReceiveFlow::class) {
|
val erroringFlow = node2.registerFlowFactory(ReceiveFlow::class) {
|
||||||
ExceptionFlow { MyFlowException("Nothing useful") }
|
ExceptionFlow { MyFlowException("Nothing useful") }
|
||||||
}
|
}
|
||||||
val erroringFlowSteps = erroringFlow.flatMap { it.progressSteps }
|
val erroringFlowSteps = erroringFlow.flatMap { it.progressSteps }
|
||||||
@ -456,8 +460,8 @@ class FlowFrameworkTests {
|
|||||||
val node3 = net.createNode(node1.info.address)
|
val node3 = net.createNode(node1.info.address)
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
|
|
||||||
node3.initiateSingleShotFlow(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } }
|
node3.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } }
|
||||||
node2.initiateSingleShotFlow(ReceiveFlow::class) { ReceiveFlow(node3.info.legalIdentity) }
|
node2.registerFlowFactory(ReceiveFlow::class) { ReceiveFlow(node3.info.legalIdentity) }
|
||||||
val receivingFiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity))
|
val receivingFiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity))
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
assertThatExceptionOfType(MyFlowException::class.java)
|
assertThatExceptionOfType(MyFlowException::class.java)
|
||||||
@ -473,9 +477,9 @@ class FlowFrameworkTests {
|
|||||||
// Node 2 will send its payload and then block waiting for the receive from node 1. Meanwhile node 1 will move
|
// Node 2 will send its payload and then block waiting for the receive from node 1. Meanwhile node 1 will move
|
||||||
// onto node 3 which will throw the exception
|
// onto node 3 which will throw the exception
|
||||||
val node2Fiber = node2
|
val node2Fiber = node2
|
||||||
.initiateSingleShotFlow(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") }
|
.registerFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") }
|
||||||
.map { it.stateMachine }
|
.map { it.stateMachine }
|
||||||
node3.initiateSingleShotFlow(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } }
|
node3.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } }
|
||||||
|
|
||||||
val node1Fiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity, node3.info.legalIdentity)) as FlowStateMachineImpl
|
val node1Fiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity, node3.info.legalIdentity)) as FlowStateMachineImpl
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
@ -528,7 +532,7 @@ class FlowFrameworkTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
node2.registerServiceFlow(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") }
|
node2.registerFlowFactory(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") }
|
||||||
val resultFuture = node1.services.startFlow(RetryOnExceptionFlow(node2.info.legalIdentity)).resultFuture
|
val resultFuture = node1.services.startFlow(RetryOnExceptionFlow(node2.info.legalIdentity)).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
assertThat(resultFuture.getOrThrow()).isEqualTo("Hello")
|
assertThat(resultFuture.getOrThrow()).isEqualTo("Hello")
|
||||||
@ -536,7 +540,7 @@ class FlowFrameworkTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `serialisation issue in counterparty`() {
|
fun `serialisation issue in counterparty`() {
|
||||||
node2.registerServiceFlow(ReceiveFlow::class) { SendFlow(NonSerialisableData(1), it) }
|
node2.registerFlowFactory(ReceiveFlow::class) { SendFlow(NonSerialisableData(1), it) }
|
||||||
val result = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture
|
val result = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy {
|
assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy {
|
||||||
@ -546,7 +550,7 @@ class FlowFrameworkTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `FlowException has non-serialisable object`() {
|
fun `FlowException has non-serialisable object`() {
|
||||||
node2.initiateSingleShotFlow(ReceiveFlow::class) {
|
node2.registerFlowFactory(ReceiveFlow::class) {
|
||||||
ExceptionFlow { NonSerialisableFlowException(NonSerialisableData(1)) }
|
ExceptionFlow { NonSerialisableFlowException(NonSerialisableData(1)) }
|
||||||
}
|
}
|
||||||
val result = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture
|
val result = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture
|
||||||
@ -562,9 +566,9 @@ class FlowFrameworkTests {
|
|||||||
ptx.addOutputState(DummyState())
|
ptx.addOutputState(DummyState())
|
||||||
val stx = node1.services.signInitialTransaction(ptx)
|
val stx = node1.services.signInitialTransaction(ptx)
|
||||||
|
|
||||||
val committerFiber = node1
|
val committerFiber = node1.registerFlowFactory(WaitingFlows.Waiter::class) {
|
||||||
.initiateSingleShotFlow(WaitingFlows.Waiter::class) { WaitingFlows.Committer(it) }
|
WaitingFlows.Committer(it)
|
||||||
.map { it.stateMachine }
|
}.map { it.stateMachine }
|
||||||
val waiterStx = node2.services.startFlow(WaitingFlows.Waiter(stx, node1.info.legalIdentity)).resultFuture
|
val waiterStx = node2.services.startFlow(WaitingFlows.Waiter(stx, node1.info.legalIdentity)).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
assertThat(waiterStx.getOrThrow()).isEqualTo(committerFiber.getOrThrow().resultFuture.getOrThrow())
|
assertThat(waiterStx.getOrThrow()).isEqualTo(committerFiber.getOrThrow().resultFuture.getOrThrow())
|
||||||
@ -576,7 +580,7 @@ class FlowFrameworkTests {
|
|||||||
ptx.addOutputState(DummyState())
|
ptx.addOutputState(DummyState())
|
||||||
val stx = node1.services.signInitialTransaction(ptx)
|
val stx = node1.services.signInitialTransaction(ptx)
|
||||||
|
|
||||||
node1.registerServiceFlow(WaitingFlows.Waiter::class) {
|
node1.registerFlowFactory(WaitingFlows.Waiter::class) {
|
||||||
WaitingFlows.Committer(it) { throw Exception("Error") }
|
WaitingFlows.Committer(it) { throw Exception("Error") }
|
||||||
}
|
}
|
||||||
val waiter = node2.services.startFlow(WaitingFlows.Waiter(stx, node1.info.legalIdentity)).resultFuture
|
val waiter = node2.services.startFlow(WaitingFlows.Waiter(stx, node1.info.legalIdentity)).resultFuture
|
||||||
@ -594,13 +598,22 @@ class FlowFrameworkTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `custom client flow`() {
|
fun `customised client flow`() {
|
||||||
val receiveFlowFuture = node2.initiateSingleShotFlow(SendFlow::class) { ReceiveFlow(it) }
|
val receiveFlowFuture = node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it) }
|
||||||
node1.services.startFlow(CustomSendFlow("Hello", node2.info.legalIdentity)).resultFuture
|
node1.services.startFlow(CustomSendFlow("Hello", node2.info.legalIdentity)).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
assertThat(receiveFlowFuture.getOrThrow().receivedPayloads).containsOnly("Hello")
|
assertThat(receiveFlowFuture.getOrThrow().receivedPayloads).containsOnly("Hello")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `customised client flow which has annotated @InitiatingFlow again`() {
|
||||||
|
val result = node1.services.startFlow(IncorrectCustomSendFlow("Hello", node2.info.legalIdentity)).resultFuture
|
||||||
|
net.runNetwork()
|
||||||
|
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
||||||
|
result.getOrThrow()
|
||||||
|
}.withMessageContaining(InitiatingFlow::class.java.simpleName)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `upgraded flow`() {
|
fun `upgraded flow`() {
|
||||||
node1.services.startFlow(UpgradedFlow(node2.info.legalIdentity))
|
node1.services.startFlow(UpgradedFlow(node2.info.legalIdentity))
|
||||||
@ -612,7 +625,11 @@ class FlowFrameworkTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `unsupported new flow version`() {
|
fun `unsupported new flow version`() {
|
||||||
node2.registerServiceFlow(UpgradedFlow::class, flowVersion = 1) { SendFlow("Hello", it) }
|
node2.registerFlowFactory(
|
||||||
|
UpgradedFlow::class.java,
|
||||||
|
InitiatedFlowFactory.CorDapp(version = 1, factory = ::DoubleInlinedSubFlow),
|
||||||
|
DoubleInlinedSubFlow::class.java,
|
||||||
|
track = false)
|
||||||
val result = node1.services.startFlow(UpgradedFlow(node2.info.legalIdentity)).resultFuture
|
val result = node1.services.startFlow(UpgradedFlow(node2.info.legalIdentity)).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy {
|
assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy {
|
||||||
@ -622,7 +639,7 @@ class FlowFrameworkTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `single inlined sub-flow`() {
|
fun `single inlined sub-flow`() {
|
||||||
node2.registerServiceFlow(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) }
|
node2.registerFlowFactory(SendAndReceiveFlow::class, ::SingleInlinedSubFlow)
|
||||||
val result = node1.services.startFlow(SendAndReceiveFlow(node2.info.legalIdentity, "Hello")).resultFuture
|
val result = node1.services.startFlow(SendAndReceiveFlow(node2.info.legalIdentity, "Hello")).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
assertThat(result.getOrThrow()).isEqualTo("HelloHello")
|
assertThat(result.getOrThrow()).isEqualTo("HelloHello")
|
||||||
@ -630,7 +647,7 @@ class FlowFrameworkTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `double inlined sub-flow`() {
|
fun `double inlined sub-flow`() {
|
||||||
node2.registerServiceFlow(SendAndReceiveFlow::class) { DoubleInlinedSubFlow(it) }
|
node2.registerFlowFactory(SendAndReceiveFlow::class, ::DoubleInlinedSubFlow)
|
||||||
val result = node1.services.startFlow(SendAndReceiveFlow(node2.info.legalIdentity, "Hello")).resultFuture
|
val result = node1.services.startFlow(SendAndReceiveFlow(node2.info.legalIdentity, "Hello")).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
assertThat(result.getOrThrow()).isEqualTo("HelloHello")
|
assertThat(result.getOrThrow()).isEqualTo("HelloHello")
|
||||||
@ -654,6 +671,18 @@ class FlowFrameworkTests {
|
|||||||
return smm.findStateMachines(P::class.java).single()
|
return smm.findStateMachines(P::class.java).single()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inline fun <reified P : FlowLogic<*>> MockNode.registerFlowFactory(
|
||||||
|
initiatingFlowClass: KClass<out FlowLogic<*>>,
|
||||||
|
noinline flowFactory: (Party) -> P): ListenableFuture<P>
|
||||||
|
{
|
||||||
|
val observable = registerFlowFactory(initiatingFlowClass.java, object : InitiatedFlowFactory<P> {
|
||||||
|
override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): P {
|
||||||
|
return flowFactory(otherParty)
|
||||||
|
}
|
||||||
|
}, P::class.java, track = true)
|
||||||
|
return observable.toFuture()
|
||||||
|
}
|
||||||
|
|
||||||
private fun sessionInit(clientFlowClass: KClass<out FlowLogic<*>>, flowVersion: Int = 1, payload: Any? = null): SessionInit {
|
private fun sessionInit(clientFlowClass: KClass<out FlowLogic<*>>, flowVersion: Int = 1, payload: Any? = null): SessionInit {
|
||||||
return SessionInit(0, clientFlowClass.java, flowVersion, payload)
|
return SessionInit(0, clientFlowClass.java, flowVersion, payload)
|
||||||
}
|
}
|
||||||
@ -730,8 +759,12 @@ class FlowFrameworkTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private interface CustomInterface
|
private interface CustomInterface
|
||||||
|
|
||||||
private class CustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty)
|
private class CustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty)
|
||||||
|
|
||||||
|
@InitiatingFlow
|
||||||
|
private class IncorrectCustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty)
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
private class ReceiveFlow(vararg val otherParties: Party) : FlowLogic<Unit>() {
|
private class ReceiveFlow(vararg val otherParties: Party) : FlowLogic<Unit>() {
|
||||||
object START_STEP : ProgressTracker.Step("Starting")
|
object START_STEP : ProgressTracker.Step("Starting")
|
||||||
@ -852,7 +885,7 @@ class FlowFrameworkTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private data class NonSerialisableData(val a: Int)
|
private data class NonSerialisableData(val a: Int)
|
||||||
private class NonSerialisableFlowException(val data: NonSerialisableData) : FlowException()
|
private class NonSerialisableFlowException(@Suppress("unused") val data: NonSerialisableData) : FlowException()
|
||||||
|
|
||||||
//endregion Helpers
|
//endregion Helpers
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
package net.corda.bank.plugin
|
package net.corda.bank.plugin
|
||||||
|
|
||||||
import net.corda.bank.api.BankOfCordaWebApi
|
import net.corda.bank.api.BankOfCordaWebApi
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.node.CordaPluginRegistry
|
import net.corda.core.node.CordaPluginRegistry
|
||||||
import net.corda.flows.IssuerFlow
|
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
|
||||||
class BankOfCordaPlugin : CordaPluginRegistry() {
|
class BankOfCordaPlugin : CordaPluginRegistry() {
|
||||||
// A list of classes that expose web APIs.
|
// A list of classes that expose web APIs.
|
||||||
override val webApis = listOf(Function(::BankOfCordaWebApi))
|
override val webApis = listOf(Function(::BankOfCordaWebApi))
|
||||||
override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service))
|
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,11 @@ class IRSDemoTest : IntegrationTestCategory {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `runs IRS demo`() {
|
fun `runs IRS demo`() {
|
||||||
driver(useTestClock = true, isDebug = true) {
|
driver(
|
||||||
|
useTestClock = true,
|
||||||
|
isDebug = true,
|
||||||
|
systemProperties = mapOf("net.corda.node.cordapp.scan.package" to "net.corda.irs"))
|
||||||
|
{
|
||||||
val (controller, nodeA, nodeB) = Futures.allAsList(
|
val (controller, nodeA, nodeB) = Futures.allAsList(
|
||||||
startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.type))),
|
startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.type))),
|
||||||
startNode(DUMMY_BANK_A.name, rpcUsers = listOf(rpcUser)),
|
startNode(DUMMY_BANK_A.name, rpcUsers = listOf(rpcUser)),
|
||||||
@ -83,18 +87,22 @@ class IRSDemoTest : IntegrationTestCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun runTrade(nodeAddr: HostAndPort) {
|
private fun runTrade(nodeAddr: HostAndPort) {
|
||||||
val fileContents = IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream("net/corda/irs/simulation/example-irs-trade.json"), Charsets.UTF_8.name())
|
val fileContents = loadResourceFile("net/corda/irs/simulation/example-irs-trade.json")
|
||||||
val tradeFile = fileContents.replace("tradeXXX", "trade1")
|
val tradeFile = fileContents.replace("tradeXXX", "trade1")
|
||||||
val url = URL("http://$nodeAddr/api/irs/deals")
|
val url = URL("http://$nodeAddr/api/irs/deals")
|
||||||
assertThat(postJson(url, tradeFile)).isTrue()
|
assertThat(postJson(url, tradeFile)).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runUploadRates(host: HostAndPort) {
|
private fun runUploadRates(host: HostAndPort) {
|
||||||
val fileContents = IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream("net/corda/irs/simulation/example.rates.txt"), Charsets.UTF_8.name())
|
val fileContents = loadResourceFile("net/corda/irs/simulation/example.rates.txt")
|
||||||
val url = URL("http://$host/upload/interest-rates")
|
val url = URL("http://$host/upload/interest-rates")
|
||||||
assertThat(uploadFile(url, fileContents)).isTrue()
|
assertThat(uploadFile(url, fileContents)).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadResourceFile(filename: String): String {
|
||||||
|
return IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream(filename), Charsets.UTF_8.name())
|
||||||
|
}
|
||||||
|
|
||||||
private fun getTradeCount(nodeAddr: HostAndPort): Int {
|
private fun getTradeCount(nodeAddr: HostAndPort): Int {
|
||||||
val api = HttpApi.fromHostAndPort(nodeAddr, "api/irs")
|
val api = HttpApi.fromHostAndPort(nodeAddr, "api/irs")
|
||||||
val deals = api.getJson<Array<*>>("deals")
|
val deals = api.getJson<Array<*>>("deals")
|
||||||
|
@ -6,15 +6,15 @@ import net.corda.core.contracts.*
|
|||||||
import net.corda.core.crypto.DigitalSignature
|
import net.corda.core.crypto.DigitalSignature
|
||||||
import net.corda.core.crypto.MerkleTreeException
|
import net.corda.core.crypto.MerkleTreeException
|
||||||
import net.corda.core.crypto.keys
|
import net.corda.core.crypto.keys
|
||||||
import net.corda.core.crypto.sign
|
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.InitiatedBy
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.math.CubicSplineInterpolator
|
import net.corda.core.math.CubicSplineInterpolator
|
||||||
import net.corda.core.math.Interpolator
|
import net.corda.core.math.Interpolator
|
||||||
import net.corda.core.math.InterpolatorFactory
|
import net.corda.core.math.InterpolatorFactory
|
||||||
import net.corda.core.node.CordaPluginRegistry
|
|
||||||
import net.corda.core.node.PluginServiceHub
|
import net.corda.core.node.PluginServiceHub
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
|
import net.corda.core.node.services.CordaService
|
||||||
import net.corda.core.node.services.ServiceType
|
import net.corda.core.node.services.ServiceType
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import net.corda.core.transactions.FilteredTransaction
|
import net.corda.core.transactions.FilteredTransaction
|
||||||
@ -30,14 +30,10 @@ import org.jetbrains.exposed.sql.ResultRow
|
|||||||
import org.jetbrains.exposed.sql.statements.InsertStatement
|
import org.jetbrains.exposed.sql.statements.InsertStatement
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.security.KeyPair
|
|
||||||
import java.time.Clock
|
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Duration
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.function.Function
|
|
||||||
import javax.annotation.concurrent.ThreadSafe
|
import javax.annotation.concurrent.ThreadSafe
|
||||||
import kotlin.collections.component1
|
import kotlin.collections.component1
|
||||||
import kotlin.collections.component2
|
import kotlin.collections.component2
|
||||||
@ -55,83 +51,50 @@ import kotlin.collections.set
|
|||||||
object NodeInterestRates {
|
object NodeInterestRates {
|
||||||
val type = ServiceType.corda.getSubType("interest_rates")
|
val type = ServiceType.corda.getSubType("interest_rates")
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the flow that is used with the Fixing integration tests.
|
|
||||||
*/
|
|
||||||
class Plugin : CordaPluginRegistry() {
|
|
||||||
override val servicePlugins = listOf(Function(::Service))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Service that wraps [Oracle] and handles messages/network interaction/request scrubbing.
|
|
||||||
*/
|
|
||||||
// DOCSTART 2
|
// DOCSTART 2
|
||||||
class Service(val services: PluginServiceHub) : AcceptsFileUpload, SingletonSerializeAsToken() {
|
@InitiatedBy(RatesFixFlow.FixSignFlow::class)
|
||||||
val oracle: Oracle by lazy {
|
class FixSignHandler(val otherParty: Party) : FlowLogic<Unit>() {
|
||||||
val myNodeInfo = services.myInfo
|
|
||||||
val myIdentity = myNodeInfo.serviceIdentities(type).first()
|
|
||||||
val mySigningKey = myIdentity.owningKey.keys.first { services.keyManagementService.keys.contains(it) }
|
|
||||||
Oracle(myIdentity, mySigningKey, services)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Note: access to the singleton oracle property is via the registered SingletonSerializeAsToken Service.
|
|
||||||
// Otherwise the Kryo serialisation of the call stack in the Quasar Fiber extends to include
|
|
||||||
// the framework Oracle and the flow will crash.
|
|
||||||
services.registerServiceFlow(RatesFixFlow.FixSignFlow::class.java) { FixSignHandler(it, this) }
|
|
||||||
services.registerServiceFlow(RatesFixFlow.FixQueryFlow::class.java) { FixQueryHandler(it, this) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FixSignHandler(val otherParty: Party, val service: Service) : FlowLogic<Unit>() {
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
val request = receive<RatesFixFlow.SignRequest>(otherParty).unwrap { it }
|
val request = receive<RatesFixFlow.SignRequest>(otherParty).unwrap { it }
|
||||||
send(otherParty, service.oracle.sign(request.ftx))
|
send(otherParty, serviceHub.cordaService(Oracle::class.java).sign(request.ftx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FixQueryHandler(val otherParty: Party, val service: Service) : FlowLogic<Unit>() {
|
@InitiatedBy(RatesFixFlow.FixQueryFlow::class)
|
||||||
companion object {
|
class FixQueryHandler(val otherParty: Party) : FlowLogic<Unit>() {
|
||||||
object RECEIVED : ProgressTracker.Step("Received fix request")
|
object RECEIVED : ProgressTracker.Step("Received fix request")
|
||||||
object SENDING : ProgressTracker.Step("Sending fix response")
|
object SENDING : ProgressTracker.Step("Sending fix response")
|
||||||
}
|
|
||||||
|
|
||||||
override val progressTracker = ProgressTracker(RECEIVED, SENDING)
|
override val progressTracker = ProgressTracker(RECEIVED, SENDING)
|
||||||
|
|
||||||
init {
|
|
||||||
progressTracker.currentStep = RECEIVED
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): Unit {
|
override fun call(): Unit {
|
||||||
val request = receive<RatesFixFlow.QueryRequest>(otherParty).unwrap { it }
|
val request = receive<RatesFixFlow.QueryRequest>(otherParty).unwrap { it }
|
||||||
val answers = service.oracle.query(request.queries, request.deadline)
|
progressTracker.currentStep = RECEIVED
|
||||||
|
val answers = serviceHub.cordaService(Oracle::class.java).query(request.queries, request.deadline)
|
||||||
progressTracker.currentStep = SENDING
|
progressTracker.currentStep = SENDING
|
||||||
send(otherParty, answers)
|
send(otherParty, answers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// DOCEND 2
|
// DOCEND 2
|
||||||
|
|
||||||
// File upload support
|
|
||||||
override val dataTypePrefix = "interest-rates"
|
|
||||||
override val acceptableFileExtensions = listOf(".rates", ".txt")
|
|
||||||
|
|
||||||
override fun upload(file: InputStream): String {
|
|
||||||
val fixes = parseFile(file.bufferedReader().readText())
|
|
||||||
oracle.knownFixes = fixes
|
|
||||||
val msg = "Interest rates oracle accepted ${fixes.size} new interest rate fixes"
|
|
||||||
println(msg)
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of an interest rate fix oracle which is given data in a simple string format.
|
* An implementation of an interest rate fix oracle which is given data in a simple string format.
|
||||||
*
|
*
|
||||||
* The oracle will try to interpolate the missing value of a tenor for the given fix name and date.
|
* The oracle will try to interpolate the missing value of a tenor for the given fix name and date.
|
||||||
*/
|
*/
|
||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
class Oracle(val identity: Party, private val signingKey: PublicKey, val services: ServiceHub) {
|
// DOCSTART 3
|
||||||
|
@CordaService
|
||||||
|
class Oracle(val identity: Party, private val signingKey: PublicKey, val services: ServiceHub) : AcceptsFileUpload, SingletonSerializeAsToken() {
|
||||||
|
constructor(services: PluginServiceHub) : this(
|
||||||
|
services.myInfo.serviceIdentities(type).first(),
|
||||||
|
services.myInfo.serviceIdentities(type).first().owningKey.keys.first { services.keyManagementService.keys.contains(it) },
|
||||||
|
services
|
||||||
|
)
|
||||||
|
// DOCEND 3
|
||||||
|
|
||||||
private object Table : JDBCHashedTable("demo_interest_rate_fixes") {
|
private object Table : JDBCHashedTable("demo_interest_rate_fixes") {
|
||||||
val name = varchar("index_name", length = 255)
|
val name = varchar("index_name", length = 255)
|
||||||
val forDay = localDate("for_day")
|
val forDay = localDate("for_day")
|
||||||
@ -175,7 +138,7 @@ object NodeInterestRates {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This method will now wait until the given deadline if the fix for the given [FixOf] is not immediately
|
* This method will now wait until the given deadline if the fix for the given [FixOf] is not immediately
|
||||||
* available. To implement this, [readWithDeadline] will loop if the deadline is not reached and we throw
|
* available. To implement this, [FiberBox.readWithDeadline] will loop if the deadline is not reached and we throw
|
||||||
* [UnknownFix] as it implements [RetryableException] which has special meaning to this function.
|
* [UnknownFix] as it implements [RetryableException] which has special meaning to this function.
|
||||||
*/
|
*/
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@ -231,6 +194,16 @@ object NodeInterestRates {
|
|||||||
return DigitalSignature.LegallyIdentifiable(identity, signature.bytes)
|
return DigitalSignature.LegallyIdentifiable(identity, signature.bytes)
|
||||||
}
|
}
|
||||||
// DOCEND 1
|
// DOCEND 1
|
||||||
|
|
||||||
|
// File upload support
|
||||||
|
override val dataTypePrefix = "interest-rates"
|
||||||
|
override val acceptableFileExtensions = listOf(".rates", ".txt")
|
||||||
|
|
||||||
|
override fun upload(file: InputStream): String {
|
||||||
|
val fixes = parseFile(file.bufferedReader().readText())
|
||||||
|
knownFixes = fixes
|
||||||
|
return "Interest rates oracle accepted ${fixes.size} new interest rate fixes"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: can we split into two? Fix not available (retryable/transient) and unknown (permanent)
|
// TODO: can we split into two? Fix not available (retryable/transient) and unknown (permanent)
|
||||||
|
@ -3,19 +3,17 @@ package net.corda.irs.flows
|
|||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.DealState
|
import net.corda.core.contracts.DealState
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.InitiatedBy
|
||||||
import net.corda.core.flows.InitiatingFlow
|
import net.corda.core.flows.InitiatingFlow
|
||||||
import net.corda.core.flows.StartableByRPC
|
import net.corda.core.flows.StartableByRPC
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.node.CordaPluginRegistry
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.PluginServiceHub
|
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import net.corda.flows.TwoPartyDealFlow
|
import net.corda.flows.TwoPartyDealFlow
|
||||||
import net.corda.flows.TwoPartyDealFlow.Acceptor
|
import net.corda.flows.TwoPartyDealFlow.Acceptor
|
||||||
import net.corda.flows.TwoPartyDealFlow.AutoOffer
|
import net.corda.flows.TwoPartyDealFlow.AutoOffer
|
||||||
import net.corda.flows.TwoPartyDealFlow.Instigator
|
import net.corda.flows.TwoPartyDealFlow.Instigator
|
||||||
import java.util.function.Function
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This whole class is really part of a demo just to initiate the agreement of a deal with a simple
|
* This whole class is really part of a demo just to initiate the agreement of a deal with a simple
|
||||||
@ -25,18 +23,6 @@ import java.util.function.Function
|
|||||||
* or the flow would have to reach out to external systems (or users) to verify the deals.
|
* or the flow would have to reach out to external systems (or users) to verify the deals.
|
||||||
*/
|
*/
|
||||||
object AutoOfferFlow {
|
object AutoOfferFlow {
|
||||||
|
|
||||||
class Plugin : CordaPluginRegistry() {
|
|
||||||
override val servicePlugins = listOf(Function(::Service))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Service(services: PluginServiceHub) : SingletonSerializeAsToken() {
|
|
||||||
init {
|
|
||||||
services.registerServiceFlow(Requester::class.java) { Acceptor(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class Requester(val dealToBeOffered: DealState) : FlowLogic<SignedTransaction>() {
|
class Requester(val dealToBeOffered: DealState) : FlowLogic<SignedTransaction>() {
|
||||||
@ -81,4 +67,7 @@ object AutoOfferFlow {
|
|||||||
return parties.filter { serviceHub.myInfo.legalIdentity != it }
|
return parties.filter { serviceHub.myInfo.legalIdentity != it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(Requester::class)
|
||||||
|
class AutoOfferAcceptor(otherParty: Party) : Acceptor(otherParty)
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,11 @@ import net.corda.core.TransientProperty
|
|||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.toBase58String
|
import net.corda.core.crypto.toBase58String
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.InitiatedBy
|
||||||
import net.corda.core.flows.InitiatingFlow
|
import net.corda.core.flows.InitiatingFlow
|
||||||
import net.corda.core.flows.SchedulableFlow
|
import net.corda.core.flows.SchedulableFlow
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.PluginServiceHub
|
|
||||||
import net.corda.core.node.services.ServiceType
|
import net.corda.core.node.services.ServiceType
|
||||||
import net.corda.core.seconds
|
import net.corda.core.seconds
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
@ -22,13 +22,6 @@ import java.math.BigDecimal
|
|||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
object FixingFlow {
|
object FixingFlow {
|
||||||
|
|
||||||
class Service(services: PluginServiceHub) {
|
|
||||||
init {
|
|
||||||
services.registerServiceFlow(FixingRoleDecider::class.java) { Fixer(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One side of the fixing flow for an interest rate swap, but could easily be generalised further.
|
* One side of the fixing flow for an interest rate swap, but could easily be generalised further.
|
||||||
*
|
*
|
||||||
@ -36,8 +29,9 @@ object FixingFlow {
|
|||||||
* of the flow that is run by the party with the fixed leg of swap deal, which is the basis for deciding
|
* of the flow that is run by the party with the fixed leg of swap deal, which is the basis for deciding
|
||||||
* who does what in the flow.
|
* who does what in the flow.
|
||||||
*/
|
*/
|
||||||
class Fixer(override val otherParty: Party,
|
@InitiatedBy(FixingRoleDecider::class)
|
||||||
override val progressTracker: ProgressTracker = TwoPartyDealFlow.Secondary.tracker()) : TwoPartyDealFlow.Secondary<FixingSession>() {
|
class Fixer(override val otherParty: Party) : TwoPartyDealFlow.Secondary<FixingSession>() {
|
||||||
|
override val progressTracker: ProgressTracker = TwoPartyDealFlow.Secondary.tracker()
|
||||||
|
|
||||||
private lateinit var txState: TransactionState<*>
|
private lateinit var txState: TransactionState<*>
|
||||||
private lateinit var deal: FixableDealState
|
private lateinit var deal: FixableDealState
|
||||||
|
@ -2,19 +2,17 @@ package net.corda.irs.flows
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.InitiatedBy
|
||||||
import net.corda.core.flows.InitiatingFlow
|
import net.corda.core.flows.InitiatingFlow
|
||||||
import net.corda.core.flows.StartableByRPC
|
import net.corda.core.flows.StartableByRPC
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.CordaPluginRegistry
|
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.PluginServiceHub
|
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
import net.corda.node.utilities.TestClock
|
import net.corda.node.utilities.TestClock
|
||||||
import net.corda.testing.node.MockNetworkMapCache
|
import net.corda.testing.node.MockNetworkMapCache
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.util.function.Function
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a less temporary, demo-oriented way of initiating processing of temporal events.
|
* This is a less temporary, demo-oriented way of initiating processing of temporal events.
|
||||||
@ -26,16 +24,7 @@ object UpdateBusinessDayFlow {
|
|||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
data class UpdateBusinessDayMessage(val date: LocalDate)
|
data class UpdateBusinessDayMessage(val date: LocalDate)
|
||||||
|
|
||||||
class Plugin : CordaPluginRegistry() {
|
@InitiatedBy(Broadcast::class)
|
||||||
override val servicePlugins = listOf(Function(::Service))
|
|
||||||
}
|
|
||||||
|
|
||||||
class Service(services: PluginServiceHub) {
|
|
||||||
init {
|
|
||||||
services.registerServiceFlow(Broadcast::class.java, ::UpdateBusinessDayHandler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class UpdateBusinessDayHandler(val otherParty: Party) : FlowLogic<Unit>() {
|
private class UpdateBusinessDayHandler(val otherParty: Party) : FlowLogic<Unit>() {
|
||||||
override fun call() {
|
override fun call() {
|
||||||
val message = receive<UpdateBusinessDayMessage>(otherParty).unwrap { it }
|
val message = receive<UpdateBusinessDayMessage>(otherParty).unwrap { it }
|
||||||
|
@ -2,7 +2,6 @@ package net.corda.irs.plugin
|
|||||||
|
|
||||||
import net.corda.core.node.CordaPluginRegistry
|
import net.corda.core.node.CordaPluginRegistry
|
||||||
import net.corda.irs.api.InterestRateSwapAPI
|
import net.corda.irs.api.InterestRateSwapAPI
|
||||||
import net.corda.irs.flows.FixingFlow
|
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
|
||||||
class IRSPlugin : CordaPluginRegistry() {
|
class IRSPlugin : CordaPluginRegistry() {
|
||||||
@ -10,5 +9,4 @@ class IRSPlugin : CordaPluginRegistry() {
|
|||||||
override val staticServeDirs: Map<String, String> = mapOf(
|
override val staticServeDirs: Map<String, String> = mapOf(
|
||||||
"irsdemo" to javaClass.classLoader.getResource("irsweb").toExternalForm()
|
"irsdemo" to javaClass.classLoader.getResource("irsweb").toExternalForm()
|
||||||
)
|
)
|
||||||
override val servicePlugins = listOf(Function(FixingFlow::Service))
|
|
||||||
}
|
}
|
||||||
|
@ -7,27 +7,26 @@ import com.google.common.util.concurrent.FutureCallback
|
|||||||
import com.google.common.util.concurrent.Futures
|
import com.google.common.util.concurrent.Futures
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import com.google.common.util.concurrent.SettableFuture
|
import com.google.common.util.concurrent.SettableFuture
|
||||||
import net.corda.core.RunOnCallerThread
|
import net.corda.core.*
|
||||||
import net.corda.core.contracts.StateAndRef
|
import net.corda.core.contracts.StateAndRef
|
||||||
import net.corda.core.contracts.UniqueIdentifier
|
import net.corda.core.contracts.UniqueIdentifier
|
||||||
import net.corda.core.flatMap
|
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.flows.FlowStateMachine
|
import net.corda.core.flows.FlowStateMachine
|
||||||
|
import net.corda.core.flows.InitiatedBy
|
||||||
import net.corda.core.flows.InitiatingFlow
|
import net.corda.core.flows.InitiatingFlow
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.map
|
|
||||||
import net.corda.core.node.services.linearHeadsOfType
|
import net.corda.core.node.services.linearHeadsOfType
|
||||||
import net.corda.core.success
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.flows.TwoPartyDealFlow.Acceptor
|
import net.corda.flows.TwoPartyDealFlow.Acceptor
|
||||||
import net.corda.flows.TwoPartyDealFlow.AutoOffer
|
import net.corda.flows.TwoPartyDealFlow.AutoOffer
|
||||||
import net.corda.flows.TwoPartyDealFlow.Instigator
|
import net.corda.flows.TwoPartyDealFlow.Instigator
|
||||||
import net.corda.irs.contract.InterestRateSwap
|
import net.corda.irs.contract.InterestRateSwap
|
||||||
|
import net.corda.irs.flows.FixingFlow
|
||||||
import net.corda.jackson.JacksonSupport
|
import net.corda.jackson.JacksonSupport
|
||||||
import net.corda.node.services.identity.InMemoryIdentityService
|
import net.corda.node.services.identity.InMemoryIdentityService
|
||||||
import net.corda.node.utilities.transaction
|
import net.corda.node.utilities.transaction
|
||||||
import net.corda.testing.initiateSingleShotFlow
|
|
||||||
import net.corda.testing.node.InMemoryMessagingNetwork
|
import net.corda.testing.node.InMemoryMessagingNetwork
|
||||||
|
import rx.Observable
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -126,6 +125,9 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten
|
|||||||
irs.fixedLeg.fixedRatePayer = node1.info.legalIdentity
|
irs.fixedLeg.fixedRatePayer = node1.info.legalIdentity
|
||||||
irs.floatingLeg.floatingRatePayer = node2.info.legalIdentity
|
irs.floatingLeg.floatingRatePayer = node2.info.legalIdentity
|
||||||
|
|
||||||
|
node1.registerInitiatedFlow(FixingFlow.Fixer::class.java)
|
||||||
|
node2.registerInitiatedFlow(FixingFlow.Fixer::class.java)
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
class StartDealFlow(val otherParty: Party,
|
class StartDealFlow(val otherParty: Party,
|
||||||
val payload: AutoOffer,
|
val payload: AutoOffer,
|
||||||
@ -134,8 +136,13 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten
|
|||||||
override fun call(): SignedTransaction = subFlow(Instigator(otherParty, payload, myKey))
|
override fun call(): SignedTransaction = subFlow(Instigator(otherParty, payload, myKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(StartDealFlow::class)
|
||||||
|
class AcceptDealFlow(otherParty: Party) : Acceptor(otherParty)
|
||||||
|
|
||||||
|
val acceptDealFlows: Observable<AcceptDealFlow> = node2.registerInitiatedFlow(AcceptDealFlow::class.java)
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
val acceptorTx = node2.initiateSingleShotFlow(StartDealFlow::class) { Acceptor(it) }.flatMap {
|
val acceptorTxFuture = acceptDealFlows.toFuture().flatMap {
|
||||||
(it.stateMachine as FlowStateMachine<SignedTransaction>).resultFuture
|
(it.stateMachine as FlowStateMachine<SignedTransaction>).resultFuture
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,9 +153,9 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten
|
|||||||
node2.info.legalIdentity,
|
node2.info.legalIdentity,
|
||||||
AutoOffer(notary.info.notaryIdentity, irs),
|
AutoOffer(notary.info.notaryIdentity, irs),
|
||||||
node1.services.legalIdentityKey)
|
node1.services.legalIdentityKey)
|
||||||
val instigatorTx = node1.services.startFlow(instigator).resultFuture
|
val instigatorTxFuture = node1.services.startFlow(instigator).resultFuture
|
||||||
|
|
||||||
return Futures.allAsList(instigatorTx, acceptorTx).flatMap { instigatorTx }
|
return Futures.allAsList(instigatorTxFuture, acceptorTxFuture).flatMap { instigatorTxFuture }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun iterate(): InMemoryMessagingNetwork.MessageTransfer? {
|
override fun iterate(): InMemoryMessagingNetwork.MessageTransfer? {
|
||||||
|
@ -126,9 +126,11 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean,
|
|||||||
return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) {
|
return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) {
|
||||||
override fun start(): MockNetwork.MockNode {
|
override fun start(): MockNetwork.MockNode {
|
||||||
super.start()
|
super.start()
|
||||||
|
registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java)
|
||||||
|
registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java)
|
||||||
javaClass.classLoader.getResourceAsStream("net/corda/irs/simulation/example.rates.txt").use {
|
javaClass.classLoader.getResourceAsStream("net/corda/irs/simulation/example.rates.txt").use {
|
||||||
database.transaction {
|
database.transaction {
|
||||||
findService<NodeInterestRates.Service>().upload(it)
|
installCordaService(NodeInterestRates.Oracle::class.java).upload(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
|
@ -1,5 +1,2 @@
|
|||||||
# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry
|
# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry
|
||||||
net.corda.irs.plugin.IRSPlugin
|
net.corda.irs.plugin.IRSPlugin
|
||||||
net.corda.irs.api.NodeInterestRates$Plugin
|
|
||||||
net.corda.irs.flows.AutoOfferFlow$Plugin
|
|
||||||
net.corda.irs.flows.UpdateBusinessDayFlow$Plugin
|
|
||||||
|
@ -210,8 +210,10 @@ class NodeInterestRatesTest {
|
|||||||
val net = MockNetwork()
|
val net = MockNetwork()
|
||||||
val n1 = net.createNotaryNode()
|
val n1 = net.createNotaryNode()
|
||||||
val n2 = net.createNode(n1.info.address, advertisedServices = ServiceInfo(NodeInterestRates.type))
|
val n2 = net.createNode(n1.info.address, advertisedServices = ServiceInfo(NodeInterestRates.type))
|
||||||
|
n2.registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java)
|
||||||
|
n2.registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java)
|
||||||
n2.database.transaction {
|
n2.database.transaction {
|
||||||
n2.findService<NodeInterestRates.Service>().oracle.knownFixes = TEST_DATA
|
n2.installCordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA
|
||||||
}
|
}
|
||||||
val tx = TransactionType.General.Builder(null)
|
val tx = TransactionType.General.Builder(null)
|
||||||
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||||
@ -233,7 +235,8 @@ class NodeInterestRatesTest {
|
|||||||
fixOf: FixOf,
|
fixOf: FixOf,
|
||||||
expectedRate: BigDecimal,
|
expectedRate: BigDecimal,
|
||||||
rateTolerance: BigDecimal,
|
rateTolerance: BigDecimal,
|
||||||
progressTracker: ProgressTracker = RatesFixFlow.tracker(fixOf.name)) : RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) {
|
progressTracker: ProgressTracker = RatesFixFlow.tracker(fixOf.name))
|
||||||
|
: RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) {
|
||||||
override fun filtering(elem: Any): Boolean {
|
override fun filtering(elem: Any): Boolean {
|
||||||
return when (elem) {
|
return when (elem) {
|
||||||
is Command -> oracle.owningKey in elem.signers && elem.value is Fix
|
is Command -> oracle.owningKey in elem.signers && elem.value is Fix
|
||||||
@ -242,5 +245,6 @@ class NodeInterestRatesTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun makeTX() = TransactionType.General.Builder(DUMMY_NOTARY).withItems(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE `with notary` DUMMY_NOTARY)
|
private fun makeTX() = TransactionType.General.Builder(DUMMY_NOTARY).withItems(
|
||||||
|
1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE `with notary` DUMMY_NOTARY)
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ class SimmValuationTest : IntegrationTestCategory {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `runs SIMM valuation demo`() {
|
fun `runs SIMM valuation demo`() {
|
||||||
driver(isDebug = true) {
|
driver(isDebug = true, systemProperties = mapOf("net.corda.node.cordapp.scan.package" to "net.corda.vega")) {
|
||||||
startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow()
|
startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow()
|
||||||
val (nodeA, nodeB) = Futures.allAsList(startNode(nodeALegalName), startNode(nodeBLegalName)).getOrThrow()
|
val (nodeA, nodeB) = Futures.allAsList(startNode(nodeALegalName), startNode(nodeBLegalName)).getOrThrow()
|
||||||
val (nodeAApi, nodeBApi) = Futures.allAsList(startWebserver(nodeA), startWebserver(nodeB))
|
val (nodeAApi, nodeBApi) = Futures.allAsList(startWebserver(nodeA), startWebserver(nodeB))
|
||||||
@ -50,8 +50,9 @@ class SimmValuationTest : IntegrationTestCategory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPartyWithName(partyApi: HttpApi, counterparty: X500Name): PortfolioApi.ApiParty =
|
private fun getPartyWithName(partyApi: HttpApi, counterparty: X500Name): PortfolioApi.ApiParty {
|
||||||
getAvailablePartiesFor(partyApi).counterparties.single { it.text == counterparty }
|
return getAvailablePartiesFor(partyApi).counterparties.single { it.text == counterparty }
|
||||||
|
}
|
||||||
|
|
||||||
private fun getAvailablePartiesFor(partyApi: HttpApi): PortfolioApi.AvailableParties {
|
private fun getAvailablePartiesFor(partyApi: HttpApi): PortfolioApi.AvailableParties {
|
||||||
return partyApi.getJson<PortfolioApi.AvailableParties>("whoami")
|
return partyApi.getJson<PortfolioApi.AvailableParties>("whoami")
|
||||||
|
@ -2,10 +2,10 @@ package net.corda.vega.flows
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.InitiatedBy
|
||||||
import net.corda.core.flows.InitiatingFlow
|
import net.corda.core.flows.InitiatingFlow
|
||||||
import net.corda.core.flows.StartableByRPC
|
import net.corda.core.flows.StartableByRPC
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.PluginServiceHub
|
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
@ -15,12 +15,6 @@ import net.corda.vega.contracts.OGTrade
|
|||||||
import net.corda.vega.contracts.SwapData
|
import net.corda.vega.contracts.SwapData
|
||||||
|
|
||||||
object IRSTradeFlow {
|
object IRSTradeFlow {
|
||||||
class Service(services: PluginServiceHub) {
|
|
||||||
init {
|
|
||||||
services.registerServiceFlow(Requester::class.java, ::Receiver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
data class OfferMessage(val notary: Party, val dealBeingOffered: IRSState)
|
data class OfferMessage(val notary: Party, val dealBeingOffered: IRSState)
|
||||||
|
|
||||||
@ -52,6 +46,7 @@ object IRSTradeFlow {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(Requester::class)
|
||||||
class Receiver(private val replyToParty: Party) : FlowLogic<Unit>() {
|
class Receiver(private val replyToParty: Party) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
|
@ -11,6 +11,7 @@ import com.opengamma.strata.pricer.swap.DiscountingSwapProductPricer
|
|||||||
import net.corda.core.contracts.StateAndRef
|
import net.corda.core.contracts.StateAndRef
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.InitiatedBy
|
||||||
import net.corda.core.flows.InitiatingFlow
|
import net.corda.core.flows.InitiatingFlow
|
||||||
import net.corda.core.flows.StartableByRPC
|
import net.corda.core.flows.StartableByRPC
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
@ -180,18 +181,10 @@ object SimmFlow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Service plugin for listening for incoming Simm flow communication
|
|
||||||
*/
|
|
||||||
class Service(services: PluginServiceHub) {
|
|
||||||
init {
|
|
||||||
services.registerServiceFlow(Requester::class.java, ::Receiver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receives and validates a portfolio and comes to consensus over the portfolio initial margin using SIMM.
|
* Receives and validates a portfolio and comes to consensus over the portfolio initial margin using SIMM.
|
||||||
*/
|
*/
|
||||||
|
@InitiatedBy(Requester::class)
|
||||||
class Receiver(val replyToParty: Party) : FlowLogic<Unit>() {
|
class Receiver(val replyToParty: Party) : FlowLogic<Unit>() {
|
||||||
lateinit var ownParty: Party
|
lateinit var ownParty: Party
|
||||||
lateinit var offer: OfferMessage
|
lateinit var offer: OfferMessage
|
||||||
|
@ -15,8 +15,6 @@ import net.corda.core.serialization.SerializationCustomization
|
|||||||
import net.corda.vega.analytics.CordaMarketData
|
import net.corda.vega.analytics.CordaMarketData
|
||||||
import net.corda.vega.analytics.InitialMarginTriple
|
import net.corda.vega.analytics.InitialMarginTriple
|
||||||
import net.corda.vega.api.PortfolioApi
|
import net.corda.vega.api.PortfolioApi
|
||||||
import net.corda.vega.flows.IRSTradeFlow
|
|
||||||
import net.corda.vega.flows.SimmFlow
|
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,7 +26,6 @@ object SimmService {
|
|||||||
class Plugin : CordaPluginRegistry() {
|
class Plugin : CordaPluginRegistry() {
|
||||||
override val webApis = listOf(Function(::PortfolioApi))
|
override val webApis = listOf(Function(::PortfolioApi))
|
||||||
override val staticServeDirs: Map<String, String> = mapOf("simmvaluationdemo" to javaClass.classLoader.getResource("simmvaluationweb").toExternalForm())
|
override val staticServeDirs: Map<String, String> = mapOf("simmvaluationdemo" to javaClass.classLoader.getResource("simmvaluationweb").toExternalForm())
|
||||||
override val servicePlugins = listOf(Function(SimmFlow::Service), Function(IRSTradeFlow::Service))
|
|
||||||
override fun customizeSerialization(custom: SerializationCustomization): Boolean {
|
override fun customizeSerialization(custom: SerializationCustomization): Boolean {
|
||||||
custom.apply {
|
custom.apply {
|
||||||
// OpenGamma classes.
|
// OpenGamma classes.
|
||||||
|
@ -16,6 +16,7 @@ import net.corda.node.services.transactions.SimpleNotaryService
|
|||||||
import net.corda.nodeapi.User
|
import net.corda.nodeapi.User
|
||||||
import net.corda.testing.BOC
|
import net.corda.testing.BOC
|
||||||
import net.corda.testing.node.NodeBasedTest
|
import net.corda.testing.node.NodeBasedTest
|
||||||
|
import net.corda.traderdemo.flow.BuyerFlow
|
||||||
import net.corda.traderdemo.flow.SellerFlow
|
import net.corda.traderdemo.flow.SellerFlow
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -36,6 +37,8 @@ class TraderDemoTest : NodeBasedTest() {
|
|||||||
startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||||
).getOrThrow()
|
).getOrThrow()
|
||||||
|
|
||||||
|
nodeA.registerInitiatedFlow(BuyerFlow::class.java)
|
||||||
|
|
||||||
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map {
|
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map {
|
||||||
val client = CordaRPCClient(it.configuration.rpcAddress!!)
|
val client = CordaRPCClient(it.configuration.rpcAddress!!)
|
||||||
client.start(demoUser[0].username, demoUser[0].password).proxy
|
client.start(demoUser[0].username, demoUser[0].password).proxy
|
||||||
@ -57,12 +60,8 @@ class TraderDemoTest : NodeBasedTest() {
|
|||||||
val executor = Executors.newScheduledThreadPool(1)
|
val executor = Executors.newScheduledThreadPool(1)
|
||||||
poll(executor, "A to be notified of the commercial paper", pollInterval = 100.millis) {
|
poll(executor, "A to be notified of the commercial paper", pollInterval = 100.millis) {
|
||||||
val actualPaper = listOf(clientA.commercialPaperCount, clientB.commercialPaperCount)
|
val actualPaper = listOf(clientA.commercialPaperCount, clientB.commercialPaperCount)
|
||||||
if (actualPaper == expectedPaper) {
|
if (actualPaper == expectedPaper) Unit else null
|
||||||
Unit
|
}.getOrThrow()
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.get()
|
|
||||||
executor.shutdown()
|
executor.shutdown()
|
||||||
assertThat(clientA.dollarCashBalance).isEqualTo(95.DOLLARS)
|
assertThat(clientA.dollarCashBalance).isEqualTo(95.DOLLARS)
|
||||||
assertThat(clientB.dollarCashBalance).isEqualTo(5.DOLLARS)
|
assertThat(clientB.dollarCashBalance).isEqualTo(5.DOLLARS)
|
||||||
|
@ -4,36 +4,24 @@ import co.paralleluniverse.fibers.Suspendable
|
|||||||
import net.corda.contracts.CommercialPaper
|
import net.corda.contracts.CommercialPaper
|
||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.contracts.TransactionGraphSearch
|
import net.corda.core.contracts.TransactionGraphSearch
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.div
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.InitiatedBy
|
||||||
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.PluginServiceHub
|
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.Emoji
|
import net.corda.core.utilities.Emoji
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
import net.corda.flows.TwoPartyTradeFlow
|
import net.corda.flows.TwoPartyTradeFlow
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class BuyerFlow(val otherParty: Party,
|
@InitiatedBy(SellerFlow::class)
|
||||||
private val attachmentsDirectory: String,
|
class BuyerFlow(val otherParty: Party) : FlowLogic<Unit>() {
|
||||||
override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY)) : FlowLogic<Unit>() {
|
|
||||||
|
|
||||||
object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset")
|
object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset")
|
||||||
|
|
||||||
class Service(services: PluginServiceHub) : SingletonSerializeAsToken() {
|
override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY)
|
||||||
init {
|
|
||||||
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
|
|
||||||
// For demo purposes just extract attachment jars when saved to disk, so the user can explore them.
|
|
||||||
val attachmentsPath = (services.storageService.attachments).let {
|
|
||||||
it.automaticallyExtractAttachments = true
|
|
||||||
it.storePath
|
|
||||||
}
|
|
||||||
services.registerServiceFlow(SellerFlow::class.java) { BuyerFlow(it, attachmentsPath.toString()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
@ -72,8 +60,15 @@ class BuyerFlow(val otherParty: Party,
|
|||||||
followInputsOfType = CommercialPaper.State::class.java)
|
followInputsOfType = CommercialPaper.State::class.java)
|
||||||
val cpIssuance = search.call().single()
|
val cpIssuance = search.call().single()
|
||||||
|
|
||||||
|
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
|
||||||
|
// For demo purposes just extract attachment jars when saved to disk, so the user can explore them.
|
||||||
|
val attachmentsPath = (serviceHub.storageService.attachments).let {
|
||||||
|
it.automaticallyExtractAttachments = true
|
||||||
|
it.storePath
|
||||||
|
}
|
||||||
|
|
||||||
cpIssuance.attachments.first().let {
|
cpIssuance.attachments.first().let {
|
||||||
val p = Paths.get(attachmentsDirectory, "$it.jar")
|
val p = attachmentsPath / "$it.jar"
|
||||||
println("""
|
println("""
|
||||||
|
|
||||||
The issuance of the commercial paper came with an attachment. You can find it expanded in this directory:
|
The issuance of the commercial paper came with an attachment. You can find it expanded in this directory:
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
package net.corda.traderdemo.plugin
|
|
||||||
|
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.node.CordaPluginRegistry
|
|
||||||
import net.corda.traderdemo.flow.BuyerFlow
|
|
||||||
import java.util.function.Function
|
|
||||||
|
|
||||||
class TraderDemoPlugin : CordaPluginRegistry() {
|
|
||||||
override val servicePlugins = listOf(Function(BuyerFlow::Service))
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry
|
|
||||||
net.corda.traderdemo.plugin.TraderDemoPlugin
|
|
@ -4,24 +4,21 @@
|
|||||||
package net.corda.testing
|
package net.corda.testing
|
||||||
|
|
||||||
import com.google.common.net.HostAndPort
|
import com.google.common.net.HostAndPort
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.crypto.X509Utilities
|
||||||
|
import net.corda.core.crypto.commonName
|
||||||
|
import net.corda.core.crypto.generateKeyPair
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.VersionInfo
|
import net.corda.core.node.VersionInfo
|
||||||
import net.corda.core.node.services.IdentityService
|
import net.corda.core.node.services.IdentityService
|
||||||
import net.corda.core.serialization.OpaqueBytes
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
import net.corda.core.toFuture
|
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.*
|
import net.corda.core.utilities.*
|
||||||
import net.corda.node.internal.AbstractNode
|
|
||||||
import net.corda.node.internal.NetworkMapInfo
|
import net.corda.node.internal.NetworkMapInfo
|
||||||
import net.corda.node.services.config.*
|
import net.corda.node.services.config.*
|
||||||
import net.corda.node.services.identity.InMemoryIdentityService
|
import net.corda.node.services.identity.InMemoryIdentityService
|
||||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
|
||||||
import net.corda.node.services.statemachine.StateMachineManager
|
|
||||||
import net.corda.nodeapi.User
|
import net.corda.nodeapi.User
|
||||||
import net.corda.nodeapi.config.SSLConfiguration
|
import net.corda.nodeapi.config.SSLConfiguration
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
@ -36,7 +33,6 @@ import java.security.KeyPair
|
|||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JAVA INTEROP
|
* JAVA INTEROP
|
||||||
@ -138,19 +134,6 @@ fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List<HostAndPort> {
|
|||||||
dsl: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail
|
dsl: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail
|
||||||
) = ledger { this.transaction(transactionLabel, transactionBuilder, dsl) }
|
) = ledger { this.transaction(transactionLabel, transactionBuilder, dsl) }
|
||||||
|
|
||||||
/**
|
|
||||||
* The given flow factory will be used to initiate just one instance of a flow of type [P] when a counterparty
|
|
||||||
* flow requests for it using [clientFlowClass].
|
|
||||||
* @return Returns a [ListenableFuture] holding the single [FlowStateMachineImpl] created by the request.
|
|
||||||
*/
|
|
||||||
inline fun <reified P : FlowLogic<*>> AbstractNode.initiateSingleShotFlow(
|
|
||||||
clientFlowClass: KClass<out FlowLogic<*>>,
|
|
||||||
noinline serviceFlowFactory: (Party) -> P): ListenableFuture<P> {
|
|
||||||
val future = smm.changes.filter { it is StateMachineManager.Change.Add && it.logic is P }.map { it.logic as P }.toFuture()
|
|
||||||
services.registerServiceFlow(clientFlowClass.java, serviceFlowFactory)
|
|
||||||
return future
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Replace this with testConfiguration
|
// TODO Replace this with testConfiguration
|
||||||
data class TestNodeConfiguration(
|
data class TestNodeConfiguration(
|
||||||
override val baseDirectory: Path,
|
override val baseDirectory: Path,
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
package net.corda.testing.node
|
package net.corda.testing.node
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting
|
|
||||||
import com.google.common.jimfs.Configuration.unix
|
import com.google.common.jimfs.Configuration.unix
|
||||||
import com.google.common.jimfs.Jimfs
|
import com.google.common.jimfs.Jimfs
|
||||||
import com.google.common.util.concurrent.Futures
|
import com.google.common.util.concurrent.Futures
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import net.corda.core.*
|
import net.corda.core.*
|
||||||
import net.corda.core.crypto.entropyToKeyPair
|
import net.corda.core.crypto.entropyToKeyPair
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.messaging.RPCOps
|
import net.corda.core.messaging.RPCOps
|
||||||
import net.corda.core.messaging.SingleMessageRecipient
|
import net.corda.core.messaging.SingleMessageRecipient
|
||||||
@ -18,14 +16,12 @@ import net.corda.core.node.services.*
|
|||||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import net.corda.node.internal.AbstractNode
|
import net.corda.node.internal.AbstractNode
|
||||||
import net.corda.node.internal.ServiceFlowInfo
|
|
||||||
import net.corda.node.services.config.NodeConfiguration
|
import net.corda.node.services.config.NodeConfiguration
|
||||||
import net.corda.node.services.identity.InMemoryIdentityService
|
import net.corda.node.services.identity.InMemoryIdentityService
|
||||||
import net.corda.node.services.keys.E2ETestKeyManagementService
|
import net.corda.node.services.keys.E2ETestKeyManagementService
|
||||||
import net.corda.node.services.messaging.MessagingService
|
import net.corda.node.services.messaging.MessagingService
|
||||||
import net.corda.node.services.network.InMemoryNetworkMapService
|
import net.corda.node.services.network.InMemoryNetworkMapService
|
||||||
import net.corda.node.services.network.NetworkMapService
|
import net.corda.node.services.network.NetworkMapService
|
||||||
import net.corda.node.services.statemachine.flowVersion
|
|
||||||
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
||||||
import net.corda.node.services.transactions.InMemoryUniquenessProvider
|
import net.corda.node.services.transactions.InMemoryUniquenessProvider
|
||||||
import net.corda.node.services.transactions.SimpleNotaryService
|
import net.corda.node.services.transactions.SimpleNotaryService
|
||||||
@ -45,7 +41,6 @@ import java.security.KeyPair
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A mock node brings up a suite of in-memory services in a fast manner suitable for unit testing.
|
* A mock node brings up a suite of in-memory services in a fast manner suitable for unit testing.
|
||||||
@ -232,13 +227,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false,
|
|||||||
// It is used from the network visualiser tool.
|
// It is used from the network visualiser tool.
|
||||||
@Suppress("unused") val place: PhysicalLocation get() = findMyLocation()!!
|
@Suppress("unused") val place: PhysicalLocation get() = findMyLocation()!!
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
fun registerServiceFlow(clientFlowClass: KClass<out FlowLogic<*>>,
|
|
||||||
flowVersion: Int = clientFlowClass.java.flowVersion,
|
|
||||||
serviceFlowFactory: (Party) -> FlowLogic<*>) {
|
|
||||||
serviceFlowFactories[clientFlowClass.java] = ServiceFlowInfo.CorDapp(flowVersion, serviceFlowFactory)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pumpReceive(block: Boolean = false): InMemoryMessagingNetwork.MessageTransfer? {
|
fun pumpReceive(block: Boolean = false): InMemoryMessagingNetwork.MessageTransfer? {
|
||||||
return (net as InMemoryMessagingNetwork.InMemoryMessaging).pumpReceive(block)
|
return (net as InMemoryMessagingNetwork.InMemoryMessaging).pumpReceive(block)
|
||||||
}
|
}
|
||||||
@ -262,8 +250,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false,
|
|||||||
* Returns a node, optionally created by the passed factory method.
|
* Returns a node, optionally created by the passed factory method.
|
||||||
* @param overrideServices a set of service entries to use in place of the node's default service entries,
|
* @param overrideServices a set of service entries to use in place of the node's default service entries,
|
||||||
* for example where a node's service is part of a cluster.
|
* for example where a node's service is part of a cluster.
|
||||||
* @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value,
|
|
||||||
* but can be overriden to cause nodes to have stable or colliding identity/service keys.
|
|
||||||
*/
|
*/
|
||||||
fun createNode(networkMapAddress: SingleMessageRecipient? = null, forcedID: Int = -1, nodeFactory: Factory = defaultFactory,
|
fun createNode(networkMapAddress: SingleMessageRecipient? = null, forcedID: Int = -1, nodeFactory: Factory = defaultFactory,
|
||||||
start: Boolean = true, legalName: X500Name? = null, overrideServices: Map<ServiceInfo, KeyPair>? = null,
|
start: Boolean = true, legalName: X500Name? = null, overrideServices: Map<ServiceInfo, KeyPair>? = null,
|
||||||
|
@ -8,6 +8,7 @@ import net.corda.core.messaging.SingleMessageRecipient
|
|||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.services.*
|
import net.corda.core.node.services.*
|
||||||
|
import net.corda.core.serialization.SerializeAsToken
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.DUMMY_NOTARY
|
import net.corda.core.utilities.DUMMY_NOTARY
|
||||||
@ -28,6 +29,7 @@ import java.io.ByteArrayInputStream
|
|||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
@ -75,6 +77,8 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub {
|
|||||||
HibernateObserver(vaultService.rawUpdates, NodeSchemaService())
|
HibernateObserver(vaultService.rawUpdates, NodeSchemaService())
|
||||||
return vaultService
|
return vaultService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun <T : SerializeAsToken> cordaService(type: Class<T>): T = throw IllegalArgumentException("${type.name} not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockKeyManagementService(val identityService: IdentityService,
|
class MockKeyManagementService(val identityService: IdentityService,
|
||||||
@ -91,7 +95,9 @@ class MockKeyManagementService(val identityService: IdentityService,
|
|||||||
return k.public
|
return k.public
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun freshKeyAndCert(identity: Party, revocationEnabled: Boolean): Pair<X509CertificateHolder, CertPath> = freshKeyAndCert(this, identityService, identity, revocationEnabled)
|
override fun freshKeyAndCert(identity: Party, revocationEnabled: Boolean): Pair<X509CertificateHolder, CertPath> {
|
||||||
|
return freshKeyAndCert(this, identityService, identity, revocationEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getSigningKeyPair(publicKey: PublicKey): KeyPair {
|
private fun getSigningKeyPair(publicKey: PublicKey): KeyPair {
|
||||||
val pk = publicKey.keys.first { keyStore.containsKey(it) }
|
val pk = publicKey.keys.first { keyStore.containsKey(it) }
|
||||||
@ -108,7 +114,7 @@ class MockKeyManagementService(val identityService: IdentityService,
|
|||||||
class MockAttachmentStorage : AttachmentStorage {
|
class MockAttachmentStorage : AttachmentStorage {
|
||||||
val files = HashMap<SecureHash, ByteArray>()
|
val files = HashMap<SecureHash, ByteArray>()
|
||||||
override var automaticallyExtractAttachments = false
|
override var automaticallyExtractAttachments = false
|
||||||
override var storePath = Paths.get("")
|
override var storePath: Path = Paths.get("")
|
||||||
|
|
||||||
override fun openAttachment(id: SecureHash): Attachment? {
|
override fun openAttachment(id: SecureHash): Attachment? {
|
||||||
val f = files[id] ?: return null
|
val f = files[id] ?: return null
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
package net.corda.explorer.plugin
|
|
||||||
|
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.node.CordaPluginRegistry
|
|
||||||
import net.corda.flows.IssuerFlow
|
|
||||||
import java.util.function.Function
|
|
||||||
|
|
||||||
class ExplorerPlugin : CordaPluginRegistry() {
|
|
||||||
override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service))
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
# Register a ServiceLoader service extending from net.corda.node.CordaPluginRegistry
|
|
||||||
net.corda.explorer.plugin.ExplorerPlugin
|
|
Loading…
Reference in New Issue
Block a user