Introducing StartableByRPC and SchedulableFlow annotations, needed by flows started via RPC and schedulable flows respectively.

CordaPluginRegistry.requiredFlows is no longer needed as a result.
This commit is contained in:
Shams Asari 2017-05-10 11:28:25 +01:00
parent b155764023
commit 48f58b6dbc
52 changed files with 401 additions and 434 deletions

View File

@ -9,11 +9,12 @@ import net.corda.core.serialization.CordaSerializable
* the flow to run at the scheduled time. * the flow to run at the scheduled time.
*/ */
interface FlowLogicRefFactory { interface FlowLogicRefFactory {
fun create(type: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef
} }
@CordaSerializable @CordaSerializable
class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentException("${FlowLogicRef::class.java.simpleName} cannot be constructed for ${FlowLogic::class.java.simpleName} of type ${type.name} $msg") class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentException(
"${FlowLogicRef::class.java.simpleName} cannot be constructed for ${FlowLogic::class.java.simpleName} of type ${type.name} $msg")
/** /**
* A handle interface representing a [FlowLogic] instance which would be possible to safely pass out of the contract sandbox. * A handle interface representing a [FlowLogic] instance which would be possible to safely pass out of the contract sandbox.

View File

@ -0,0 +1,14 @@
package net.corda.core.flows
import java.lang.annotation.Inherited
import kotlin.annotation.AnnotationTarget.CLASS
/**
* Any [FlowLogic] which is schedulable and is designed to be invoked by a [net.corda.core.contracts.SchedulableState]
* must have this annotation. If it's missing [FlowLogicRefFactory.create] will throw an exception when it comes time
* to schedule the next activity in [net.corda.core.contracts.SchedulableState.nextScheduledActivity].
*/
@Target(CLASS)
@Inherited
@MustBeDocumented
annotation class SchedulableFlow

View File

@ -0,0 +1,15 @@
package net.corda.core.flows
import java.lang.annotation.Inherited
import kotlin.annotation.AnnotationTarget.CLASS
/**
* Any [FlowLogic] which is to be started by the RPC interface ([net.corda.core.messaging.CordaRPCOps.startFlowDynamic]
* and [net.corda.core.messaging.CordaRPCOps.startTrackedFlowDynamic]) must have this annotation. If it's missing the
* flow will not be allowed to start and an exception will be thrown.
*/
@Target(CLASS)
@Inherited
@MustBeDocumented
// TODO Consider a different name, something along the lines of SchedulableFlow
annotation class StartableByRPC

View File

@ -148,14 +148,14 @@ interface CordaRPCOps : RPCOps {
fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>> fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>>
/** /**
* Start the given flow with the given arguments. * Start the given flow with the given arguments. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC].
*/ */
@RPCReturnsObservables @RPCReturnsObservables
fun <T : Any> startFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowHandle<T> fun <T : Any> startFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowHandle<T>
/** /**
* Start the given flow with the given arguments, returning an [Observable] with a single observation of the * Start the given flow with the given arguments, returning an [Observable] with a single observation of the
* result of running the flow. * result of running the flow. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC].
*/ */
@RPCReturnsObservables @RPCReturnsObservables
fun <T : Any> startTrackedFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowProgressHandle<T> fun <T : Any> startTrackedFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowProgressHandle<T>

View File

@ -8,28 +8,24 @@ import java.util.function.Function
* Implement this interface on a class advertised in a META-INF/services/net.corda.core.node.CordaPluginRegistry file * Implement this interface on a class advertised in a META-INF/services/net.corda.core.node.CordaPluginRegistry file
* to extend a Corda node with additional application services. * to extend a Corda node with additional application services.
*/ */
abstract class CordaPluginRegistry( abstract class CordaPluginRegistry {
/** /**
* List of lambdas returning JAX-RS objects. They may only depend on the RPC interface, as the webserver should * List of lambdas returning JAX-RS objects. They may only depend on the RPC interface, as the webserver should
* potentially be able to live in a process separate from the node itself. * potentially be able to live in a process separate from the node itself.
*/ */
open val webApis: List<Function<CordaRPCOps, out Any>> = emptyList(), open val webApis: List<Function<CordaRPCOps, out Any>> get() = emptyList()
/** /**
* Map of static serving endpoints to the matching resource directory. All endpoints will be prefixed with "/web" and postfixed with "\*. * Map of static serving endpoints to the matching resource directory. All endpoints will be prefixed with "/web" and postfixed with "\*.
* Resource directories can be either on disk directories (especially when debugging) in the form "a/b/c". Serving from a JAR can * Resource directories can be either on disk directories (especially when debugging) in the form "a/b/c". Serving from a JAR can
* be specified with: javaClass.getResource("<folder-in-jar>").toExternalForm() * be specified with: javaClass.getResource("<folder-in-jar>").toExternalForm()
*/ */
open val staticServeDirs: Map<String, String> = emptyMap(), open val staticServeDirs: Map<String, String> get() = emptyMap()
/** @Suppress("unused")
* A Map with an entry for each consumed Flow used by the webAPIs. @Deprecated("This is no longer needed. Instead annotate any flows that need to be invoked via RPC with " +
* The key of each map entry should contain the FlowLogic<T> class name. "@StartableByRPC and any scheduled flows with @SchedulableFlow", level = DeprecationLevel.ERROR)
* The associated map values are the union of all concrete class names passed to the Flow constructor. open val requiredFlows: Map<String, Set<String>> get() = emptyMap()
* Standard java.lang.* and kotlin.* types do not need to be included explicitly.
* This is used to extend the white listed Flows that can be initiated from the ServiceHub invokeFlowAsync method.
*/
open val requiredFlows: Map<String, Set<String>> = emptyMap(),
/** /**
* List of lambdas constructing additional long lived services to be hosted within the node. * List of lambdas constructing additional long lived services to be hosted within the node.
@ -37,13 +33,13 @@ 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.
*/ */
open val servicePlugins: List<Function<PluginServiceHub, out Any>> = emptyList() open val servicePlugins: List<Function<PluginServiceHub, out Any>> get() = emptyList()
) {
/** /**
* Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized. * Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized.
* *
* For example, if you add a new [ContractState] it needs to be whitelisted. You can do that either by * For example, if you add a new [net.corda.core.contracts.ContractState] it needs to be whitelisted. You can do that
* adding the @CordaSerializable annotation or via this method. * either by adding the [net.corda.core.serialization.CordaSerializable] annotation or via this method.
** **
* @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future. * @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future.
*/ */

View File

@ -2,6 +2,7 @@ package net.corda.flows
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
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
@ -15,6 +16,7 @@ import java.security.PublicKey
* use the new updated state for future transactions. * use the new updated state for future transactions.
*/ */
@InitiatingFlow @InitiatingFlow
@StartableByRPC
class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState>( class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState>(
originalState: StateAndRef<OldState>, originalState: StateAndRef<OldState>,
newContractClass: Class<out UpgradedContract<OldState, NewState>> newContractClass: Class<out UpgradedContract<OldState, NewState>>

View File

@ -1,5 +1,6 @@
package net.corda.core.flows package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash import net.corda.contracts.asset.Cash
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
@ -9,6 +10,7 @@ import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.node.services.unconsumedStates import net.corda.core.node.services.unconsumedStates
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.Emoji import net.corda.core.utilities.Emoji
import net.corda.flows.CashIssueFlow import net.corda.flows.CashIssueFlow
import net.corda.flows.ContractUpgradeFlow import net.corda.flows.ContractUpgradeFlow
@ -27,7 +29,6 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import java.security.PublicKey import java.security.PublicKey
import java.util.* import java.util.*
import java.util.concurrent.ExecutionException
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -69,10 +70,10 @@ class ContractUpgradeFlowTest {
requireNotNull(atx) requireNotNull(atx)
requireNotNull(btx) requireNotNull(btx)
// The request is expected to be rejected because party B haven't authorise the upgrade yet. // The request is expected to be rejected because party B hasn't authorised the upgrade yet.
val rejectedFuture = a.services.startFlow(ContractUpgradeFlow(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture val rejectedFuture = a.services.startFlow(ContractUpgradeFlow(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
assertFailsWith(ExecutionException::class) { rejectedFuture.get() } assertFailsWith(FlowSessionException::class) { rejectedFuture.getOrThrow() }
// Party B authorise the contract state upgrade. // Party B authorise the contract state upgrade.
b.services.vaultService.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java) b.services.vaultService.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)
@ -81,7 +82,7 @@ class ContractUpgradeFlowTest {
val resultFuture = a.services.startFlow(ContractUpgradeFlow(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture val resultFuture = a.services.startFlow(ContractUpgradeFlow(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
val result = resultFuture.get() val result = resultFuture.getOrThrow()
fun check(node: MockNetwork.MockNode) { fun check(node: MockNetwork.MockNode) {
val nodeStx = node.database.transaction { val nodeStx = node.database.transaction {
@ -124,12 +125,12 @@ class ContractUpgradeFlowTest {
.toSignedTransaction() .toSignedTransaction()
val user = rpcTestUser.copy(permissions = setOf( val user = rpcTestUser.copy(permissions = setOf(
startFlowPermission<FinalityFlow>(), startFlowPermission<FinalityInvoker>(),
startFlowPermission<ContractUpgradeFlow<*, *>>() startFlowPermission<ContractUpgradeFlow<*, *>>()
)) ))
val rpcA = startProxy(a, user) val rpcA = startProxy(a, user)
val rpcB = startProxy(b, user) val rpcB = startProxy(b, user)
val handle = rpcA.startFlow(::FinalityFlow, stx, setOf(a.info.legalIdentity, b.info.legalIdentity)) val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(a.info.legalIdentity, b.info.legalIdentity))
mockNet.runNetwork() mockNet.runNetwork()
handle.returnValue.getOrThrow() handle.returnValue.getOrThrow()
@ -143,7 +144,7 @@ class ContractUpgradeFlowTest {
DummyContractV2::class.java).returnValue DummyContractV2::class.java).returnValue
mockNet.runNetwork() mockNet.runNetwork()
assertFailsWith(ExecutionException::class) { rejectedFuture.get() } assertFailsWith(FlowSessionException::class) { rejectedFuture.getOrThrow() }
// Party B authorise the contract state upgrade. // Party B authorise the contract state upgrade.
rpcB.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java) rpcB.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)
@ -154,7 +155,7 @@ class ContractUpgradeFlowTest {
DummyContractV2::class.java).returnValue DummyContractV2::class.java).returnValue
mockNet.runNetwork() mockNet.runNetwork()
val result = resultFuture.get() val result = resultFuture.getOrThrow()
// Check results. // Check results.
listOf(a, b).forEach { listOf(a, b).forEach {
val signedTX = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) } val signedTX = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) }
@ -210,4 +211,11 @@ class ContractUpgradeFlowTest {
// Dummy Cash contract for testing. // Dummy Cash contract for testing.
override val legalContractReference = SecureHash.sha256("") override val legalContractReference = SecureHash.sha256("")
} }
@StartableByRPC
class FinalityInvoker(val transaction: SignedTransaction,
val extraRecipients: Set<Party>) : FlowLogic<List<SignedTransaction>>() {
@Suspendable
override fun call(): List<SignedTransaction> = subFlow(FinalityFlow(transaction, extraRecipients))
}
} }

View File

@ -8,8 +8,11 @@ UNRELEASED
---------- ----------
* API changes: * API changes:
* Initiating flows (i.e. those which initiate flows in a counterparty) are now required to be annotated with * ``CordaPluginRegistry.requiredFlows`` is no longer needed. Instead annotate any flows you wish to start via RPC with
``InitiatingFlow``. ``@StartableByRPC`` and any scheduled flows with ``@SchedulableFlow``.
* Flows which initiate flows in their counterparties (an example of which is the ``NotaryFlow.Client``) are now
required to be annotated with ``@InitiatingFlow``.
* ``PluginServiceHub.registerFlowInitiator`` has been deprecated and replaced by ``registerServiceFlow`` with the * ``PluginServiceHub.registerFlowInitiator`` has been deprecated and replaced by ``registerServiceFlow`` with the
marker Class restricted to ``FlowLogic``. In line with the introduction of ``InitiatingFlow``, it throws an marker Class restricted to ``FlowLogic``. In line with the introduction of ``InitiatingFlow``, it throws an

View File

@ -45,19 +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 ``requiredFlows`` property is used to declare new protocols in c. The ``servicePlugins`` property returns a list of classes which will
the plugin jar. Specifically the property must return a map with a key
naming each exposed top level flow class and a value which is a set
naming every parameter class that will be passed to the flow's
constructor. Standard ``java.lang.*`` and ``kotlin.*`` types do not need
to be included, but all other parameter types, or concrete interface
implementations need declaring. Declaring a specific flow in this map
white lists it for activation by the ``FlowLogicRefFactory``. White
listing is not strictly required for ``subFlows`` used internally, but
is required for any top level flow, or a flow which is invoked through
the scheduler.
d. The ``servicePlugins`` property returns a list of classes which will
be instantiated once during the ``AbstractNode.start`` call. These be instantiated once during the ``AbstractNode.start`` call. These
classes must provide a single argument constructor which will receive a classes must provide a single argument constructor which will receive a
``PluginServiceHub`` reference. They must also extend the abstract class ``PluginServiceHub`` reference. They must also extend the abstract class
@ -90,7 +78,7 @@ extensions to be created, or registered at startup. In particular:
functions inside the node, for instance to initiate workflows when functions inside the node, for instance to initiate workflows when
certain conditions are met. certain conditions are met.
e. The ``customizeSerialization`` function allows classes to be whitelisted 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

View File

@ -12,11 +12,10 @@ 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. Required flows: Specify which flows will be whitelisted for use in your RPC calls. 1. Service plugins: Register your services (see below).
2. Service plugins: Register your services (see below). 2. Web APIs: You may register your own endpoints under /api/ of the bundled web server.
3. 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.
4. 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
5. 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 Services

View File

@ -42,7 +42,8 @@ There are two main steps to implementing scheduled events:
``nextScheduledActivity`` to be implemented which returns an optional ``ScheduledActivity`` instance. ``nextScheduledActivity`` to be implemented which returns an optional ``ScheduledActivity`` instance.
``ScheduledActivity`` captures what ``FlowLogic`` instance each node will run, to perform the activity, and when it ``ScheduledActivity`` captures what ``FlowLogic`` instance each node will run, to perform the activity, and when it
will run is described by a ``java.time.Instant``. Once your state implements this interface and is tracked by the will run is described by a ``java.time.Instant``. Once your state implements this interface and is tracked by the
wallet, it can expect to be queried for the next activity when committed to the wallet. wallet, it can expect to be queried for the next activity when committed to the wallet. The ``FlowLogic`` must be
annotated with ``@SchedulableFlow``.
* If nothing suitable exists, implement a ``FlowLogic`` to be executed by each node as the activity itself. * If nothing suitable exists, implement a ``FlowLogic`` to be executed by each node as the activity itself.
The important thing to remember is that in the current implementation, each node that is party to the transaction The important thing to remember is that in the current implementation, each node that is party to the transaction
will execute the same ``FlowLogic``, so it needs to establish roles in the business process based on the contract will execute the same ``FlowLogic``, so it needs to establish roles in the business process based on the contract
@ -90,10 +91,7 @@ business process and to take on those roles. That ``FlowLogic`` will be handed
rate swap ``State`` in question, as well as a tolerance ``Duration`` of how long to wait after the activity is triggered rate swap ``State`` in question, as well as a tolerance ``Duration`` of how long to wait after the activity is triggered
for the interest rate before indicating an error. for the interest rate before indicating an error.
.. note:: This is a way to create a reference to the FlowLogic class and its constructor parameters to .. note:: This is a way to create a reference to the FlowLogic class and its constructor parameters to instantiate.
instantiate. The reference can be checked against a per-node whitelist of approved and allowable types as
part of our overall security sandboxing.
As previously mentioned, we currently need a small network handler to assist with session setup until the work to As previously mentioned, we currently need a small network handler to assist with session setup until the work to
automate that is complete. See the interest rate swap specific implementation ``FixingSessionInitiationHandler`` which automate that is complete. See the interest rate swap specific implementation ``FixingSessionInitiationHandler`` which

View File

@ -206,9 +206,10 @@ how to register handlers with the messaging system (see ":doc:`messaging`") and
when messages arrive. It provides the send/receive/sendAndReceive calls that let the code request network when messages arrive. It provides the send/receive/sendAndReceive calls that let the code request network
interaction and it will save/restore serialised versions of the fiber at the right times. interaction and it will save/restore serialised versions of the fiber at the right times.
Flows can be invoked in several ways. For instance, they can be triggered by scheduled events, Flows can be invoked in several ways. For instance, they can be triggered by scheduled events (in which case they need to
see ":doc:`event-scheduling`" to learn more about this. Or they can be triggered directly via the Java-level node RPC be annotated with ``@SchedulableFlow``), see ":doc:`event-scheduling`" to learn more about this. They can also be triggered
APIs from your app code. directly via the node's RPC API from your app code (in which case they need to be annotated with `StartableByRPC`). It's
possible for a flow to be of both types.
You request a flow to be invoked by using the ``CordaRPCOps.startFlowDynamic`` method. This takes a You request a flow to be invoked by using the ``CordaRPCOps.startFlowDynamic`` method. This takes a
Java reflection ``Class`` object that describes the flow class to use (in this case, either ``Buyer`` or ``Seller``). Java reflection ``Class`` object that describes the flow class to use (in this case, either ``Buyer`` or ``Seller``).
@ -399,15 +400,35 @@ This code is longer but no more complicated. Here are some things to pay attenti
As you can see, the flow logic is straightforward and does not contain any callbacks or network glue code, despite As you can see, the flow logic is straightforward and does not contain any callbacks or network glue code, despite
the fact that it takes minimal resources and can survive node restarts. the fact that it takes minimal resources and can survive node restarts.
Initiating communication Flow sessions
------------------------ -------------
Now that we have both sides of the deal negotation implemented as flows we need a way to start things off. We do this by Before going any further it will be useful to describe how flows communicate with each other. A node may have many flows
having one side initiate communication and the other respond to it and start their flow. Initiation is typically done using running at the same time, and perhaps communicating with the same counterparty node but for different purposes. Therefore
RPC with the ``startFlowDynamic`` method. The initiating flow has be to annotated with ``InitiatingFlow``. In our example flows need a way to segregate communication channels so that concurrent conversations between flows on the same set of nodes
it doesn't matter which flow is the initiator and which is the initiated, which is why neither ``Buyer`` nor ``Seller`` do not interfere with each other.
are annotated with it. For example, if we choose the seller side as the initiator then we need a seller starter flow that
might look something like this: To achieve this the flow framework initiates a new flow session each time a flow starts communicating with a ``Party``
for the first time. A session is simply a pair of IDs, one for each side, to allow the node to route received messages to
the correct flow. If the other side accepts the session request then subsequent sends and receives to that same ``Party``
will use the same session. A session ends when either flow ends, whether as expected or pre-maturely. If a flow ends
pre-maturely then the other side will be notified of that and they will also end, as the whole point of flows is a known
sequence of message transfers. Flows end pre-maturely due to exceptions, and as described above, if that exception is
``FlowException`` or a sub-type then it will propagate to the other side. Any other exception will not propagate.
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
making the request - each session initiation includes the initiating flow type. The registration is done by a CorDapp
which has made available the particular flow communication, using ``PluginServiceHub.registerServiceFlow``. This method
specifies a flow factory for generating the counter-flow to any given initiating flow. If this registration doesn't exist
then no further communication takes place and the initiating flow ends with an exception. The initiating flow has to be
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
with one side started manually using the ``startFlowDynamic`` RPC and this initiates the counter-flow on the other side.
In this case it doesn't matter which flow is the initiator and which is the initiated, which is why neither ``Buyer`` nor
``Seller`` are annotated with ``InitiatingFlow``. For example, if we choose the seller side as the initiator then we need
to create a simple seller starter flow that has the annotation we need:
.. container:: codeset .. container:: codeset

View File

@ -3,13 +3,13 @@
Client RPC API tutorial Client RPC API tutorial
======================= =======================
In this tutorial we will build a simple command line utility that In this tutorial we will build a simple command line utility that connects to a node, creates some Cash transactions and
connects to a node, creates some Cash transactions and meanwhile dumps meanwhile dumps the transaction graph to the standard output. We will then put some simple visualisation on top. For an
the transaction graph to the standard output. We will then put some explanation on how the RPC works see :doc:`clientrpc`.
simple visualisation on top. For an explanation on how the RPC works
see :doc:`clientrpc`.
We start off by connecting to the node itself. For the purposes of the tutorial we will use the Driver to start up a notary and a node that issues/exits and moves Cash around for herself. To authenticate we will use the certificates of the nodes directly. We start off by connecting to the node itself. For the purposes of the tutorial we will use the Driver to start up a notary
and a node that issues/exits and moves Cash around for herself. To authenticate we will use the certificates of the nodes
directly.
Note how we configure the node to create a user that has permission to start the CashFlow. Note how we configure the node to create a user that has permission to start the CashFlow.
@ -25,14 +25,16 @@ Now we can connect to the node itself using a valid RPC login. We login using th
:start-after: START 2 :start-after: START 2
:end-before: END 2 :end-before: END 2
We start generating transactions in a different thread (``generateTransactions`` to be defined later) using ``proxy``, which exposes the full RPC interface of the node: We start generating transactions in a different thread (``generateTransactions`` to be defined later) using ``proxy``,
which exposes the full RPC interface of the node:
.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt
:language: kotlin :language: kotlin
:start-after: interface CordaRPCOps :start-after: interface CordaRPCOps
:end-before: } :end-before: }
.. warning:: This API is evolving and will continue to grow as new functionality and features added to Corda are made available to RPC clients. .. warning:: This API is evolving and will continue to grow as new functionality and features added to Corda are made
available to RPC clients.
The one we need in order to dump the transaction graph is ``verifiedTransactions``. The type signature tells us that the The one we need in order to dump the transaction graph is ``verifiedTransactions``. The type signature tells us that the
RPC will return a list of transactions and an Observable stream. This is a general pattern, we query some data and the RPC will return a list of transactions and an Observable stream. This is a general pattern, we query some data and the
@ -61,13 +63,19 @@ Now we just need to create the transactions themselves!
:start-after: START 6 :start-after: START 6
:end-before: END 6 :end-before: END 6
We utilise several RPC functions here to query things like the notaries in the node cluster or our own vault. These RPC functions also return ``Observable`` objects so that the node can send us updated values. However, we don't need updates here and so we mark these observables as ``notUsed``. (As a rule, you should always either subscribe to an ``Observable`` or mark it as not used. Failing to do this will leak resources in the node.) We utilise several RPC functions here to query things like the notaries in the node cluster or our own vault. These RPC
functions also return ``Observable`` objects so that the node can send us updated values. However, we don't need updates
here and so we mark these observables as ``notUsed``. (As a rule, you should always either subscribe to an ``Observable``
or mark it as not used. Failing to do this will leak resources in the node.)
Then in a loop we generate randomly either an Issue, a Pay or an Exit transaction. Then in a loop we generate randomly either an Issue, a Pay or an Exit transaction.
The RPC we need to initiate a Cash transaction is ``startFlowDynamic`` which may start an arbitrary flow, given sufficient permissions to do so. We won't use this function directly, but rather a type-safe wrapper around it ``startFlow`` that type-checks the arguments for us. The RPC we need to initiate a Cash transaction is ``startFlowDynamic`` which may start an arbitrary flow, given sufficient
permissions to do so. We won't use this function directly, but rather a type-safe wrapper around it ``startFlow`` that
type-checks the arguments for us.
Finally we have everything in place: we start a couple of nodes, connect to them, and start creating transactions while listening on successfully created ones, which are dumped to the console. We just need to run it!: Finally we have everything in place: we start a couple of nodes, connect to them, and start creating transactions while
listening on successfully created ones, which are dumped to the console. We just need to run it!:
.. code-block:: text .. code-block:: text
@ -106,9 +114,10 @@ RPC credentials associated with a Client must match the permission set configure
This refers to both authentication (username and password) and role-based authorisation (a permissioned set of RPC operations an This refers to both authentication (username and password) and role-based authorisation (a permissioned set of RPC operations an
authenticated user is entitled to run). authenticated user is entitled to run).
.. note:: Permissions are represented as *String's* to allow RPC implementations to add their own permissioning. .. note:: Permissions are represented as *String's* to allow RPC implementations to add their own permissioning. Currently
Currently the only permission type defined is *StartFlow*, which defines a list of whitelisted flows an authenticated use may execute. the only permission type defined is *StartFlow*, which defines a list of whitelisted flows an authenticated use may
An administrator user (or a developer) may also be assigned the ``ALL`` permission, which grants access to any flow. execute. An administrator user (or a developer) may also be assigned the ``ALL`` permission, which grants access to
any flow.
In the instructions above the server node permissions are configured programmatically in the driver code: In the instructions above the server node permissions are configured programmatically in the driver code:
@ -126,7 +135,8 @@ When starting a standalone node using a configuration file we must supply the RP
{ username=user, password=password, permissions=[ StartFlow.net.corda.flows.CashFlow ] } { username=user, password=password, permissions=[ StartFlow.net.corda.flows.CashFlow ] }
] ]
When using the gradle Cordformation plugin to configure and deploy a node you must supply the RPC credentials in a similar manner: When using the gradle Cordformation plugin to configure and deploy a node you must supply the RPC credentials in a similar
manner:
.. code-block:: text .. code-block:: text
@ -148,5 +158,8 @@ You can then deploy and launch the nodes (Notary and Alice) as follows:
./docs/source/example-code/build/install/docs/source/example-code/bin/client-rpc-tutorial Print ./docs/source/example-code/build/install/docs/source/example-code/bin/client-rpc-tutorial Print
./docs/source/example-code/build/install/docs/source/example-code/bin/client-rpc-tutorial Visualise ./docs/source/example-code/build/install/docs/source/example-code/bin/client-rpc-tutorial Visualise
With regards to the start flow RPCs, there is an extra layer of security whereby the flow to be executed has to be
annotated with ``@StartableByRPC``. Flows without this annotation cannot execute using RPC.
See more on security in :doc:`secure-coding-guidelines`, node configuration in :doc:`corda-configuration-file` and See more on security in :doc:`secure-coding-guidelines`, node configuration in :doc:`corda-configuration-file` and
Cordformation in :doc:`creating-a-cordapp` Cordformation in :doc:`creating-a-cordapp`

View File

@ -6,6 +6,7 @@ import net.corda.core.contracts.Amount
import net.corda.core.contracts.TransactionType import net.corda.core.contracts.TransactionType
import net.corda.core.contracts.issuedBy import net.corda.core.contracts.issuedBy
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.flows.StartableByRPC
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
@ -20,6 +21,7 @@ import java.util.*
* @param recipient the party who should own the currency after it is issued. * @param recipient the party who should own the currency after it is issued.
* @param notary the notary to set on the output states. * @param notary the notary to set on the output states.
*/ */
@StartableByRPC
class CashIssueFlow(val amount: Amount<Currency>, class CashIssueFlow(val amount: Amount<Currency>,
val issueRef: OpaqueBytes, val issueRef: OpaqueBytes,
val recipient: Party, val recipient: Party,

View File

@ -6,6 +6,7 @@ import net.corda.core.contracts.InsufficientBalanceException
import net.corda.core.contracts.TransactionType import net.corda.core.contracts.TransactionType
import net.corda.core.crypto.expandedCompositeKeys import net.corda.core.crypto.expandedCompositeKeys
import net.corda.core.crypto.toStringShort import net.corda.core.crypto.toStringShort
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
@ -20,6 +21,7 @@ import java.util.*
* @param recipient the party to pay the currency to. * @param recipient the party to pay the currency to.
* @param issuerConstraint if specified, the payment will be made using only cash issued by the given parties. * @param issuerConstraint if specified, the payment will be made using only cash issued by the given parties.
*/ */
@StartableByRPC
open class CashPaymentFlow( open class CashPaymentFlow(
val amount: Amount<Currency>, val amount: Amount<Currency>,
val recipient: Party, val recipient: Party,

View File

@ -6,6 +6,7 @@ import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
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.flows.StartableByRPC
import net.corda.core.node.PluginServiceHub 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
@ -30,6 +31,7 @@ object IssuerFlow {
* Returns the transaction created by the Issuer to move the cash to the Requester. * Returns the transaction created by the Issuer to move the cash to the Requester.
*/ */
@InitiatingFlow @InitiatingFlow
@StartableByRPC
class IssuanceRequester(val amount: Amount<Currency>, val issueToParty: Party, val issueToPartyRef: OpaqueBytes, class IssuanceRequester(val amount: Amount<Currency>, val issueToParty: Party, val issueToPartyRef: OpaqueBytes,
val issuerBankParty: Party) : FlowLogic<SignedTransaction>() { val issuerBankParty: Party) : FlowLogic<SignedTransaction>() {
@Suspendable @Suspendable

View File

@ -150,6 +150,9 @@ dependencies {
// Requery: object mapper for Kotlin // Requery: object mapper for Kotlin
compile "io.requery:requery-kotlin:$requery_version" compile "io.requery:requery-kotlin:$requery_version"
// FastClasspathScanner: classpath scanning
compile 'io.github.lukehutch:fast-classpath-scanner:2.0.19'
// Integration test helpers // Integration test helpers
integrationTestCompile "junit:junit:$junit_version" integrationTestCompile "junit:junit:$junit_version"
} }

View File

@ -1,10 +1,11 @@
package net.corda.node package net.corda.node
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.div import net.corda.core.div
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.utilities.ALICE import net.corda.core.utilities.ALICE
import net.corda.node.driver.driver import net.corda.node.driver.driver
import net.corda.node.services.startFlowPermission import net.corda.node.services.startFlowPermission
@ -48,18 +49,12 @@ class BootTests {
} }
} }
@StartableByRPC
class ObjectInputStreamFlow : FlowLogic<Unit>() { class ObjectInputStreamFlow : FlowLogic<Unit>() {
@Suspendable
override fun call() { override fun call() {
System.clearProperty("jdk.serialFilter") // This checks that the node has already consumed the property. System.clearProperty("jdk.serialFilter") // This checks that the node has already consumed the property.
val data = ByteArrayOutputStream().apply { ObjectOutputStream(this).use { it.writeObject(object : Serializable {}) } }.toByteArray() val data = ByteArrayOutputStream().apply { ObjectOutputStream(this).use { it.writeObject(object : Serializable {}) } }.toByteArray()
ObjectInputStream(data.inputStream()).use { it.readObject() } ObjectInputStream(data.inputStream()).use { it.readObject() }
} }
}
class BootTestsPlugin : CordaPluginRegistry() {
override val requiredFlows: Map<String, Set<String>> = mapOf(ObjectInputStreamFlow::class.java.name to setOf())
} }

View File

@ -5,15 +5,16 @@ import com.google.common.annotations.VisibleForTesting
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.scanner.ClassInfo
import net.corda.core.* import net.corda.core.*
import net.corda.core.contracts.Amount
import net.corda.core.contracts.PartyAndReference
import net.corda.core.crypto.KeyStoreUtilities import net.corda.core.crypto.KeyStoreUtilities
import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.replaceCommonName import net.corda.core.crypto.replaceCommonName
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.flows.InitiatingFlow 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
@ -21,7 +22,6 @@ 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.OpaqueBytes
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.serialization.serialize import net.corda.core.serialization.serialize
@ -47,7 +47,6 @@ import net.corda.node.services.network.PersistentNetworkMapService
import net.corda.node.services.persistence.* import net.corda.node.services.persistence.*
import net.corda.node.services.schema.HibernateObserver 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.FlowLogicRefFactoryImpl
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.flowVersion
@ -64,8 +63,11 @@ import org.bouncycastle.asn1.x500.X500Name
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.slf4j.Logger import org.slf4j.Logger
import java.io.IOException import java.io.IOException
import java.lang.reflect.Modifier.*
import java.net.URL
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.security.KeyPair import java.security.KeyPair
import java.security.KeyStoreException import java.security.KeyStoreException
import java.time.Clock import java.time.Clock
@ -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 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
@ -93,14 +96,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
companion object { companion object {
val PRIVATE_KEY_FILE_NAME = "identity-private-key" val PRIVATE_KEY_FILE_NAME = "identity-private-key"
val PUBLIC_IDENTITY_FILE_NAME = "identity-public" val PUBLIC_IDENTITY_FILE_NAME = "identity-public"
val defaultFlowWhiteList: Map<Class<out FlowLogic<*>>, Set<Class<*>>> = mapOf(
CashExitFlow::class.java to setOf(Amount::class.java, PartyAndReference::class.java),
CashIssueFlow::class.java to setOf(Amount::class.java, OpaqueBytes::class.java, Party::class.java),
CashPaymentFlow::class.java to setOf(Amount::class.java, Party::class.java),
FinalityFlow::class.java to setOf(LinkedHashSet::class.java),
ContractUpgradeFlow::class.java to emptySet()
)
} }
// TODO: Persist this, as well as whether the node is registered. // TODO: Persist this, as well as whether the node is registered.
@ -133,10 +128,10 @@ 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 val rpcFlows: List<Class<out FlowLogic<*>>> get() = this@AbstractNode.rpcFlows
// Internal only // Internal only
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
override val flowLogicRefFactory: FlowLogicRefFactoryInternal get() = flowLogicFactory
override fun <T> startFlow(logic: FlowLogic<T>, flowInitiator: FlowInitiator): FlowStateMachineImpl<T> { override fun <T> startFlow(logic: FlowLogic<T>, flowInitiator: FlowInitiator): FlowStateMachineImpl<T> {
return serverThread.fetchFrom { smm.add(logic, flowInitiator) } return serverThread.fetchFrom { smm.add(logic, flowInitiator) }
@ -176,13 +171,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
lateinit var net: MessagingService lateinit var net: MessagingService
lateinit var netMapCache: NetworkMapCacheInternal lateinit var netMapCache: NetworkMapCacheInternal
lateinit var scheduler: NodeSchedulerService lateinit var scheduler: NodeSchedulerService
lateinit var flowLogicFactory: FlowLogicRefFactoryInternal
lateinit var schemas: SchemaService lateinit var schemas: SchemaService
lateinit var auditService: AuditService lateinit var auditService: AuditService
val customServices: ArrayList<Any> = ArrayList() 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<*>>>
/** Locates and returns a service of the given type if loaded, or throws an exception if not found. */ /** 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() inline fun <reified T : Any> findService() = customServices.filterIsInstance<T>().single()
@ -250,6 +245,16 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
} }
startMessagingService(rpcOps) startMessagingService(rpcOps)
installCoreFlows() installCoreFlows()
fun Class<out FlowLogic<*>>.isUserInvokable(): Boolean {
return isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || isStatic(modifiers))
}
val flows = scanForFlows()
rpcFlows = flows.filter { it.isUserInvokable() && it.isAnnotationPresent(StartableByRPC::class.java) } +
// Add any core flows here
listOf(ContractUpgradeFlow::class.java)
runOnStop += Runnable { net.stop() } runOnStop += Runnable { net.stop() }
_networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured()) _networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured())
smm.start() smm.start()
@ -305,8 +310,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
// the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with
// the identity key. But the infrastructure to make that easy isn't here yet. // the identity key. But the infrastructure to make that easy isn't here yet.
keyManagement = makeKeyManagementService() keyManagement = makeKeyManagementService()
flowLogicFactory = initialiseFlowLogicFactory() scheduler = NodeSchedulerService(services, database, unfinishedSchedules = busyNodeLatch)
scheduler = NodeSchedulerService(services, database, flowLogicFactory, unfinishedSchedules = busyNodeLatch)
val tokenizableServices = mutableListOf(storage, net, vault, keyManagement, identity, platformClock, scheduler) val tokenizableServices = mutableListOf(storage, net, vault, keyManagement, identity, platformClock, scheduler)
makeAdvertisedServices(tokenizableServices) makeAdvertisedServices(tokenizableServices)
@ -318,6 +322,53 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
return tokenizableServices return tokenizableServices
} }
private fun scanForFlows(): List<Class<out FlowLogic<*>>> {
val pluginsDir = configuration.baseDirectory / "plugins"
log.info("Scanning plugins in $pluginsDir ...")
if (!pluginsDir.exists()) return emptyList()
val pluginJars = pluginsDir.list {
it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.toArray()
}
val scanResult = FastClasspathScanner().overrideClasspath(*pluginJars).scan() // This will only scan the plugin jars and nothing else
fun loadFlowClass(className: String): Class<out FlowLogic<*>>? {
return try {
// TODO Make sure this is loaded by the correct class loader
@Suppress("UNCHECKED_CAST")
Class.forName(className, false, javaClass.classLoader) as Class<out FlowLogic<*>>
} catch (e: Exception) {
log.warn("Unable to load flow class $className", e)
null
}
}
val flowClasses = scanResult.getNamesOfSubclassesOf(FlowLogic::class.java)
.mapNotNull { loadFlowClass(it) }
.filterNot { isAbstract(it.modifiers) }
fun URL.pluginName(): String {
return try {
Paths.get(toURI()).fileName.toString()
} catch (e: Exception) {
toString()
}
}
val classpathURLsField = ClassInfo::class.java.getDeclaredField("classpathElementURLs").apply { isAccessible = true }
flowClasses.groupBy {
val classInfo = scanResult.classNameToClassInfo[it.name]
@Suppress("UNCHECKED_CAST")
(classpathURLsField.get(classInfo) as Set<URL>).first()
}.forEach { url, classes ->
log.info("Found flows in plugin ${url.pluginName()}: ${classes.joinToString { it.name }}")
}
return flowClasses
}
private fun initUploaders(storageServices: Pair<TxWritableStorageService, CheckpointStorage>) { private fun initUploaders(storageServices: Pair<TxWritableStorageService, CheckpointStorage>) {
val uploaders: List<FileUploader> = listOf(storageServices.first.attachments as NodeAttachmentService) + val uploaders: List<FileUploader> = listOf(storageServices.first.attachments as NodeAttachmentService) +
customServices.filterIsInstance(AcceptsFileUpload::class.java) customServices.filterIsInstance(AcceptsFileUpload::class.java)
@ -386,26 +437,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
} }
} }
private fun initialiseFlowLogicFactory(): FlowLogicRefFactoryInternal {
val flowWhitelist = HashMap<String, Set<String>>()
for ((flowClass, extraArgumentTypes) in defaultFlowWhiteList) {
val argumentWhitelistClassNames = HashSet(extraArgumentTypes.map { it.name })
flowClass.constructors.forEach {
it.parameters.mapTo(argumentWhitelistClassNames) { it.type.name }
}
flowWhitelist.merge(flowClass.name, argumentWhitelistClassNames, { x, y -> x + y })
}
for (plugin in pluginRegistries) {
for ((className, classWhitelist) in plugin.requiredFlows) {
flowWhitelist.merge(className, classWhitelist, { x, y -> x + y })
}
}
return FlowLogicRefFactoryImpl(flowWhitelist)
}
private fun makePluginServices(tokenizableServices: MutableList<Any>): List<Any> { private fun makePluginServices(tokenizableServices: MutableList<Any>): List<Any> {
val pluginServices = pluginRegistries.flatMap { it.servicePlugins }.map { it.apply(services) } val pluginServices = pluginRegistries.flatMap { it.servicePlugins }.map { it.apply(services) }
tokenizableServices.addAll(pluginServices) tokenizableServices.addAll(pluginServices)

View File

@ -7,6 +7,7 @@ import net.corda.core.contracts.UpgradedContract
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
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.flows.StartableByRPC
import net.corda.core.messaging.* import net.corda.core.messaging.*
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache
@ -20,6 +21,7 @@ import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.messaging.getRpcContext import net.corda.node.services.messaging.getRpcContext
import net.corda.node.services.messaging.requirePermission import net.corda.node.services.messaging.requirePermission
import net.corda.node.services.startFlowPermission import net.corda.node.services.startFlowPermission
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.utilities.transaction import net.corda.node.utilities.transaction
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
@ -39,8 +41,6 @@ class CordaRPCOpsImpl(
private val smm: StateMachineManager, private val smm: StateMachineManager,
private val database: Database private val database: Database
) : CordaRPCOps { ) : CordaRPCOps {
override val protocolVersion: Int = 0
override fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>> { override fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>> {
return database.transaction { return database.transaction {
services.networkMapCache.track() services.networkMapCache.track()
@ -115,12 +115,8 @@ class CordaRPCOpsImpl(
} }
} }
// TODO: Check that this flow is annotated as being intended for RPC invocation
override fun <T : Any> startTrackedFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowProgressHandle<T> { override fun <T : Any> startTrackedFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowProgressHandle<T> {
val rpcContext = getRpcContext() val stateMachine = startFlow(logicType, args)
rpcContext.requirePermission(startFlowPermission(logicType))
val currentUser = FlowInitiator.RPC(rpcContext.currentUser.username)
val stateMachine = services.invokeFlowAsync(logicType, currentUser, *args)
return FlowProgressHandleImpl( return FlowProgressHandleImpl(
id = stateMachine.id, id = stateMachine.id,
returnValue = stateMachine.resultFuture, returnValue = stateMachine.resultFuture,
@ -128,13 +124,17 @@ class CordaRPCOpsImpl(
) )
} }
// TODO: Check that this flow is annotated as being intended for RPC invocation
override fun <T : Any> startFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowHandle<T> { override fun <T : Any> startFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowHandle<T> {
val stateMachine = startFlow(logicType, args)
return FlowHandleImpl(id = stateMachine.id, returnValue = stateMachine.resultFuture)
}
private fun <T : Any> startFlow(logicType: Class<out FlowLogic<T>>, args: Array<out Any?>): FlowStateMachineImpl<T> {
require(logicType.isAnnotationPresent(StartableByRPC::class.java)) { "${logicType.name} was not designed for RPC" }
val rpcContext = getRpcContext() val rpcContext = getRpcContext()
rpcContext.requirePermission(startFlowPermission(logicType)) rpcContext.requirePermission(startFlowPermission(logicType))
val currentUser = FlowInitiator.RPC(rpcContext.currentUser.username) val currentUser = FlowInitiator.RPC(rpcContext.currentUser.username)
val stateMachine = services.invokeFlowAsync(logicType, currentUser, *args) return services.invokeFlowAsync(logicType, currentUser, *args)
return FlowHandleImpl(id = stateMachine.id, returnValue = stateMachine.resultFuture)
} }
override fun attachmentExists(id: SecureHash): Boolean { override fun attachmentExists(id: SecureHash): Boolean {
@ -171,11 +171,12 @@ class CordaRPCOpsImpl(
override fun waitUntilRegisteredWithNetworkMap() = services.networkMapCache.mapServiceRegistered override fun waitUntilRegisteredWithNetworkMap() = services.networkMapCache.mapServiceRegistered
override fun partyFromKey(key: PublicKey) = services.identityService.partyFromKey(key) override fun partyFromKey(key: PublicKey) = services.identityService.partyFromKey(key)
@Suppress("DEPRECATION")
@Deprecated("Use partyFromX500Name instead") @Deprecated("Use partyFromX500Name instead")
override fun partyFromName(name: String) = services.identityService.partyFromName(name) override fun partyFromName(name: String) = services.identityService.partyFromName(name)
override fun partyFromX500Name(x500Name: X500Name)= services.identityService.partyFromX500Name(x500Name) override fun partyFromX500Name(x500Name: X500Name)= services.identityService.partyFromX500Name(x500Name)
override fun registeredFlows(): List<String> = services.flowLogicRefFactory.flowWhitelist.keys.sorted() override fun registeredFlows(): List<String> = services.rpcFlows.map { it.name }.sorted()
companion object { companion object {
private fun stateMachineInfoFromFlowLogic(flowLogic: FlowLogic<*>): StateMachineInfo { private fun stateMachineInfoFromFlowLogic(flowLogic: FlowLogic<*>): StateMachineInfo {

View File

@ -2,7 +2,9 @@ package net.corda.node.services.api
import com.google.common.annotations.VisibleForTesting import com.google.common.annotations.VisibleForTesting
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.flows.* import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowStateMachine
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.PluginServiceHub import net.corda.core.node.PluginServiceHub
@ -13,6 +15,7 @@ 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.ServiceFlowInfo
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.FlowStateMachineImpl import net.corda.node.services.statemachine.FlowStateMachineImpl
interface NetworkMapCacheInternal : NetworkMapCache { interface NetworkMapCacheInternal : NetworkMapCache {
@ -47,11 +50,6 @@ interface NetworkMapCacheInternal : NetworkMapCache {
} }
interface FlowLogicRefFactoryInternal : FlowLogicRefFactory {
val flowWhitelist: Map<String, Set<String>>
fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*>
}
@CordaSerializable @CordaSerializable
sealed class NetworkCacheError : Exception() { sealed class NetworkCacheError : Exception() {
/** Indicates a failure to deregister, because of a rejected request from the remote node */ /** Indicates a failure to deregister, because of a rejected request from the remote node */
@ -64,12 +62,11 @@ abstract class ServiceHubInternal : PluginServiceHub {
} }
abstract val monitoringService: MonitoringService abstract val monitoringService: MonitoringService
abstract val flowLogicRefFactory: FlowLogicRefFactoryInternal
abstract val schemaService: SchemaService abstract val schemaService: SchemaService
abstract override val networkMapCache: NetworkMapCacheInternal abstract override val networkMapCache: NetworkMapCacheInternal
abstract val schedulerService: SchedulerService abstract val schedulerService: SchedulerService
abstract val auditService: AuditService abstract val auditService: AuditService
abstract val rpcFlows: List<Class<out FlowLogic<*>>>
abstract val networkService: MessagingService abstract val networkService: MessagingService
/** /**
@ -119,9 +116,9 @@ abstract class ServiceHubInternal : PluginServiceHub {
logicType: Class<out FlowLogic<T>>, logicType: Class<out FlowLogic<T>>,
flowInitiator: FlowInitiator, flowInitiator: FlowInitiator,
vararg args: Any?): FlowStateMachineImpl<T> { vararg args: Any?): FlowStateMachineImpl<T> {
val logicRef = flowLogicRefFactory.create(logicType, *args) val logicRef = FlowLogicRefFactoryImpl.createForRPC(logicType, *args)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val logic = flowLogicRefFactory.toFlowLogic(logicRef) as FlowLogic<T> val logic = FlowLogicRefFactoryImpl.toFlowLogic(logicRef) as FlowLogic<T>
return startFlow(logic, flowInitiator) return startFlow(logic, flowInitiator)
} }

View File

@ -12,9 +12,9 @@ import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.then import net.corda.core.then
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.services.api.FlowLogicRefFactoryInternal
import net.corda.node.services.api.SchedulerService import net.corda.node.services.api.SchedulerService
import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
import net.corda.node.utilities.* import net.corda.node.utilities.*
import org.apache.activemq.artemis.utils.ReusableLatch import org.apache.activemq.artemis.utils.ReusableLatch
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
@ -38,14 +38,12 @@ import javax.annotation.concurrent.ThreadSafe
* but that starts to sound a lot like off-ledger state. * but that starts to sound a lot like off-ledger state.
* *
* @param services Core node services. * @param services Core node services.
* @param flowLogicRefFactory Factory for restoring [FlowLogic] instances from references.
* @param schedulerTimerExecutor The executor the scheduler blocks on waiting for the clock to advance to the next * @param schedulerTimerExecutor The executor the scheduler blocks on waiting for the clock to advance to the next
* activity. Only replace this for unit testing purposes. This is not the executor the [FlowLogic] is launched on. * activity. Only replace this for unit testing purposes. This is not the executor the [FlowLogic] is launched on.
*/ */
@ThreadSafe @ThreadSafe
class NodeSchedulerService(private val services: ServiceHubInternal, class NodeSchedulerService(private val services: ServiceHubInternal,
private val database: Database, private val database: Database,
private val flowLogicRefFactory: FlowLogicRefFactoryInternal,
private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(), private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(),
private val unfinishedSchedules: ReusableLatch = ReusableLatch()) private val unfinishedSchedules: ReusableLatch = ReusableLatch())
: SchedulerService, SingletonSerializeAsToken() { : SchedulerService, SingletonSerializeAsToken() {
@ -192,7 +190,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal,
ScheduledStateRef(scheduledState.ref, scheduledActivity.scheduledAt) ScheduledStateRef(scheduledState.ref, scheduledActivity.scheduledAt)
} else { } else {
// TODO: FlowLogicRefFactory needs to sort out the class loader etc // TODO: FlowLogicRefFactory needs to sort out the class loader etc
val flowLogic = flowLogicRefFactory.toFlowLogic(scheduledActivity.logicRef) val flowLogic = FlowLogicRefFactoryImpl.toFlowLogic(scheduledActivity.logicRef)
log.trace { "Scheduler starting FlowLogic $flowLogic" } log.trace { "Scheduler starting FlowLogic $flowLogic" }
scheduledFlow = flowLogic scheduledFlow = flowLogic
null null
@ -213,7 +211,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal,
val state = txState.data as SchedulableState val state = txState.data as SchedulableState
return try { return try {
// This can throw as running contract code. // This can throw as running contract code.
state.nextScheduledActivity(scheduledState.ref, flowLogicRefFactory) state.nextScheduledActivity(scheduledState.ref, FlowLogicRefFactoryImpl)
} catch (e: Exception) { } catch (e: Exception) {
log.error("Attempt to run scheduled state $scheduledState resulted in error.", e) log.error("Attempt to run scheduled state $scheduledState resulted in error.", e)
null null

View File

@ -4,8 +4,8 @@ import net.corda.core.contracts.ContractState
import net.corda.core.contracts.SchedulableState import net.corda.core.contracts.SchedulableState
import net.corda.core.contracts.ScheduledStateRef import net.corda.core.contracts.ScheduledStateRef
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.flows.FlowLogicRefFactory
import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.ServiceHubInternal
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
/** /**
* This observes the vault and schedules and unschedules activities appropriately based on state production and * This observes the vault and schedules and unschedules activities appropriately based on state production and
@ -13,16 +13,16 @@ import net.corda.node.services.api.ServiceHubInternal
*/ */
class ScheduledActivityObserver(val services: ServiceHubInternal) { class ScheduledActivityObserver(val services: ServiceHubInternal) {
init { init {
services.vaultService.rawUpdates.subscribe { update -> services.vaultService.rawUpdates.subscribe { (consumed, produced) ->
update.consumed.forEach { services.schedulerService.unscheduleStateActivity(it.ref) } consumed.forEach { services.schedulerService.unscheduleStateActivity(it.ref) }
update.produced.forEach { scheduleStateActivity(it, services.flowLogicRefFactory) } produced.forEach { scheduleStateActivity(it) }
} }
} }
private fun scheduleStateActivity(produced: StateAndRef<ContractState>, flowLogicRefFactory: FlowLogicRefFactory) { private fun scheduleStateActivity(produced: StateAndRef<ContractState>) {
val producedState = produced.state.data val producedState = produced.state.data
if (producedState is SchedulableState) { if (producedState is SchedulableState) {
val scheduledAt = sandbox { producedState.nextScheduledActivity(produced.ref, flowLogicRefFactory)?.scheduledAt } ?: return val scheduledAt = sandbox { producedState.nextScheduledActivity(produced.ref, FlowLogicRefFactoryImpl)?.scheduledAt } ?: return
services.schedulerService.scheduleStateActivity(ScheduledStateRef(produced.ref, scheduledAt)) services.schedulerService.scheduleStateActivity(ScheduledStateRef(produced.ref, scheduledAt))
} }
} }

View File

@ -1,14 +1,10 @@
package net.corda.node.services.statemachine package net.corda.node.services.statemachine
import com.google.common.annotations.VisibleForTesting
import com.google.common.primitives.Primitives import com.google.common.primitives.Primitives
import net.corda.core.crypto.SecureHash import net.corda.core.flows.*
import net.corda.core.flows.AppContext
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowLogicRef
import net.corda.core.flows.IllegalFlowLogicException
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.node.services.api.FlowLogicRefFactoryInternal
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type import java.lang.reflect.Type
import java.util.* import java.util.*
@ -29,59 +25,26 @@ data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String,
* Validation of types is performed on the way in and way out in case this object is passed between JVMs which might have differing * Validation of types is performed on the way in and way out in case this object is passed between JVMs which might have differing
* whitelists. * whitelists.
* *
* TODO: Ways to populate whitelist of "blessed" flows per node/party
* TODO: Ways to populate argument types whitelist. Per node/party or global?
* TODO: Align with API related logic for passing in FlowLogic references (FlowRef) * TODO: Align with API related logic for passing in FlowLogic references (FlowRef)
* TODO: Actual support for AppContext / AttachmentsClassLoader * TODO: Actual support for AppContext / AttachmentsClassLoader
* TODO: at some point check whether there is permission, beyond the annotations, to start flows. For example, as a security
* measure we might want the ability for the node admin to blacklist a flow such that it moves immediately to the "Flow Hospital"
* in response to a potential malicious use or buggy update to an app etc.
*/ */
class FlowLogicRefFactoryImpl(override val flowWhitelist: Map<String, Set<String>>) : SingletonSerializeAsToken(), FlowLogicRefFactoryInternal { object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactory {
constructor() : this(mapOf()) override fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
if (!flowClass.isAnnotationPresent(SchedulableFlow::class.java)) {
// Pending real dependence on AppContext for class loading etc throw IllegalFlowLogicException(flowClass, "because it's not a schedulable flow")
@Suppress("UNUSED_PARAMETER") }
private fun validateFlowClassName(className: String, appContext: AppContext) { return createForRPC(flowClass, *args)
// TODO: make this specific to the attachments in the [AppContext] by including [SecureHash] in whitelist check
require(flowWhitelist.containsKey(className)) { "${FlowLogic::class.java.simpleName} of ${FlowLogicRef::class.java.simpleName} must have type on the whitelist: $className" }
} }
// Pending real dependence on AppContext for class loading etc fun createForRPC(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
@Suppress("UNUSED_PARAMETER")
private fun validateArgClassName(className: String, argClassName: String, appContext: AppContext) {
// TODO: consider more carefully what to whitelist and how to secure flows
// For now automatically accept standard java.lang.* and kotlin.* types.
// All other types require manual specification at FlowLogicRefFactory construction time.
if (argClassName.startsWith("java.lang.") || argClassName.startsWith("kotlin.")) {
return
}
// TODO: make this specific to the attachments in the [AppContext] by including [SecureHash] in whitelist check
require(flowWhitelist[className]!!.contains(argClassName)) { "Args to $className must have types on the args whitelist: $argClassName, but it has ${flowWhitelist[className]}" }
}
/**
* Create a [FlowLogicRef] for the Kotlin primary constructor of a named [FlowLogic]
*/
fun createKotlin(flowLogicClassName: String, args: Map<String, Any?>, attachments: List<SecureHash> = emptyList()): FlowLogicRef {
val context = AppContext(attachments)
validateFlowClassName(flowLogicClassName, context)
for (arg in args.values.filterNotNull()) {
validateArgClassName(flowLogicClassName, arg.javaClass.name, context)
}
val clazz = Class.forName(flowLogicClassName)
require(FlowLogic::class.java.isAssignableFrom(clazz)) { "$flowLogicClassName is not a FlowLogic" }
@Suppress("UNCHECKED_CAST")
val logic = clazz as Class<FlowLogic<FlowLogic<*>>>
return createKotlin(logic, args)
}
/**
* Create a [FlowLogicRef] by assuming a single constructor and the given args.
*/
override fun create(type: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
// TODO: This is used via RPC but it's probably better if we pass in argument names and values explicitly // TODO: This is used via RPC but it's probably better if we pass in argument names and values explicitly
// to avoid requiring only a single constructor. // to avoid requiring only a single constructor.
val argTypes = args.map { it?.javaClass } val argTypes = args.map { it?.javaClass }
val constructor = try { val constructor = try {
type.kotlin.constructors.single { ctor -> flowClass.kotlin.constructors.single { ctor ->
// Get the types of the arguments, always boxed (as that's what we get in the invocation). // Get the types of the arguments, always boxed (as that's what we get in the invocation).
val ctorTypes = ctor.javaConstructor!!.parameterTypes.map { Primitives.wrap(it) } val ctorTypes = ctor.javaConstructor!!.parameterTypes.map { Primitives.wrap(it) }
if (argTypes.size != ctorTypes.size) if (argTypes.size != ctorTypes.size)
@ -93,14 +56,14 @@ class FlowLogicRefFactoryImpl(override val flowWhitelist: Map<String, Set<String
true true
} }
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
throw IllegalFlowLogicException(type, "due to ambiguous match against the constructors: $argTypes") throw IllegalFlowLogicException(flowClass, "due to ambiguous match against the constructors: $argTypes")
} catch (e: NoSuchElementException) { } catch (e: NoSuchElementException) {
throw IllegalFlowLogicException(type, "due to missing constructor for arguments: $argTypes") throw IllegalFlowLogicException(flowClass, "due to missing constructor for arguments: $argTypes")
} }
// Build map of args from array // Build map of args from array
val argsMap = args.zip(constructor.parameters).map { Pair(it.second.name!!, it.first) }.toMap() val argsMap = args.zip(constructor.parameters).map { Pair(it.second.name!!, it.first) }.toMap()
return createKotlin(type, argsMap) return createKotlin(flowClass, argsMap)
} }
/** /**
@ -108,44 +71,32 @@ class FlowLogicRefFactoryImpl(override val flowWhitelist: Map<String, Set<String
* *
* TODO: Rethink language specific naming. * TODO: Rethink language specific naming.
*/ */
fun createKotlin(type: Class<out FlowLogic<*>>, args: Map<String, Any?>): FlowLogicRef { @VisibleForTesting
internal fun createKotlin(type: Class<out FlowLogic<*>>, args: Map<String, Any?>): FlowLogicRef {
// TODO: we need to capture something about the class loader or "application context" into the ref, // TODO: we need to capture something about the class loader or "application context" into the ref,
// perhaps as some sort of ThreadLocal style object. For now, just create an empty one. // perhaps as some sort of ThreadLocal style object. For now, just create an empty one.
val appContext = AppContext(emptyList()) val appContext = AppContext(emptyList())
validateFlowClassName(type.name, appContext)
// Check we can find a constructor and populate the args to it, but don't call it // Check we can find a constructor and populate the args to it, but don't call it
createConstructor(appContext, type, args) createConstructor(type, args)
return FlowLogicRefImpl(type.name, appContext, args) return FlowLogicRefImpl(type.name, appContext, args)
} }
/** fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> {
* Create a [FlowLogicRef] by trying to find a Java constructor that matches the given args.
*/
private fun createJava(type: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
// Build map for each
val argsMap = HashMap<String, Any?>(args.size)
var index = 0
args.forEach { argsMap["arg${index++}"] = it }
return createKotlin(type, argsMap)
}
override fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> {
if (ref !is FlowLogicRefImpl) throw IllegalFlowLogicException(ref.javaClass, "FlowLogicRef was not created via correct FlowLogicRefFactory interface") if (ref !is FlowLogicRefImpl) throw IllegalFlowLogicException(ref.javaClass, "FlowLogicRef was not created via correct FlowLogicRefFactory interface")
validateFlowClassName(ref.flowLogicClassName, ref.appContext)
val klass = Class.forName(ref.flowLogicClassName, true, ref.appContext.classLoader).asSubclass(FlowLogic::class.java) val klass = Class.forName(ref.flowLogicClassName, true, ref.appContext.classLoader).asSubclass(FlowLogic::class.java)
return createConstructor(ref.appContext, klass, ref.args)() return createConstructor(klass, ref.args)()
} }
private fun createConstructor(appContext: AppContext, clazz: Class<out FlowLogic<*>>, args: Map<String, Any?>): () -> FlowLogic<*> { private fun createConstructor(clazz: Class<out FlowLogic<*>>, args: Map<String, Any?>): () -> FlowLogic<*> {
for (constructor in clazz.kotlin.constructors) { for (constructor in clazz.kotlin.constructors) {
val params = buildParams(appContext, clazz, constructor, args) ?: continue val params = buildParams(constructor, args) ?: continue
// If we get here then we matched every parameter // If we get here then we matched every parameter
return { constructor.callBy(params) } return { constructor.callBy(params) }
} }
throw IllegalFlowLogicException(clazz, "as could not find matching constructor for: $args") throw IllegalFlowLogicException(clazz, "as could not find matching constructor for: $args")
} }
private fun buildParams(appContext: AppContext, clazz: Class<out FlowLogic<*>>, constructor: KFunction<FlowLogic<*>>, args: Map<String, Any?>): HashMap<KParameter, Any?>? { private fun buildParams(constructor: KFunction<FlowLogic<*>>, args: Map<String, Any?>): HashMap<KParameter, Any?>? {
val params = hashMapOf<KParameter, Any?>() val params = hashMapOf<KParameter, Any?>()
val usedKeys = hashSetOf<String>() val usedKeys = hashSetOf<String>()
for (parameter in constructor.parameters) { for (parameter in constructor.parameters) {
@ -159,7 +110,6 @@ class FlowLogicRefFactoryImpl(override val flowWhitelist: Map<String, Set<String
// Not all args were used // Not all args were used
return null return null
} }
params.values.forEach { if (it is Any) validateArgClassName(clazz.name, it.javaClass.name, appContext) }
return params return params
} }

View File

@ -210,7 +210,7 @@ object InteractiveShell {
*/ */
@JvmStatic @JvmStatic
fun runFlowByNameFragment(nameFragment: String, inputData: String, output: RenderPrintWriter) { fun runFlowByNameFragment(nameFragment: String, inputData: String, output: RenderPrintWriter) {
val matches = node.flowLogicFactory.flowWhitelist.keys.filter { nameFragment in it } val matches = node.services.rpcFlows.filter { nameFragment in it.name }
if (matches.isEmpty()) { if (matches.isEmpty()) {
output.println("No matching flow found, run 'flow list' to see your options.", Color.red) output.println("No matching flow found, run 'flow list' to see your options.", Color.red)
return return
@ -219,14 +219,12 @@ object InteractiveShell {
matches.forEachIndexed { i, s -> output.println("${i + 1}. $s", Color.yellow) } matches.forEachIndexed { i, s -> output.println("${i + 1}. $s", Color.yellow) }
return return
} }
val match = matches.single()
val clazz = Class.forName(match) @Suppress("UNCHECKED_CAST")
if (!FlowLogic::class.java.isAssignableFrom(clazz)) val clazz = matches.single() as Class<FlowLogic<*>>
throw IllegalStateException("Found a non-FlowLogic class in the whitelist? $clazz")
try { try {
// TODO Flow invocation should use startFlowDynamic. // TODO Flow invocation should use startFlowDynamic.
@Suppress("UNCHECKED_CAST") val fsm = runFlowFromString({ node.services.startFlow(it, FlowInitiator.Shell) }, inputData, clazz)
val fsm = runFlowFromString({ node.services.startFlow(it, FlowInitiator.Shell) }, inputData, clazz as Class<FlowLogic<*>>)
// Show the progress tracker on the console until the flow completes or is interrupted with a // Show the progress tracker on the console until the flow completes or is interrupted with a
// Ctrl-C keypress. // Ctrl-C keypress.
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
@ -275,10 +273,7 @@ object InteractiveShell {
var paramNamesFromConstructor: List<String>? = null var paramNamesFromConstructor: List<String>? = null
fun getPrototype(ctor: Constructor<*>): List<String> { fun getPrototype(ctor: Constructor<*>): List<String> {
val argTypes = ctor.parameterTypes.map { it.simpleName } val argTypes = ctor.parameterTypes.map { it.simpleName }
val prototype = paramNamesFromConstructor!!.zip(argTypes).map { pair -> val prototype = paramNamesFromConstructor!!.zip(argTypes).map { (name, type) -> "$name: $type" }
val (name, type) = pair
"$name: $type"
}
return prototype return prototype
} }
try { try {

View File

@ -1,15 +1,9 @@
package net.corda.node.services.events; package net.corda.node.services.events;
import net.corda.core.flows.FlowLogic; import net.corda.core.flows.FlowLogic;
import net.corda.core.flows.FlowLogicRefFactory;
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl; import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl;
import org.junit.Test; import org.junit.Test;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class FlowLogicRefFromJavaTest { public class FlowLogicRefFromJavaTest {
@SuppressWarnings("unused") @SuppressWarnings("unused")
@ -56,20 +50,11 @@ public class FlowLogicRefFromJavaTest {
@Test @Test
public void test() { public void test() {
Map<String, Set<String>> whiteList = new HashMap<>(); FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack"));
Set<String> argsList = new HashSet<>();
argsList.add(ParamType1.class.getName());
argsList.add(ParamType2.class.getName());
whiteList.put(JavaFlowLogic.class.getName(), argsList);
FlowLogicRefFactory factory = new FlowLogicRefFactoryImpl(whiteList);
factory.create(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack"));
} }
@Test @Test
public void testNoArg() { public void testNoArg() {
Map<String, Set<String>> whiteList = new HashMap<>(); FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaNoArgFlowLogic.class);
whiteList.put(JavaNoArgFlowLogic.class.getName(), new HashSet<>());
FlowLogicRefFactory factory = new FlowLogicRefFactoryImpl(whiteList);
factory.create(JavaNoArgFlowLogic.class);
} }
} }

View File

@ -1,9 +1,11 @@
package net.corda.node package net.corda.node
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash import net.corda.contracts.asset.Cash
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.keys import net.corda.core.crypto.keys
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
@ -35,7 +37,9 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import rx.Observable import rx.Observable
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import kotlin.test.* import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class CordaRPCOpsImplTest { class CordaRPCOpsImplTest {
@ -118,7 +122,6 @@ class CordaRPCOpsImplTest {
@Test @Test
fun `issue and move`() { fun `issue and move`() {
rpc.startFlow(::CashIssueFlow, rpc.startFlow(::CashIssueFlow,
Amount(100, USD), Amount(100, USD),
OpaqueBytes(ByteArray(1, { 1 })), OpaqueBytes(ByteArray(1, { 1 })),
@ -225,4 +228,19 @@ class CordaRPCOpsImplTest {
assertArrayEquals(bufferFile.toByteArray(), bufferRpc.toByteArray()) assertArrayEquals(bufferFile.toByteArray(), bufferRpc.toByteArray())
} }
@Test
fun `attempt to start non-RPC flow`() {
CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = setOf(
startFlowPermission<NonRPCFlow>()
))))
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
rpc.startFlow(::NonRPCFlow)
}
}
class NonRPCFlow : FlowLogic<Unit>() {
@Suspendable
override fun call() = Unit
}
} }

View File

@ -12,7 +12,6 @@ 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
import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
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.transactions.InMemoryTransactionVerifierService import net.corda.node.services.transactions.InMemoryTransactionVerifierService
@ -30,7 +29,6 @@ open class MockServiceHubInternal(
val mapCache: NetworkMapCacheInternal? = MockNetworkMapCache(), val mapCache: NetworkMapCacheInternal? = MockNetworkMapCache(),
val scheduler: SchedulerService? = null, val scheduler: SchedulerService? = null,
val overrideClock: Clock? = NodeClock(), val overrideClock: Clock? = NodeClock(),
val flowFactory: FlowLogicRefFactoryInternal? = FlowLogicRefFactoryImpl(),
val schemas: SchemaService? = NodeSchemaService(), val schemas: SchemaService? = NodeSchemaService(),
val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2) val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2)
) : ServiceHubInternal() { ) : ServiceHubInternal() {
@ -56,8 +54,8 @@ open class MockServiceHubInternal(
get() = throw UnsupportedOperationException() get() = throw UnsupportedOperationException()
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
override val flowLogicRefFactory: FlowLogicRefFactoryInternal override val rpcFlows: List<Class<out FlowLogic<*>>>
get() = flowFactory ?: throw UnsupportedOperationException() get() = throw UnsupportedOperationException()
override val schemaService: SchemaService override val schemaService: SchemaService
get() = schemas ?: throw UnsupportedOperationException() get() = schemas ?: throw UnsupportedOperationException()
override val auditService: AuditService = DummyAuditService() override val auditService: AuditService = DummyAuditService()

View File

@ -1,9 +1,8 @@
package net.corda.node.services.events package net.corda.node.services.events
import net.corda.core.days
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.IllegalFlowLogicException
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
import org.junit.Before
import org.junit.Test import org.junit.Test
import java.time.Duration import java.time.Duration
@ -31,67 +30,51 @@ class FlowLogicRefTest {
override fun call() = Unit override fun call() = Unit
} }
@Suppress("UNUSED_PARAMETER") // We will never use A or b class NonSchedulableFlow : FlowLogic<Unit>() {
class NotWhiteListedKotlinFlowLogic(A: Int, b: String) : FlowLogic<Unit>() {
override fun call() = Unit override fun call() = Unit
} }
lateinit var factory: FlowLogicRefFactoryImpl @Test
fun `create kotlin no arg`() {
@Before FlowLogicRefFactoryImpl.createForRPC(KotlinNoArgFlowLogic::class.java)
fun setup() {
// We have to allow Java boxed primitives but Kotlin warns we shouldn't be using them
factory = FlowLogicRefFactoryImpl(mapOf(Pair(KotlinFlowLogic::class.java.name, setOf(ParamType1::class.java.name, ParamType2::class.java.name)),
Pair(KotlinNoArgFlowLogic::class.java.name, setOf())))
} }
@Test @Test
fun testCreateKotlinNoArg() { fun `create kotlin`() {
factory.create(KotlinNoArgFlowLogic::class.java)
}
@Test
fun testCreateKotlin() {
val args = mapOf(Pair("A", ParamType1(1)), Pair("b", ParamType2("Hello Jack"))) val args = mapOf(Pair("A", ParamType1(1)), Pair("b", ParamType2("Hello Jack")))
factory.createKotlin(KotlinFlowLogic::class.java, args) FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
} }
@Test @Test
fun testCreatePrimary() { fun `create primary`() {
factory.create(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack")) FlowLogicRefFactoryImpl.createForRPC(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack"))
}
@Test(expected = IllegalArgumentException::class)
fun testCreateNotWhiteListed() {
factory.create(NotWhiteListedKotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack"))
} }
@Test @Test
fun testCreateKotlinVoid() { fun `create kotlin void`() {
factory.createKotlin(KotlinFlowLogic::class.java, emptyMap()) FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, emptyMap())
} }
@Test @Test
fun testCreateKotlinNonPrimary() { fun `create kotlin non primary`() {
val args = mapOf(Pair("C", ParamType2("Hello Jack"))) val args = mapOf(Pair("C", ParamType2("Hello Jack")))
factory.createKotlin(KotlinFlowLogic::class.java, args) FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
}
@Test(expected = IllegalArgumentException::class)
fun testCreateArgNotWhiteListed() {
val args = mapOf(Pair("illegal", 1.days))
factory.createKotlin(KotlinFlowLogic::class.java, args)
} }
@Test @Test
fun testCreateJavaPrimitiveNoRegistrationRequired() { fun `create java primitive no registration required`() {
val args = mapOf(Pair("primitive", "A string")) val args = mapOf(Pair("primitive", "A string"))
factory.createKotlin(KotlinFlowLogic::class.java, args) FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
} }
@Test @Test
fun testCreateKotlinPrimitiveNoRegistrationRequired() { fun `create kotlin primitive no registration required`() {
val args = mapOf(Pair("kotlinType", 3)) val args = mapOf(Pair("kotlinType", 3))
factory.createKotlin(KotlinFlowLogic::class.java, args) FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
}
@Test(expected = IllegalFlowLogicException::class)
fun `create for non-schedulable flow logic`() {
FlowLogicRefFactoryImpl.create(NonSchedulableFlow::class.java)
} }
} }

View File

@ -44,10 +44,6 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
val schedulerGatedExecutor = AffinityExecutor.Gate(true) val schedulerGatedExecutor = AffinityExecutor.Gate(true)
// We have to allow Java boxed primitives but Kotlin warns we shouldn't be using them
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
val factory = FlowLogicRefFactoryImpl(mapOf(Pair(TestFlowLogic::class.java.name, setOf(NodeSchedulerServiceTest::class.java.name, Integer::class.java.name))))
lateinit var services: MockServiceHubInternal lateinit var services: MockServiceHubInternal
lateinit var scheduler: NodeSchedulerService lateinit var scheduler: NodeSchedulerService
@ -82,12 +78,16 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
database.transaction { database.transaction {
val kms = MockKeyManagementService(ALICE_KEY) val kms = MockKeyManagementService(ALICE_KEY)
val nullIdentity = X500Name("cn=None") val nullIdentity = X500Name("cn=None")
val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging(false, InMemoryMessagingNetwork.PeerHandle(0, nullIdentity), AffinityExecutor.ServiceAffinityExecutor("test", 1), database) val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging(
false,
InMemoryMessagingNetwork.PeerHandle(0, nullIdentity),
AffinityExecutor.ServiceAffinityExecutor("test", 1),
database)
services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference { services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference {
override val vaultService: VaultService = NodeVaultService(this, dataSourceProps) override val vaultService: VaultService = NodeVaultService(this, dataSourceProps)
override val testReference = this@NodeSchedulerServiceTest override val testReference = this@NodeSchedulerServiceTest
} }
scheduler = NodeSchedulerService(services, database, factory, 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, listOf(services, scheduler), DBCheckpointStorage(), smmExecutor, database)
mockSMM.changes.subscribe { change -> mockSMM.changes.subscribe { change ->
@ -269,7 +269,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
database.transaction { database.transaction {
apply { apply {
val freshKey = services.keyManagementService.freshKey() val freshKey = services.keyManagementService.freshKey()
val state = TestState(factory.create(TestFlowLogic::class.java, increment), instant) val state = TestState(FlowLogicRefFactoryImpl.createForRPC(TestFlowLogic::class.java, increment), instant)
val usefulTX = TransactionType.General.Builder(null).apply { val usefulTX = TransactionType.General.Builder(null).apply {
addOutputState(state, DUMMY_NOTARY) addOutputState(state, DUMMY_NOTARY)
addCommand(Command(), freshKey.public) addCommand(Command(), freshKey.public)

View File

@ -7,7 +7,7 @@ import net.corda.core.crypto.containsAny
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.flows.FlowLogicRefFactory import net.corda.core.flows.FlowLogicRefFactory
import net.corda.core.node.CordaPluginRegistry import net.corda.core.flows.SchedulableFlow
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
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
@ -15,7 +15,6 @@ import net.corda.flows.FinalityFlow
import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.statemachine.StateMachineManager
import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.node.services.transactions.ValidatingNotaryService
import net.corda.node.utilities.AddOrRemove
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
@ -68,6 +67,7 @@ class ScheduledFlowTests {
} }
} }
@SchedulableFlow
class ScheduledFlow(val stateRef: StateRef) : FlowLogic<Unit>() { class ScheduledFlow(val stateRef: StateRef) : FlowLogic<Unit>() {
@Suspendable @Suspendable
override fun call() { override fun call() {
@ -87,14 +87,6 @@ class ScheduledFlowTests {
} }
} }
object ScheduledFlowTestPlugin : CordaPluginRegistry() {
override val requiredFlows: Map<String, Set<String>> = mapOf(
InsertInitialStateFlow::class.java.name to setOf(Party::class.java.name),
ScheduledFlow::class.java.name to setOf(StateRef::class.java.name)
)
}
@Before @Before
fun setup() { fun setup() {
net = MockNetwork(threadPerNode = true) net = MockNetwork(threadPerNode = true)
@ -103,8 +95,6 @@ class ScheduledFlowTests {
advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type))) advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type)))
nodeA = net.createNode(notaryNode.info.address, start = false) nodeA = net.createNode(notaryNode.info.address, start = false)
nodeB = net.createNode(notaryNode.info.address, start = false) nodeB = net.createNode(notaryNode.info.address, start = false)
nodeA.testPluginRegistries.add(ScheduledFlowTestPlugin)
nodeB.testPluginRegistries.add(ScheduledFlowTestPlugin)
net.startNodes() net.startNodes()
} }
@ -138,7 +128,7 @@ class ScheduledFlowTests {
} }
@Test @Test
fun `Run a whole batch of scheduled flows`() { fun `run a whole batch of scheduled flows`() {
val N = 100 val N = 100
for (i in 0..N - 1) { for (i in 0..N - 1) {
nodeA.services.startFlow(InsertInitialStateFlow(nodeB.info.legalIdentity)) nodeA.services.startFlow(InsertInitialStateFlow(nodeB.info.legalIdentity))

View File

@ -7,6 +7,7 @@ import net.corda.core.utilities.DUMMY_BANK_A
import net.corda.core.utilities.DUMMY_BANK_B import net.corda.core.utilities.DUMMY_BANK_B
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.node.driver.driver import net.corda.node.driver.driver
import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.nodeapi.User import net.corda.nodeapi.User
import org.junit.Test import org.junit.Test
@ -17,11 +18,11 @@ class AttachmentDemoTest {
@Test fun `attachment demo using a 10MB zip file`() { @Test fun `attachment demo using a 10MB zip file`() {
val numOfExpectedBytes = 10_000_000 val numOfExpectedBytes = 10_000_000
driver(dsl = { driver(dsl = {
val demoUser = listOf(User("demo", "demo", setOf("StartFlow.net.corda.flows.FinalityFlow"))) val demoUser = listOf(User("demo", "demo", setOf(startFlowPermission<AttachmentDemoFlow>())))
val (nodeA, nodeB) = Futures.allAsList( val (nodeA, nodeB) = Futures.allAsList(
startNode(DUMMY_BANK_A.name, rpcUsers = demoUser), startNode(DUMMY_BANK_A.name, rpcUsers = demoUser),
startNode(DUMMY_BANK_B.name, rpcUsers = demoUser), startNode(DUMMY_BANK_B.name, rpcUsers = demoUser),
startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.Companion.type))) startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type)))
).getOrThrow() ).getOrThrow()
val senderThread = CompletableFuture.supplyAsync { val senderThread = CompletableFuture.supplyAsync {

View File

@ -1,5 +1,6 @@
package net.corda.attachmentdemo package net.corda.attachmentdemo
import co.paralleluniverse.fibers.Suspendable
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import joptsimple.OptionParser import joptsimple.OptionParser
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
@ -9,14 +10,14 @@ import net.corda.core.contracts.TransactionForContract
import net.corda.core.contracts.TransactionType import net.corda.core.contracts.TransactionType
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startTrackedFlow import net.corda.core.messaging.startTrackedFlow
import net.corda.core.sizedInputStreamAndHash import net.corda.core.sizedInputStreamAndHash
import net.corda.core.utilities.DUMMY_BANK_B import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.*
import net.corda.core.utilities.DUMMY_NOTARY_KEY
import net.corda.core.utilities.Emoji
import net.corda.flows.FinalityFlow import net.corda.flows.FinalityFlow
import net.corda.node.driver.poll import net.corda.node.driver.poll
import java.io.InputStream import java.io.InputStream
@ -83,24 +84,38 @@ fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.SHA256)
val id = rpc.uploadAttachment(it) val id = rpc.uploadAttachment(it)
require(hash == id) { "Id was '$id' instead of '$hash'" } require(hash == id) { "Id was '$id' instead of '$hash'" }
} }
require(rpc.attachmentExists(hash))
} }
val flowHandle = rpc.startTrackedFlow(::AttachmentDemoFlow, otherSide, hash)
flowHandle.progress.subscribe(::println)
val stx = flowHandle.returnValue.getOrThrow()
println("Sent ${stx.id}")
}
@StartableByRPC
class AttachmentDemoFlow(val otherSide: Party, val hash: SecureHash.SHA256) : FlowLogic<SignedTransaction>() {
object SIGNING : ProgressTracker.Step("Signing transaction")
override val progressTracker: ProgressTracker = ProgressTracker(SIGNING)
@Suspendable
override fun call(): SignedTransaction {
// Create a trivial transaction with an output that describes the attachment, and the attachment itself // Create a trivial transaction with an output that describes the attachment, and the attachment itself
val ptx = TransactionType.General.Builder(notary = DUMMY_NOTARY) val ptx = TransactionType.General.Builder(notary = DUMMY_NOTARY)
require(rpc.attachmentExists(hash))
ptx.addOutputState(AttachmentContract.State(hash)) ptx.addOutputState(AttachmentContract.State(hash))
ptx.addAttachment(hash) ptx.addAttachment(hash)
progressTracker.currentStep = SIGNING
// Sign with the notary key // Sign with the notary key
ptx.signWith(DUMMY_NOTARY_KEY) ptx.signWith(DUMMY_NOTARY_KEY)
// Send the transaction to the other recipient // Send the transaction to the other recipient
val stx = ptx.toSignedTransaction() val stx = ptx.toSignedTransaction()
println("Sending ${stx.id}")
val flowHandle = rpc.startTrackedFlow(::FinalityFlow, stx, setOf(otherSide)) return subFlow(FinalityFlow(stx, setOf(otherSide))).single()
flowHandle.progress.subscribe(::println) }
flowHandle.returnValue.getOrThrow()
println("Sent ${stx.id}")
} }
fun recipient(rpc: CordaRPCOps) { fun recipient(rpc: CordaRPCOps) {

View File

@ -1,12 +0,0 @@
package net.corda.attachmentdemo.plugin
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.transactions.SignedTransaction
import net.corda.flows.FinalityFlow
class AttachmentDemoPlugin : CordaPluginRegistry() {
// A list of Flows that are required for this cordapp
override val requiredFlows: Map<String, Set<String>> = mapOf(
FinalityFlow::class.java.name to setOf(SignedTransaction::class.java.name, setOf(Unit).javaClass.name, setOf(Unit).javaClass.name)
)
}

View File

@ -1,2 +0,0 @@
# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry
net.corda.attachmentdemo.plugin.AttachmentDemoPlugin

View File

@ -1,17 +1,13 @@
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.contracts.Amount
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.CordaPluginRegistry
import net.corda.core.serialization.OpaqueBytes
import net.corda.flows.IssuerFlow 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))
// A list of flow that are required for this cordapp
override val requiredFlows: Map<String, Set<String>> = mapOf(IssuerFlow.IssuanceRequester::class.java.name to setOf(Amount::class.java.name, Party::class.java.name, OpaqueBytes::class.java.name, Party::class.java.name))
override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service)) override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service))
} }

View File

@ -19,7 +19,6 @@ import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.FilteredTransaction
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.irs.flows.FixingFlow
import net.corda.irs.flows.RatesFixFlow import net.corda.irs.flows.RatesFixFlow
import net.corda.node.services.api.AcceptsFileUpload import net.corda.node.services.api.AcceptsFileUpload
import net.corda.node.utilities.AbstractJDBCHashSet import net.corda.node.utilities.AbstractJDBCHashSet
@ -32,12 +31,14 @@ import java.io.InputStream
import java.math.BigDecimal import java.math.BigDecimal
import java.security.KeyPair import java.security.KeyPair
import java.time.Clock import java.time.Clock
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 java.util.function.Function
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set
/** /**
* An interest rates service is an oracle that signs transactions which contain embedded assertions about an interest * An interest rates service is an oracle that signs transactions which contain embedded assertions about an interest
@ -55,7 +56,6 @@ object NodeInterestRates {
* Register the flow that is used with the Fixing integration tests. * Register the flow that is used with the Fixing integration tests.
*/ */
class Plugin : CordaPluginRegistry() { class Plugin : CordaPluginRegistry() {
override val requiredFlows = mapOf(Pair(FixingFlow.FixingRoleDecider::class.java.name, setOf(Duration::class.java.name, StateRef::class.java.name)))
override val servicePlugins = listOf(Function(::Service)) override val servicePlugins = listOf(Function(::Service))
} }

View File

@ -5,6 +5,7 @@ import net.corda.core.contracts.DealState
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.CordaPluginRegistry
import net.corda.core.node.PluginServiceHub import net.corda.core.node.PluginServiceHub
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
@ -37,6 +38,7 @@ object AutoOfferFlow {
} }
@InitiatingFlow @InitiatingFlow
@StartableByRPC
class Requester(val dealToBeOffered: DealState) : FlowLogic<SignedTransaction>() { class Requester(val dealToBeOffered: DealState) : FlowLogic<SignedTransaction>() {
companion object { companion object {

View File

@ -8,6 +8,7 @@ import net.corda.core.crypto.keys
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.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.SchedulableFlow
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.PluginServiceHub import net.corda.core.node.PluginServiceHub
import net.corda.core.node.services.ServiceType import net.corda.core.node.services.ServiceType
@ -136,6 +137,7 @@ object FixingFlow {
* Fixer role is chosen, then that will be initiated by the [FixingSession] message sent from the other party. * Fixer role is chosen, then that will be initiated by the [FixingSession] message sent from the other party.
*/ */
@InitiatingFlow @InitiatingFlow
@SchedulableFlow
class FixingRoleDecider(val ref: StateRef, override val progressTracker: ProgressTracker) : FlowLogic<Unit>() { class FixingRoleDecider(val ref: StateRef, override val progressTracker: ProgressTracker) : FlowLogic<Unit>() {
@Suppress("unused") // Used via reflection. @Suppress("unused") // Used via reflection.
constructor(ref: StateRef) : this(ref, tracker()) constructor(ref: StateRef) : this(ref, tracker())

View File

@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.node.CordaPluginRegistry 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.node.PluginServiceHub
@ -44,6 +45,7 @@ object UpdateBusinessDayFlow {
@InitiatingFlow @InitiatingFlow
@StartableByRPC
class Broadcast(val date: LocalDate, override val progressTracker: ProgressTracker) : FlowLogic<Unit>() { class Broadcast(val date: LocalDate, override val progressTracker: ProgressTracker) : FlowLogic<Unit>() {
constructor(date: LocalDate) : this(date, tracker()) constructor(date: LocalDate) : this(date, tracker())

View File

@ -1,15 +1,9 @@
package net.corda.irs.plugin package net.corda.irs.plugin
import net.corda.core.contracts.StateRef
import net.corda.core.identity.Party import net.corda.core.identity.Party
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.contract.InterestRateSwap
import net.corda.irs.flows.AutoOfferFlow
import net.corda.irs.flows.FixingFlow import net.corda.irs.flows.FixingFlow
import net.corda.irs.flows.UpdateBusinessDayFlow
import java.time.Duration
import java.time.LocalDate
import java.util.function.Function import java.util.function.Function
class IRSPlugin : CordaPluginRegistry() { class IRSPlugin : CordaPluginRegistry() {
@ -18,9 +12,4 @@ class IRSPlugin : CordaPluginRegistry() {
"irsdemo" to javaClass.classLoader.getResource("irsweb").toExternalForm() "irsdemo" to javaClass.classLoader.getResource("irsweb").toExternalForm()
) )
override val servicePlugins = listOf(Function(FixingFlow::Service)) override val servicePlugins = listOf(Function(FixingFlow::Service))
override val requiredFlows: Map<String, Set<String>> = mapOf(
AutoOfferFlow.Requester::class.java.name to setOf(InterestRateSwap.State::class.java.name),
UpdateBusinessDayFlow.Broadcast::class.java.name to setOf(LocalDate::class.java.name),
FixingFlow.FixingRoleDecider::class.java.name to setOf(StateRef::class.java.name, Duration::class.java.name),
FixingFlow.Floater::class.java.name to setOf(Party::class.java.name, FixingFlow.FixingSession::class.java.name))
} }

View File

@ -1,15 +0,0 @@
package net.corda.notarydemo.plugin
import net.corda.core.identity.Party
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.transactions.SignedTransaction
import net.corda.flows.NotaryFlow
import net.corda.notarydemo.flows.DummyIssueAndMove
class NotaryDemoPlugin : CordaPluginRegistry() {
// A list of protocols that are required for this cordapp
override val requiredFlows = mapOf(
NotaryFlow.Client::class.java.name to setOf(SignedTransaction::class.java.name, setOf(Unit).javaClass.name),
DummyIssueAndMove::class.java.name to setOf(Party::class.java.name)
)
}

View File

@ -1,2 +0,0 @@
# Register a ServiceLoader service extending from net.corda.node.CordaPluginRegistry
net.corda.notarydemo.plugin.NotaryDemoPlugin

View File

@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.node.PluginServiceHub 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
@ -24,6 +25,7 @@ object IRSTradeFlow {
data class OfferMessage(val notary: Party, val dealBeingOffered: IRSState) data class OfferMessage(val notary: Party, val dealBeingOffered: IRSState)
@InitiatingFlow @InitiatingFlow
@StartableByRPC
class Requester(val swap: SwapData, val otherParty: Party) : FlowLogic<SignedTransaction>() { class Requester(val swap: SwapData, val otherParty: Party) : FlowLogic<SignedTransaction>() {
@Suspendable @Suspendable
override fun call(): SignedTransaction { override fun call(): SignedTransaction {

View File

@ -14,6 +14,7 @@ import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.node.PluginServiceHub import net.corda.core.node.PluginServiceHub
import net.corda.core.node.services.dealsWith import net.corda.core.node.services.dealsWith
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
@ -51,6 +52,7 @@ object SimmFlow {
* margin using SIMM. If there is an existing state it will update and revalue the portfolio agreement. * margin using SIMM. If there is an existing state it will update and revalue the portfolio agreement.
*/ */
@InitiatingFlow @InitiatingFlow
@StartableByRPC
class Requester(val otherParty: Party, class Requester(val otherParty: Party,
val valuationDate: LocalDate, val valuationDate: LocalDate,
val existing: StateAndRef<PortfolioState>?) val existing: StateAndRef<PortfolioState>?)

View File

@ -3,6 +3,8 @@ package net.corda.vega.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
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.SchedulableFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.node.services.linearHeadsOfType import net.corda.core.node.services.linearHeadsOfType
import net.corda.vega.contracts.PortfolioState import net.corda.vega.contracts.PortfolioState
import java.time.LocalDate import java.time.LocalDate
@ -12,6 +14,8 @@ import java.time.LocalDate
* requirements * requirements
*/ */
object SimmRevaluation { object SimmRevaluation {
@StartableByRPC
@SchedulableFlow
class Initiator(val curStateRef: StateRef, val valuationDate: LocalDate) : FlowLogic<Unit>() { class Initiator(val curStateRef: StateRef, val valuationDate: LocalDate) : FlowLogic<Unit>() {
@Suspendable @Suspendable
override fun call(): Unit { override fun call(): Unit {

View File

@ -10,18 +10,14 @@ import com.opengamma.strata.market.curve.CurveName
import com.opengamma.strata.market.param.CurrencyParameterSensitivities import com.opengamma.strata.market.param.CurrencyParameterSensitivities
import com.opengamma.strata.market.param.CurrencyParameterSensitivity import com.opengamma.strata.market.param.CurrencyParameterSensitivity
import com.opengamma.strata.market.param.TenorDateParameterMetadata import com.opengamma.strata.market.param.TenorDateParameterMetadata
import net.corda.core.contracts.StateRef
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.CordaPluginRegistry
import net.corda.core.serialization.SerializationCustomization 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.contracts.SwapData
import net.corda.vega.flows.IRSTradeFlow import net.corda.vega.flows.IRSTradeFlow
import net.corda.vega.flows.SimmFlow import net.corda.vega.flows.SimmFlow
import net.corda.vega.flows.SimmRevaluation
import java.time.LocalDate
import java.util.function.Function import java.util.function.Function
/** /**
@ -32,10 +28,6 @@ import java.util.function.Function
object SimmService { object SimmService {
class Plugin : CordaPluginRegistry() { class Plugin : CordaPluginRegistry() {
override val webApis = listOf(Function(::PortfolioApi)) override val webApis = listOf(Function(::PortfolioApi))
override val requiredFlows: Map<String, Set<String>> = mapOf(
SimmFlow.Requester::class.java.name to setOf(Party::class.java.name, LocalDate::class.java.name),
SimmRevaluation.Initiator::class.java.name to setOf(StateRef::class.java.name, LocalDate::class.java.name),
IRSTradeFlow.Requester::class.java.name to setOf(SwapData::class.java.name, Party::class.java.name))
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 val servicePlugins = listOf(Function(SimmFlow::Service), Function(IRSTradeFlow::Service))
override fun customizeSerialization(custom: SerializationCustomization): Boolean { override fun customizeSerialization(custom: SerializationCustomization): Boolean {

View File

@ -10,6 +10,7 @@ import net.corda.core.crypto.generateKeyPair
import net.corda.core.days import net.corda.core.days
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.seconds import net.corda.core.seconds
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
@ -22,6 +23,7 @@ import java.time.Instant
import java.util.* import java.util.*
@InitiatingFlow @InitiatingFlow
@StartableByRPC
class SellerFlow(val otherParty: Party, class SellerFlow(val otherParty: Party,
val amount: Amount<Currency>, val amount: Amount<Currency>,
override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() { override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() {

View File

@ -1,16 +1,10 @@
package net.corda.traderdemo.plugin package net.corda.traderdemo.plugin
import net.corda.core.contracts.Amount
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.CordaPluginRegistry
import net.corda.traderdemo.flow.BuyerFlow import net.corda.traderdemo.flow.BuyerFlow
import net.corda.traderdemo.flow.SellerFlow
import java.util.function.Function import java.util.function.Function
class TraderDemoPlugin : CordaPluginRegistry() { class TraderDemoPlugin : CordaPluginRegistry() {
// A list of Flows that are required for this cordapp
override val requiredFlows: Map<String, Set<String>> = mapOf(
SellerFlow::class.java.name to setOf(Party::class.java.name, Amount::class.java.name)
)
override val servicePlugins = listOf(Function(BuyerFlow::Service)) override val servicePlugins = listOf(Function(BuyerFlow::Service))
} }

View File

@ -1,14 +1,10 @@
package net.corda.explorer.plugin package net.corda.explorer.plugin
import net.corda.core.contracts.Amount
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.CordaPluginRegistry
import net.corda.core.serialization.OpaqueBytes
import net.corda.flows.IssuerFlow import net.corda.flows.IssuerFlow
import java.util.function.Function import java.util.function.Function
class ExplorerPlugin : CordaPluginRegistry() { class ExplorerPlugin : CordaPluginRegistry() {
// A list of flow that are required for this cordapp
override val requiredFlows: Map<String, Set<String>> = mapOf(IssuerFlow.IssuanceRequester::class.java.name to setOf(Amount::class.java.name, Party::class.java.name, OpaqueBytes::class.java.name, Party::class.java.name))
override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service)) override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service))
} }

View File

@ -30,14 +30,6 @@ class CorDappInfoServlet(val plugins: List<CordaPluginRegistry>, val rpc: CordaR
} else { } else {
plugins.forEach { plugin -> plugins.forEach { plugin ->
h3 { +plugin::class.java.name } h3 { +plugin::class.java.name }
if (plugin.requiredFlows.isNotEmpty()) {
div {
p { +"Whitelisted flows:" }
ul {
plugin.requiredFlows.map { it.key }.forEach { li { +it } }
}
}
}
if (plugin.webApis.isNotEmpty()) { if (plugin.webApis.isNotEmpty()) {
div { div {
plugin.webApis.forEach { api -> plugin.webApis.forEach { api ->
@ -56,7 +48,7 @@ class CorDappInfoServlet(val plugins: List<CordaPluginRegistry>, val rpc: CordaR
div { div {
p { +"Static web content:" } p { +"Static web content:" }
ul { ul {
plugin.staticServeDirs.map { it.key }.forEach { plugin.staticServeDirs.keys.forEach {
li { a("web/$it") { +it } } li { a("web/$it") { +it } }
} }
} }