mirror of
https://github.com/corda/corda.git
synced 2025-05-02 08:43:15 +00:00
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:
parent
b155764023
commit
48f58b6dbc
@ -9,11 +9,12 @@ import net.corda.core.serialization.CordaSerializable
|
||||
* the flow to run at the scheduled time.
|
||||
*/
|
||||
interface FlowLogicRefFactory {
|
||||
fun create(type: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef
|
||||
fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef
|
||||
}
|
||||
|
||||
@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.
|
||||
|
14
core/src/main/kotlin/net/corda/core/flows/SchedulableFlow.kt
Normal file
14
core/src/main/kotlin/net/corda/core/flows/SchedulableFlow.kt
Normal 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
|
15
core/src/main/kotlin/net/corda/core/flows/StartableByRPC.kt
Normal file
15
core/src/main/kotlin/net/corda/core/flows/StartableByRPC.kt
Normal 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
|
@ -148,14 +148,14 @@ interface CordaRPCOps : RPCOps {
|
||||
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
|
||||
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
|
||||
* result of running the flow.
|
||||
* result of running the flow. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC].
|
||||
*/
|
||||
@RPCReturnsObservables
|
||||
fun <T : Any> startTrackedFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowProgressHandle<T>
|
||||
|
@ -8,42 +8,38 @@ import java.util.function.Function
|
||||
* 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.
|
||||
*/
|
||||
abstract class CordaPluginRegistry(
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
open val webApis: List<Function<CordaRPCOps, out Any>> = emptyList(),
|
||||
abstract class CordaPluginRegistry {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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 "\*.
|
||||
* 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()
|
||||
*/
|
||||
open val staticServeDirs: Map<String, String> = emptyMap(),
|
||||
/**
|
||||
* 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
|
||||
* be specified with: javaClass.getResource("<folder-in-jar>").toExternalForm()
|
||||
*/
|
||||
open val staticServeDirs: Map<String, String> get() = emptyMap()
|
||||
|
||||
/**
|
||||
* A Map with an entry for each consumed Flow used by the webAPIs.
|
||||
* The key of each map entry should contain the FlowLogic<T> class name.
|
||||
* The associated map values are the union of all concrete class names passed to the Flow constructor.
|
||||
* 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(),
|
||||
@Suppress("unused")
|
||||
@Deprecated("This is no longer needed. Instead annotate any flows that need to be invoked via RPC with " +
|
||||
"@StartableByRPC and any scheduled flows with @SchedulableFlow", level = DeprecationLevel.ERROR)
|
||||
open val requiredFlows: Map<String, Set<String>> get() = emptyMap()
|
||||
|
||||
/**
|
||||
* List of lambdas constructing additional long lived services to be hosted within the node.
|
||||
* They expect a single [PluginServiceHub] parameter as input.
|
||||
* 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.
|
||||
*/
|
||||
open val servicePlugins: List<Function<PluginServiceHub, out Any>> get() = emptyList()
|
||||
|
||||
/**
|
||||
* List of lambdas constructing additional long lived services to be hosted within the node.
|
||||
* They expect a single [PluginServiceHub] parameter as input.
|
||||
* 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.
|
||||
*/
|
||||
open val servicePlugins: List<Function<PluginServiceHub, out Any>> = emptyList()
|
||||
) {
|
||||
/**
|
||||
* 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
|
||||
* adding the @CordaSerializable annotation or via this method.
|
||||
* For example, if you add a new [net.corda.core.contracts.ContractState] it needs to be whitelisted. You can do that
|
||||
* 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.
|
||||
*/
|
||||
|
@ -2,6 +2,7 @@ package net.corda.flows
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
@ -15,6 +16,7 @@ import java.security.PublicKey
|
||||
* use the new updated state for future transactions.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState>(
|
||||
originalState: StateAndRef<OldState>,
|
||||
newContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
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.node.services.unconsumedStates
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.Emoji
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.ContractUpgradeFlow
|
||||
@ -27,7 +29,6 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
@ -69,10 +70,10 @@ class ContractUpgradeFlowTest {
|
||||
requireNotNull(atx)
|
||||
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
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith(ExecutionException::class) { rejectedFuture.get() }
|
||||
assertFailsWith(FlowSessionException::class) { rejectedFuture.getOrThrow() }
|
||||
|
||||
// Party B authorise the contract state upgrade.
|
||||
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
|
||||
mockNet.runNetwork()
|
||||
|
||||
val result = resultFuture.get()
|
||||
val result = resultFuture.getOrThrow()
|
||||
|
||||
fun check(node: MockNetwork.MockNode) {
|
||||
val nodeStx = node.database.transaction {
|
||||
@ -124,12 +125,12 @@ class ContractUpgradeFlowTest {
|
||||
.toSignedTransaction()
|
||||
|
||||
val user = rpcTestUser.copy(permissions = setOf(
|
||||
startFlowPermission<FinalityFlow>(),
|
||||
startFlowPermission<FinalityInvoker>(),
|
||||
startFlowPermission<ContractUpgradeFlow<*, *>>()
|
||||
))
|
||||
val rpcA = startProxy(a, 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()
|
||||
handle.returnValue.getOrThrow()
|
||||
|
||||
@ -143,7 +144,7 @@ class ContractUpgradeFlowTest {
|
||||
DummyContractV2::class.java).returnValue
|
||||
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith(ExecutionException::class) { rejectedFuture.get() }
|
||||
assertFailsWith(FlowSessionException::class) { rejectedFuture.getOrThrow() }
|
||||
|
||||
// Party B authorise the contract state upgrade.
|
||||
rpcB.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)
|
||||
@ -154,7 +155,7 @@ class ContractUpgradeFlowTest {
|
||||
DummyContractV2::class.java).returnValue
|
||||
|
||||
mockNet.runNetwork()
|
||||
val result = resultFuture.get()
|
||||
val result = resultFuture.getOrThrow()
|
||||
// Check results.
|
||||
listOf(a, b).forEach {
|
||||
val signedTX = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) }
|
||||
@ -210,4 +211,11 @@ class ContractUpgradeFlowTest {
|
||||
// Dummy Cash contract for testing.
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,11 @@ UNRELEASED
|
||||
----------
|
||||
|
||||
* API changes:
|
||||
* Initiating flows (i.e. those which initiate flows in a counterparty) are now required to be annotated with
|
||||
``InitiatingFlow``.
|
||||
* ``CordaPluginRegistry.requiredFlows`` is no longer needed. Instead annotate any flows you wish to start via RPC with
|
||||
``@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
|
||||
marker Class restricted to ``FlowLogic``. In line with the introduction of ``InitiatingFlow``, it throws an
|
||||
|
@ -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
|
||||
bundled web server is not started.
|
||||
|
||||
c. The ``requiredFlows`` property is used to declare new protocols in
|
||||
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
|
||||
c. The ``servicePlugins`` property returns a list of classes which will
|
||||
be instantiated once during the ``AbstractNode.start`` call. These
|
||||
classes must provide a single argument constructor which will receive a
|
||||
``PluginServiceHub`` reference. They must also extend the abstract class
|
||||
@ -90,7 +78,7 @@ extensions to be created, or registered at startup. In particular:
|
||||
functions inside the node, for instance to initiate workflows when
|
||||
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``
|
||||
annotation. In general the annotation should be preferred. For
|
||||
instance new state types will need to be explicitly registered. This will be called at
|
||||
|
@ -12,11 +12,10 @@ App plugins
|
||||
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:
|
||||
|
||||
1. Required flows: Specify which flows will be whitelisted for use in your RPC calls.
|
||||
2. Service plugins: Register your services (see below).
|
||||
3. Web APIs: You may register your own endpoints under /api/ of the bundled web server.
|
||||
4. Static web endpoints: You may register your own static serving directories for serving web content from the web server.
|
||||
5. Whitelisting your additional contract, state and other classes for object serialization. Any class that forms part
|
||||
1. Service plugins: Register your services (see below).
|
||||
2. Web APIs: You may register your own endpoints under /api/ of the bundled web server.
|
||||
3. Static web endpoints: You may register your own static serving directories for serving web content from the web server.
|
||||
4. Whitelisting your additional contract, state and other classes for object serialization. Any class that forms part
|
||||
of a persisted state, that is used in messaging between flows or in RPC needs to be whitelisted.
|
||||
|
||||
Services
|
||||
|
@ -42,7 +42,8 @@ There are two main steps to implementing scheduled events:
|
||||
``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
|
||||
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.
|
||||
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
|
||||
@ -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
|
||||
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
|
||||
instantiate. The reference can be checked against a per-node whitelist of approved and allowable types as
|
||||
part of our overall security sandboxing.
|
||||
|
||||
.. note:: This is a way to create a reference to the FlowLogic class and its constructor parameters to instantiate.
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
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,
|
||||
see ":doc:`event-scheduling`" to learn more about this. Or they can be triggered directly via the Java-level node RPC
|
||||
APIs from your app code.
|
||||
Flows can be invoked in several ways. For instance, they can be triggered by scheduled events (in which case they need to
|
||||
be annotated with ``@SchedulableFlow``), see ":doc:`event-scheduling`" to learn more about this. They can also be triggered
|
||||
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
|
||||
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
|
||||
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
|
||||
having one side initiate communication and the other respond to it and start their flow. Initiation is typically done using
|
||||
RPC with the ``startFlowDynamic`` method. The initiating flow has be to annotated with ``InitiatingFlow``. In our example
|
||||
it doesn't matter which flow is the initiator and which is the initiated, which is why neither ``Buyer`` nor ``Seller``
|
||||
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:
|
||||
Before going any further it will be useful to describe how flows communicate with each other. A node may have many flows
|
||||
running at the same time, and perhaps communicating with the same counterparty node but for different purposes. Therefore
|
||||
flows need a way to segregate communication channels so that concurrent conversations between flows on the same set of nodes
|
||||
do not interfere with each other.
|
||||
|
||||
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
|
||||
|
||||
|
@ -3,13 +3,13 @@
|
||||
Client RPC API tutorial
|
||||
=======================
|
||||
|
||||
In this tutorial we will build a simple command line utility that
|
||||
connects to a node, creates some Cash transactions and meanwhile dumps
|
||||
the transaction graph to the standard output. We will then put some
|
||||
simple visualisation on top. For an explanation on how the RPC works
|
||||
see :doc:`clientrpc`.
|
||||
In this tutorial we will build a simple command line utility that connects to a node, creates some Cash transactions and
|
||||
meanwhile dumps the transaction graph to the standard output. We will then put some 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.
|
||||
|
||||
@ -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
|
||||
: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
|
||||
:language: kotlin
|
||||
:start-after: interface CordaRPCOps
|
||||
: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
|
||||
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
|
||||
: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.
|
||||
|
||||
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
|
||||
|
||||
@ -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
|
||||
authenticated user is entitled to run).
|
||||
|
||||
.. note:: Permissions are represented as *String's* to allow RPC implementations to add their own permissioning.
|
||||
Currently the only permission type defined is *StartFlow*, which defines a list of whitelisted flows an authenticated use may execute.
|
||||
An administrator user (or a developer) may also be assigned the ``ALL`` permission, which grants access to any flow.
|
||||
.. note:: Permissions are represented as *String's* to allow RPC implementations to add their own permissioning. Currently
|
||||
the only permission type defined is *StartFlow*, which defines a list of whitelisted flows an authenticated use may
|
||||
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:
|
||||
|
||||
@ -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 ] }
|
||||
]
|
||||
|
||||
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
|
||||
|
||||
@ -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 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
|
||||
Cordformation in :doc:`creating-a-cordapp`
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.TransactionType
|
||||
import net.corda.core.contracts.issuedBy
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
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 notary the notary to set on the output states.
|
||||
*/
|
||||
@StartableByRPC
|
||||
class CashIssueFlow(val amount: Amount<Currency>,
|
||||
val issueRef: OpaqueBytes,
|
||||
val recipient: Party,
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.contracts.InsufficientBalanceException
|
||||
import net.corda.core.contracts.TransactionType
|
||||
import net.corda.core.crypto.expandedCompositeKeys
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
@ -20,6 +21,7 @@ import java.util.*
|
||||
* @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.
|
||||
*/
|
||||
@StartableByRPC
|
||||
open class CashPaymentFlow(
|
||||
val amount: Amount<Currency>,
|
||||
val recipient: Party,
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.node.PluginServiceHub
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
@ -30,6 +31,7 @@ object IssuerFlow {
|
||||
* Returns the transaction created by the Issuer to move the cash to the Requester.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class IssuanceRequester(val amount: Amount<Currency>, val issueToParty: Party, val issueToPartyRef: OpaqueBytes,
|
||||
val issuerBankParty: Party) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
|
@ -150,6 +150,9 @@ dependencies {
|
||||
// Requery: object mapper for Kotlin
|
||||
compile "io.requery:requery-kotlin:$requery_version"
|
||||
|
||||
// FastClasspathScanner: classpath scanning
|
||||
compile 'io.github.lukehutch:fast-classpath-scanner:2.0.19'
|
||||
|
||||
// Integration test helpers
|
||||
integrationTestCompile "junit:junit:$junit_version"
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
package net.corda.node
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.div
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.utilities.ALICE
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.startFlowPermission
|
||||
@ -48,18 +49,12 @@ class BootTests {
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class ObjectInputStreamFlow : FlowLogic<Unit>() {
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
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()
|
||||
ObjectInputStream(data.inputStream()).use { it.readObject() }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class BootTestsPlugin : CordaPluginRegistry() {
|
||||
|
||||
override val requiredFlows: Map<String, Set<String>> = mapOf(ObjectInputStreamFlow::class.java.name to setOf())
|
||||
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
net.corda.node.BootTestsPlugin
|
@ -5,15 +5,16 @@ import com.google.common.annotations.VisibleForTesting
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
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.contracts.Amount
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.crypto.KeyStoreUtilities
|
||||
import net.corda.core.crypto.X509Utilities
|
||||
import net.corda.core.crypto.replaceCommonName
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
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.services.*
|
||||
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.deserialize
|
||||
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.schema.HibernateObserver
|
||||
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.StateMachineManager
|
||||
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.slf4j.Logger
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.Modifier.*
|
||||
import java.net.URL
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyStoreException
|
||||
import java.time.Clock
|
||||
@ -73,6 +75,7 @@ import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.reflect.KClass
|
||||
import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
|
||||
|
||||
@ -93,14 +96,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
companion object {
|
||||
val PRIVATE_KEY_FILE_NAME = "identity-private-key"
|
||||
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.
|
||||
@ -133,10 +128,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
override val schemaService: SchemaService get() = schemas
|
||||
override val transactionVerifierService: TransactionVerifierService get() = txVerifierService
|
||||
override val auditService: AuditService get() = auditService
|
||||
override val rpcFlows: List<Class<out FlowLogic<*>>> get() = this@AbstractNode.rpcFlows
|
||||
|
||||
// Internal only
|
||||
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
||||
override val flowLogicRefFactory: FlowLogicRefFactoryInternal get() = flowLogicFactory
|
||||
|
||||
override fun <T> startFlow(logic: FlowLogic<T>, flowInitiator: FlowInitiator): FlowStateMachineImpl<T> {
|
||||
return serverThread.fetchFrom { smm.add(logic, flowInitiator) }
|
||||
@ -176,13 +171,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
lateinit var net: MessagingService
|
||||
lateinit var netMapCache: NetworkMapCacheInternal
|
||||
lateinit var scheduler: NodeSchedulerService
|
||||
lateinit var flowLogicFactory: FlowLogicRefFactoryInternal
|
||||
lateinit var schemas: SchemaService
|
||||
lateinit var auditService: AuditService
|
||||
val customServices: ArrayList<Any> = ArrayList()
|
||||
protected val runOnStop: ArrayList<Runnable> = ArrayList()
|
||||
lateinit var database: Database
|
||||
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. */
|
||||
inline fun <reified T : Any> findService() = customServices.filterIsInstance<T>().single()
|
||||
@ -250,6 +245,16 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
}
|
||||
startMessagingService(rpcOps)
|
||||
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() }
|
||||
_networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured())
|
||||
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 identity key. But the infrastructure to make that easy isn't here yet.
|
||||
keyManagement = makeKeyManagementService()
|
||||
flowLogicFactory = initialiseFlowLogicFactory()
|
||||
scheduler = NodeSchedulerService(services, database, flowLogicFactory, unfinishedSchedules = busyNodeLatch)
|
||||
scheduler = NodeSchedulerService(services, database, unfinishedSchedules = busyNodeLatch)
|
||||
|
||||
val tokenizableServices = mutableListOf(storage, net, vault, keyManagement, identity, platformClock, scheduler)
|
||||
makeAdvertisedServices(tokenizableServices)
|
||||
@ -318,6 +322,53 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
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>) {
|
||||
val uploaders: List<FileUploader> = listOf(storageServices.first.attachments as NodeAttachmentService) +
|
||||
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> {
|
||||
val pluginServices = pluginRegistries.flatMap { it.servicePlugins }.map { it.apply(services) }
|
||||
tokenizableServices.addAll(pluginServices)
|
||||
|
@ -7,6 +7,7 @@ import net.corda.core.contracts.UpgradedContract
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.NodeInfo
|
||||
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.requirePermission
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
import net.corda.node.utilities.transaction
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
@ -39,8 +41,6 @@ class CordaRPCOpsImpl(
|
||||
private val smm: StateMachineManager,
|
||||
private val database: Database
|
||||
) : CordaRPCOps {
|
||||
override val protocolVersion: Int = 0
|
||||
|
||||
override fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>> {
|
||||
return database.transaction {
|
||||
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> {
|
||||
val rpcContext = getRpcContext()
|
||||
rpcContext.requirePermission(startFlowPermission(logicType))
|
||||
val currentUser = FlowInitiator.RPC(rpcContext.currentUser.username)
|
||||
val stateMachine = services.invokeFlowAsync(logicType, currentUser, *args)
|
||||
val stateMachine = startFlow(logicType, args)
|
||||
return FlowProgressHandleImpl(
|
||||
id = stateMachine.id,
|
||||
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> {
|
||||
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()
|
||||
rpcContext.requirePermission(startFlowPermission(logicType))
|
||||
val currentUser = FlowInitiator.RPC(rpcContext.currentUser.username)
|
||||
val stateMachine = services.invokeFlowAsync(logicType, currentUser, *args)
|
||||
return FlowHandleImpl(id = stateMachine.id, returnValue = stateMachine.resultFuture)
|
||||
return services.invokeFlowAsync(logicType, currentUser, *args)
|
||||
}
|
||||
|
||||
override fun attachmentExists(id: SecureHash): Boolean {
|
||||
@ -171,11 +171,12 @@ class CordaRPCOpsImpl(
|
||||
|
||||
override fun waitUntilRegisteredWithNetworkMap() = services.networkMapCache.mapServiceRegistered
|
||||
override fun partyFromKey(key: PublicKey) = services.identityService.partyFromKey(key)
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use partyFromX500Name instead")
|
||||
override fun partyFromName(name: String) = services.identityService.partyFromName(name)
|
||||
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 {
|
||||
private fun stateMachineInfoFromFlowLogic(flowLogic: FlowLogic<*>): StateMachineInfo {
|
||||
|
@ -2,7 +2,9 @@ package net.corda.node.services.api
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
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.node.NodeInfo
|
||||
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.node.internal.ServiceFlowInfo
|
||||
import net.corda.node.services.messaging.MessagingService
|
||||
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
|
||||
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
|
||||
sealed class NetworkCacheError : Exception() {
|
||||
/** 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 flowLogicRefFactory: FlowLogicRefFactoryInternal
|
||||
abstract val schemaService: SchemaService
|
||||
abstract override val networkMapCache: NetworkMapCacheInternal
|
||||
abstract val schedulerService: SchedulerService
|
||||
abstract val auditService: AuditService
|
||||
|
||||
abstract val rpcFlows: List<Class<out FlowLogic<*>>>
|
||||
abstract val networkService: MessagingService
|
||||
|
||||
/**
|
||||
@ -119,9 +116,9 @@ abstract class ServiceHubInternal : PluginServiceHub {
|
||||
logicType: Class<out FlowLogic<T>>,
|
||||
flowInitiator: FlowInitiator,
|
||||
vararg args: Any?): FlowStateMachineImpl<T> {
|
||||
val logicRef = flowLogicRefFactory.create(logicType, *args)
|
||||
val logicRef = FlowLogicRefFactoryImpl.createForRPC(logicType, *args)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val logic = flowLogicRefFactory.toFlowLogic(logicRef) as FlowLogic<T>
|
||||
val logic = FlowLogicRefFactoryImpl.toFlowLogic(logicRef) as FlowLogic<T>
|
||||
return startFlow(logic, flowInitiator)
|
||||
}
|
||||
|
||||
|
@ -12,9 +12,9 @@ import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.then
|
||||
import net.corda.core.utilities.loggerFor
|
||||
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.ServiceHubInternal
|
||||
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
||||
import net.corda.node.utilities.*
|
||||
import org.apache.activemq.artemis.utils.ReusableLatch
|
||||
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.
|
||||
*
|
||||
* @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
|
||||
* activity. Only replace this for unit testing purposes. This is not the executor the [FlowLogic] is launched on.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class NodeSchedulerService(private val services: ServiceHubInternal,
|
||||
private val database: Database,
|
||||
private val flowLogicRefFactory: FlowLogicRefFactoryInternal,
|
||||
private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(),
|
||||
private val unfinishedSchedules: ReusableLatch = ReusableLatch())
|
||||
: SchedulerService, SingletonSerializeAsToken() {
|
||||
@ -192,7 +190,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal,
|
||||
ScheduledStateRef(scheduledState.ref, scheduledActivity.scheduledAt)
|
||||
} else {
|
||||
// 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" }
|
||||
scheduledFlow = flowLogic
|
||||
null
|
||||
@ -213,7 +211,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal,
|
||||
val state = txState.data as SchedulableState
|
||||
return try {
|
||||
// This can throw as running contract code.
|
||||
state.nextScheduledActivity(scheduledState.ref, flowLogicRefFactory)
|
||||
state.nextScheduledActivity(scheduledState.ref, FlowLogicRefFactoryImpl)
|
||||
} catch (e: Exception) {
|
||||
log.error("Attempt to run scheduled state $scheduledState resulted in error.", e)
|
||||
null
|
||||
|
@ -4,8 +4,8 @@ import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.SchedulableState
|
||||
import net.corda.core.contracts.ScheduledStateRef
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
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
|
||||
@ -13,16 +13,16 @@ import net.corda.node.services.api.ServiceHubInternal
|
||||
*/
|
||||
class ScheduledActivityObserver(val services: ServiceHubInternal) {
|
||||
init {
|
||||
services.vaultService.rawUpdates.subscribe { update ->
|
||||
update.consumed.forEach { services.schedulerService.unscheduleStateActivity(it.ref) }
|
||||
update.produced.forEach { scheduleStateActivity(it, services.flowLogicRefFactory) }
|
||||
services.vaultService.rawUpdates.subscribe { (consumed, produced) ->
|
||||
consumed.forEach { services.schedulerService.unscheduleStateActivity(it.ref) }
|
||||
produced.forEach { scheduleStateActivity(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleStateActivity(produced: StateAndRef<ContractState>, flowLogicRefFactory: FlowLogicRefFactory) {
|
||||
private fun scheduleStateActivity(produced: StateAndRef<ContractState>) {
|
||||
val producedState = produced.state.data
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,10 @@
|
||||
package net.corda.node.services.statemachine
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import com.google.common.primitives.Primitives
|
||||
import net.corda.core.crypto.SecureHash
|
||||
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.flows.*
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.node.services.api.FlowLogicRefFactoryInternal
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
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
|
||||
* 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: 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 {
|
||||
constructor() : this(mapOf())
|
||||
|
||||
// Pending real dependence on AppContext for class loading etc
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun validateFlowClassName(className: String, appContext: AppContext) {
|
||||
// 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
|
||||
@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
|
||||
object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactory {
|
||||
override fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef {
|
||||
if (!flowClass.isAnnotationPresent(SchedulableFlow::class.java)) {
|
||||
throw IllegalFlowLogicException(flowClass, "because it's not a schedulable flow")
|
||||
}
|
||||
// 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]}" }
|
||||
return createForRPC(flowClass, *args)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
fun createForRPC(flowClass: 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
|
||||
// to avoid requiring only a single constructor.
|
||||
val argTypes = args.map { it?.javaClass }
|
||||
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).
|
||||
val ctorTypes = ctor.javaConstructor!!.parameterTypes.map { Primitives.wrap(it) }
|
||||
if (argTypes.size != ctorTypes.size)
|
||||
@ -93,14 +56,14 @@ class FlowLogicRefFactoryImpl(override val flowWhitelist: Map<String, Set<String
|
||||
true
|
||||
}
|
||||
} 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) {
|
||||
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
|
||||
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.
|
||||
*/
|
||||
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,
|
||||
// perhaps as some sort of ThreadLocal style object. For now, just create an empty one.
|
||||
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
|
||||
createConstructor(appContext, type, args)
|
||||
createConstructor(type, args)
|
||||
return FlowLogicRefImpl(type.name, appContext, args)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<*> {
|
||||
fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> {
|
||||
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)
|
||||
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) {
|
||||
val params = buildParams(appContext, clazz, constructor, args) ?: continue
|
||||
val params = buildParams(constructor, args) ?: continue
|
||||
// If we get here then we matched every parameter
|
||||
return { constructor.callBy(params) }
|
||||
}
|
||||
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 usedKeys = hashSetOf<String>()
|
||||
for (parameter in constructor.parameters) {
|
||||
@ -159,7 +110,6 @@ class FlowLogicRefFactoryImpl(override val flowWhitelist: Map<String, Set<String
|
||||
// Not all args were used
|
||||
return null
|
||||
}
|
||||
params.values.forEach { if (it is Any) validateArgClassName(clazz.name, it.javaClass.name, appContext) }
|
||||
return params
|
||||
}
|
||||
|
||||
|
@ -210,7 +210,7 @@ object InteractiveShell {
|
||||
*/
|
||||
@JvmStatic
|
||||
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()) {
|
||||
output.println("No matching flow found, run 'flow list' to see your options.", Color.red)
|
||||
return
|
||||
@ -219,14 +219,12 @@ object InteractiveShell {
|
||||
matches.forEachIndexed { i, s -> output.println("${i + 1}. $s", Color.yellow) }
|
||||
return
|
||||
}
|
||||
val match = matches.single()
|
||||
val clazz = Class.forName(match)
|
||||
if (!FlowLogic::class.java.isAssignableFrom(clazz))
|
||||
throw IllegalStateException("Found a non-FlowLogic class in the whitelist? $clazz")
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val clazz = matches.single() as Class<FlowLogic<*>>
|
||||
try {
|
||||
// TODO Flow invocation should use startFlowDynamic.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val fsm = runFlowFromString({ node.services.startFlow(it, FlowInitiator.Shell) }, inputData, clazz as Class<FlowLogic<*>>)
|
||||
val fsm = runFlowFromString({ node.services.startFlow(it, FlowInitiator.Shell) }, inputData, clazz)
|
||||
// Show the progress tracker on the console until the flow completes or is interrupted with a
|
||||
// Ctrl-C keypress.
|
||||
val latch = CountDownLatch(1)
|
||||
@ -275,10 +273,7 @@ object InteractiveShell {
|
||||
var paramNamesFromConstructor: List<String>? = null
|
||||
fun getPrototype(ctor: Constructor<*>): List<String> {
|
||||
val argTypes = ctor.parameterTypes.map { it.simpleName }
|
||||
val prototype = paramNamesFromConstructor!!.zip(argTypes).map { pair ->
|
||||
val (name, type) = pair
|
||||
"$name: $type"
|
||||
}
|
||||
val prototype = paramNamesFromConstructor!!.zip(argTypes).map { (name, type) -> "$name: $type" }
|
||||
return prototype
|
||||
}
|
||||
try {
|
||||
|
@ -1,15 +1,9 @@
|
||||
package net.corda.node.services.events;
|
||||
|
||||
import net.corda.core.flows.FlowLogic;
|
||||
import net.corda.core.flows.FlowLogicRefFactory;
|
||||
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class FlowLogicRefFromJavaTest {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ -56,20 +50,11 @@ public class FlowLogicRefFromJavaTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
Map<String, Set<String>> whiteList = new HashMap<>();
|
||||
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"));
|
||||
FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoArg() {
|
||||
Map<String, Set<String>> whiteList = new HashMap<>();
|
||||
whiteList.put(JavaNoArgFlowLogic.class.getName(), new HashSet<>());
|
||||
FlowLogicRefFactory factory = new FlowLogicRefFactoryImpl(whiteList);
|
||||
factory.create(JavaNoArgFlowLogic.class);
|
||||
FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaNoArgFlowLogic.class);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
package net.corda.node
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.messaging.startFlow
|
||||
@ -35,7 +37,9 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlin.test.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CordaRPCOpsImplTest {
|
||||
|
||||
@ -118,7 +122,6 @@ class CordaRPCOpsImplTest {
|
||||
|
||||
@Test
|
||||
fun `issue and move`() {
|
||||
|
||||
rpc.startFlow(::CashIssueFlow,
|
||||
Amount(100, USD),
|
||||
OpaqueBytes(ByteArray(1, { 1 })),
|
||||
@ -225,4 +228,19 @@ class CordaRPCOpsImplTest {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import net.corda.node.serialization.NodeClock
|
||||
import net.corda.node.services.api.*
|
||||
import net.corda.node.services.messaging.MessagingService
|
||||
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.StateMachineManager
|
||||
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
||||
@ -30,7 +29,6 @@ open class MockServiceHubInternal(
|
||||
val mapCache: NetworkMapCacheInternal? = MockNetworkMapCache(),
|
||||
val scheduler: SchedulerService? = null,
|
||||
val overrideClock: Clock? = NodeClock(),
|
||||
val flowFactory: FlowLogicRefFactoryInternal? = FlowLogicRefFactoryImpl(),
|
||||
val schemas: SchemaService? = NodeSchemaService(),
|
||||
val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2)
|
||||
) : ServiceHubInternal() {
|
||||
@ -56,8 +54,8 @@ open class MockServiceHubInternal(
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
||||
override val flowLogicRefFactory: FlowLogicRefFactoryInternal
|
||||
get() = flowFactory ?: throw UnsupportedOperationException()
|
||||
override val rpcFlows: List<Class<out FlowLogic<*>>>
|
||||
get() = throw UnsupportedOperationException()
|
||||
override val schemaService: SchemaService
|
||||
get() = schemas ?: throw UnsupportedOperationException()
|
||||
override val auditService: AuditService = DummyAuditService()
|
||||
|
@ -1,9 +1,8 @@
|
||||
package net.corda.node.services.events
|
||||
|
||||
import net.corda.core.days
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.IllegalFlowLogicException
|
||||
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.time.Duration
|
||||
|
||||
@ -31,67 +30,51 @@ class FlowLogicRefTest {
|
||||
override fun call() = Unit
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER") // We will never use A or b
|
||||
class NotWhiteListedKotlinFlowLogic(A: Int, b: String) : FlowLogic<Unit>() {
|
||||
class NonSchedulableFlow : FlowLogic<Unit>() {
|
||||
override fun call() = Unit
|
||||
}
|
||||
|
||||
lateinit var factory: FlowLogicRefFactoryImpl
|
||||
|
||||
@Before
|
||||
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
|
||||
fun `create kotlin no arg`() {
|
||||
FlowLogicRefFactoryImpl.createForRPC(KotlinNoArgFlowLogic::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCreateKotlinNoArg() {
|
||||
factory.create(KotlinNoArgFlowLogic::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCreateKotlin() {
|
||||
fun `create kotlin`() {
|
||||
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
|
||||
fun testCreatePrimary() {
|
||||
factory.create(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack"))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testCreateNotWhiteListed() {
|
||||
factory.create(NotWhiteListedKotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack"))
|
||||
fun `create primary`() {
|
||||
FlowLogicRefFactoryImpl.createForRPC(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCreateKotlinVoid() {
|
||||
factory.createKotlin(KotlinFlowLogic::class.java, emptyMap())
|
||||
fun `create kotlin void`() {
|
||||
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, emptyMap())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCreateKotlinNonPrimary() {
|
||||
fun `create kotlin non primary`() {
|
||||
val args = mapOf(Pair("C", ParamType2("Hello Jack")))
|
||||
factory.createKotlin(KotlinFlowLogic::class.java, args)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testCreateArgNotWhiteListed() {
|
||||
val args = mapOf(Pair("illegal", 1.days))
|
||||
factory.createKotlin(KotlinFlowLogic::class.java, args)
|
||||
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCreateJavaPrimitiveNoRegistrationRequired() {
|
||||
fun `create java primitive no registration required`() {
|
||||
val args = mapOf(Pair("primitive", "A string"))
|
||||
factory.createKotlin(KotlinFlowLogic::class.java, args)
|
||||
FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCreateKotlinPrimitiveNoRegistrationRequired() {
|
||||
fun `create kotlin primitive no registration required`() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -44,10 +44,6 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
|
||||
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 scheduler: NodeSchedulerService
|
||||
@ -82,12 +78,16 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
database.transaction {
|
||||
val kms = MockKeyManagementService(ALICE_KEY)
|
||||
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 {
|
||||
override val vaultService: VaultService = NodeVaultService(this, dataSourceProps)
|
||||
override val testReference = this@NodeSchedulerServiceTest
|
||||
}
|
||||
scheduler = NodeSchedulerService(services, database, factory, schedulerGatedExecutor)
|
||||
scheduler = NodeSchedulerService(services, database, schedulerGatedExecutor)
|
||||
smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1)
|
||||
val mockSMM = StateMachineManager(services, listOf(services, scheduler), DBCheckpointStorage(), smmExecutor, database)
|
||||
mockSMM.changes.subscribe { change ->
|
||||
@ -269,7 +269,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
database.transaction {
|
||||
apply {
|
||||
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 {
|
||||
addOutputState(state, DUMMY_NOTARY)
|
||||
addCommand(Command(), freshKey.public)
|
||||
|
@ -7,7 +7,7 @@ import net.corda.core.crypto.containsAny
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.flows.FlowLogic
|
||||
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.linearHeadsOfType
|
||||
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.statemachine.StateMachineManager
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.node.utilities.AddOrRemove
|
||||
import net.corda.node.utilities.transaction
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
@ -68,6 +67,7 @@ class ScheduledFlowTests {
|
||||
}
|
||||
}
|
||||
|
||||
@SchedulableFlow
|
||||
class ScheduledFlow(val stateRef: StateRef) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
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
|
||||
fun setup() {
|
||||
net = MockNetwork(threadPerNode = true)
|
||||
@ -103,8 +95,6 @@ class ScheduledFlowTests {
|
||||
advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type)))
|
||||
nodeA = 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()
|
||||
}
|
||||
|
||||
@ -138,7 +128,7 @@ class ScheduledFlowTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Run a whole batch of scheduled flows`() {
|
||||
fun `run a whole batch of scheduled flows`() {
|
||||
val N = 100
|
||||
for (i in 0..N - 1) {
|
||||
nodeA.services.startFlow(InsertInitialStateFlow(nodeB.info.legalIdentity))
|
||||
|
@ -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_NOTARY
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
import org.junit.Test
|
||||
@ -17,11 +18,11 @@ class AttachmentDemoTest {
|
||||
@Test fun `attachment demo using a 10MB zip file`() {
|
||||
val numOfExpectedBytes = 10_000_000
|
||||
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(
|
||||
startNode(DUMMY_BANK_A.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()
|
||||
|
||||
val senderThread = CompletableFuture.supplyAsync {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.attachmentdemo
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.net.HostAndPort
|
||||
import joptsimple.OptionParser
|
||||
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.identity.Party
|
||||
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.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startTrackedFlow
|
||||
import net.corda.core.sizedInputStreamAndHash
|
||||
import net.corda.core.utilities.DUMMY_BANK_B
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.core.utilities.Emoji
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.flows.FinalityFlow
|
||||
import net.corda.node.driver.poll
|
||||
import java.io.InputStream
|
||||
@ -83,26 +84,40 @@ fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.SHA256)
|
||||
val id = rpc.uploadAttachment(it)
|
||||
require(hash == id) { "Id was '$id' instead of '$hash'" }
|
||||
}
|
||||
require(rpc.attachmentExists(hash))
|
||||
}
|
||||
|
||||
// Create a trivial transaction with an output that describes the attachment, and the attachment itself
|
||||
val ptx = TransactionType.General.Builder(notary = DUMMY_NOTARY)
|
||||
require(rpc.attachmentExists(hash))
|
||||
ptx.addOutputState(AttachmentContract.State(hash))
|
||||
ptx.addAttachment(hash)
|
||||
|
||||
// Sign with the notary key
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
|
||||
// Send the transaction to the other recipient
|
||||
val stx = ptx.toSignedTransaction()
|
||||
println("Sending ${stx.id}")
|
||||
val flowHandle = rpc.startTrackedFlow(::FinalityFlow, stx, setOf(otherSide))
|
||||
val flowHandle = rpc.startTrackedFlow(::AttachmentDemoFlow, otherSide, hash)
|
||||
flowHandle.progress.subscribe(::println)
|
||||
flowHandle.returnValue.getOrThrow()
|
||||
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
|
||||
val ptx = TransactionType.General.Builder(notary = DUMMY_NOTARY)
|
||||
ptx.addOutputState(AttachmentContract.State(hash))
|
||||
ptx.addAttachment(hash)
|
||||
|
||||
progressTracker.currentStep = SIGNING
|
||||
// Sign with the notary key
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
|
||||
// Send the transaction to the other recipient
|
||||
val stx = ptx.toSignedTransaction()
|
||||
|
||||
return subFlow(FinalityFlow(stx, setOf(otherSide))).single()
|
||||
}
|
||||
}
|
||||
|
||||
fun recipient(rpc: CordaRPCOps) {
|
||||
println("Waiting to receive transaction ...")
|
||||
val stx = rpc.verifiedTransactions().second.toBlocking().first()
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry
|
||||
net.corda.attachmentdemo.plugin.AttachmentDemoPlugin
|
@ -1,17 +1,13 @@
|
||||
package net.corda.bank.plugin
|
||||
|
||||
import net.corda.bank.api.BankOfCordaWebApi
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.flows.IssuerFlow
|
||||
import java.util.function.Function
|
||||
|
||||
class BankOfCordaPlugin : CordaPluginRegistry() {
|
||||
// A list of classes that expose web APIs.
|
||||
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))
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.irs.flows.FixingFlow
|
||||
import net.corda.irs.flows.RatesFixFlow
|
||||
import net.corda.node.services.api.AcceptsFileUpload
|
||||
import net.corda.node.utilities.AbstractJDBCHashSet
|
||||
@ -32,12 +31,14 @@ import java.io.InputStream
|
||||
import java.math.BigDecimal
|
||||
import java.security.KeyPair
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
import java.util.function.Function
|
||||
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
|
||||
@ -55,7 +56,6 @@ object NodeInterestRates {
|
||||
* Register the flow that is used with the Fixing integration tests.
|
||||
*/
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import net.corda.core.contracts.DealState
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.node.PluginServiceHub
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
@ -37,6 +38,7 @@ object AutoOfferFlow {
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class Requester(val dealToBeOffered: DealState) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
|
@ -8,6 +8,7 @@ import net.corda.core.crypto.keys
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.SchedulableFlow
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.PluginServiceHub
|
||||
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.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
@SchedulableFlow
|
||||
class FixingRoleDecider(val ref: StateRef, override val progressTracker: ProgressTracker) : FlowLogic<Unit>() {
|
||||
@Suppress("unused") // Used via reflection.
|
||||
constructor(ref: StateRef) : this(ref, tracker())
|
||||
|
@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.PluginServiceHub
|
||||
@ -44,6 +45,7 @@ object UpdateBusinessDayFlow {
|
||||
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class Broadcast(val date: LocalDate, override val progressTracker: ProgressTracker) : FlowLogic<Unit>() {
|
||||
constructor(date: LocalDate) : this(date, tracker())
|
||||
|
||||
|
@ -1,15 +1,9 @@
|
||||
package net.corda.irs.plugin
|
||||
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
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.UpdateBusinessDayFlow
|
||||
import java.time.Duration
|
||||
import java.time.LocalDate
|
||||
import java.util.function.Function
|
||||
|
||||
class IRSPlugin : CordaPluginRegistry() {
|
||||
@ -18,9 +12,4 @@ class IRSPlugin : CordaPluginRegistry() {
|
||||
"irsdemo" to javaClass.classLoader.getResource("irsweb").toExternalForm()
|
||||
)
|
||||
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))
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
# Register a ServiceLoader service extending from net.corda.node.CordaPluginRegistry
|
||||
net.corda.notarydemo.plugin.NotaryDemoPlugin
|
@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.node.PluginServiceHub
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -24,6 +25,7 @@ object IRSTradeFlow {
|
||||
data class OfferMessage(val notary: Party, val dealBeingOffered: IRSState)
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class Requester(val swap: SwapData, val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
|
@ -14,6 +14,7 @@ import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.node.PluginServiceHub
|
||||
import net.corda.core.node.services.dealsWith
|
||||
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.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class Requester(val otherParty: Party,
|
||||
val valuationDate: LocalDate,
|
||||
val existing: StateAndRef<PortfolioState>?)
|
||||
|
@ -3,6 +3,8 @@ package net.corda.vega.flows
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.StateRef
|
||||
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.vega.contracts.PortfolioState
|
||||
import java.time.LocalDate
|
||||
@ -12,6 +14,8 @@ import java.time.LocalDate
|
||||
* requirements
|
||||
*/
|
||||
object SimmRevaluation {
|
||||
@StartableByRPC
|
||||
@SchedulableFlow
|
||||
class Initiator(val curStateRef: StateRef, val valuationDate: LocalDate) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call(): Unit {
|
||||
|
@ -10,18 +10,14 @@ import com.opengamma.strata.market.curve.CurveName
|
||||
import com.opengamma.strata.market.param.CurrencyParameterSensitivities
|
||||
import com.opengamma.strata.market.param.CurrencyParameterSensitivity
|
||||
import com.opengamma.strata.market.param.TenorDateParameterMetadata
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.serialization.SerializationCustomization
|
||||
import net.corda.vega.analytics.CordaMarketData
|
||||
import net.corda.vega.analytics.InitialMarginTriple
|
||||
import net.corda.vega.api.PortfolioApi
|
||||
import net.corda.vega.contracts.SwapData
|
||||
import net.corda.vega.flows.IRSTradeFlow
|
||||
import net.corda.vega.flows.SimmFlow
|
||||
import net.corda.vega.flows.SimmRevaluation
|
||||
import java.time.LocalDate
|
||||
import java.util.function.Function
|
||||
|
||||
/**
|
||||
@ -32,10 +28,6 @@ import java.util.function.Function
|
||||
object SimmService {
|
||||
class Plugin : CordaPluginRegistry() {
|
||||
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 servicePlugins = listOf(Function(SimmFlow::Service), Function(IRSTradeFlow::Service))
|
||||
override fun customizeSerialization(custom: SerializationCustomization): Boolean {
|
||||
|
@ -10,6 +10,7 @@ import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.days
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.seconds
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -22,6 +23,7 @@ import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class SellerFlow(val otherParty: Party,
|
||||
val amount: Amount<Currency>,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() {
|
||||
|
@ -1,16 +1,10 @@
|
||||
package net.corda.traderdemo.plugin
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.traderdemo.flow.BuyerFlow
|
||||
import net.corda.traderdemo.flow.SellerFlow
|
||||
import java.util.function.Function
|
||||
|
||||
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))
|
||||
}
|
||||
|
@ -1,14 +1,10 @@
|
||||
package net.corda.explorer.plugin
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.flows.IssuerFlow
|
||||
import java.util.function.Function
|
||||
|
||||
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))
|
||||
}
|
||||
|
@ -30,14 +30,6 @@ class CorDappInfoServlet(val plugins: List<CordaPluginRegistry>, val rpc: CordaR
|
||||
} else {
|
||||
plugins.forEach { plugin ->
|
||||
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()) {
|
||||
div {
|
||||
plugin.webApis.forEach { api ->
|
||||
@ -56,7 +48,7 @@ class CorDappInfoServlet(val plugins: List<CordaPluginRegistry>, val rpc: CordaR
|
||||
div {
|
||||
p { +"Static web content:" }
|
||||
ul {
|
||||
plugin.staticServeDirs.map { it.key }.forEach {
|
||||
plugin.staticServeDirs.keys.forEach {
|
||||
li { a("web/$it") { +it } }
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user