mirror of
https://github.com/corda/corda.git
synced 2025-04-28 15:02:59 +00:00
Merge pull request #908 from corda/joel-cookbook
Flow cookbook added and merged into flow API page.
This commit is contained in:
commit
cd694b898f
@ -9,9 +9,13 @@ API: Flows
|
|||||||
|
|
||||||
.. note:: Before reading this page, you should be familiar with the key concepts of :doc:`key-concepts-flows`.
|
.. note:: Before reading this page, you should be familiar with the key concepts of :doc:`key-concepts-flows`.
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
An example flow
|
An example flow
|
||||||
---------------
|
---------------
|
||||||
Let's imagine a flow for agreeing a basic ledger update between Alice and Bob. This flow will have two sides:
|
Before we discuss the API offered by the flow, let's consider what a standard flow may look like.
|
||||||
|
|
||||||
|
Imagine a flow for agreeing a basic ledger update between Alice and Bob. This flow will have two sides:
|
||||||
|
|
||||||
* An ``Initiator`` side, that will initiate the request to update the ledger
|
* An ``Initiator`` side, that will initiate the request to update the ledger
|
||||||
* A ``Responder`` side, that will respond to the request to update the ledger
|
* A ``Responder`` side, that will respond to the request to update the ledger
|
||||||
@ -76,23 +80,42 @@ To respond to these actions, the responder takes the following steps:
|
|||||||
|
|
||||||
FlowLogic
|
FlowLogic
|
||||||
---------
|
---------
|
||||||
In practice, a flow is implemented as one or more communicating ``FlowLogic`` subclasses. Each ``FlowLogic`` subclass
|
In practice, a flow is implemented as one or more communicating ``FlowLogic`` subclasses. The ``FlowLogic``
|
||||||
must override ``FlowLogic.call()``, which describes the actions it will take as part of the flow.
|
subclass's constructor can take any number of arguments of any type. The generic of ``FlowLogic`` (e.g.
|
||||||
|
``FlowLogic<SignedTransaction>``) indicates the flow's return type.
|
||||||
|
|
||||||
So in the example above, we would have an ``Initiator`` ``FlowLogic`` subclass and a ``Responder`` ``FlowLogic``
|
.. container:: codeset
|
||||||
subclass. The actions of the initiator's side of the flow would be defined in ``Initiator.call``, and the actions
|
|
||||||
of the responder's side of the flow would be defined in ``Responder.call``.
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
|
class Initiator(val arg1: Boolean,
|
||||||
|
val arg2: Int,
|
||||||
|
val counterparty: Party): FlowLogic<SignedTransaction>() { }
|
||||||
|
|
||||||
|
class Responder(val otherParty: Party) : FlowLogic<Unit>() { }
|
||||||
|
|
||||||
|
.. sourcecode:: java
|
||||||
|
|
||||||
|
public static class Initiator extends FlowLogic<SignedTransaction> {
|
||||||
|
private final boolean arg1;
|
||||||
|
private final int arg2;
|
||||||
|
private final Party counterparty;
|
||||||
|
|
||||||
|
public Initiator(boolean arg1, int arg2, Party counterparty) {
|
||||||
|
this.arg1 = arg1;
|
||||||
|
this.arg2 = arg2;
|
||||||
|
this.counterparty = counterparty;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Responder extends FlowLogic<Void> { }
|
||||||
|
|
||||||
FlowLogic annotations
|
FlowLogic annotations
|
||||||
^^^^^^^^^^^^^^^^^^^^^
|
---------------------
|
||||||
Any flow that you wish to start either directly via RPC or as a subflow must be annotated with the
|
Any flow that you wish to start either directly via RPC or as a subflow must be annotated with the
|
||||||
``@InitiatingFlow`` annotation. Additionally, if you wish to start the flow via RPC, you must annotate it with the
|
``@InitiatingFlow`` annotation. Additionally, if you wish to start the flow via RPC, you must annotate it with the
|
||||||
``@StartableByRPC`` annotation.
|
``@StartableByRPC`` annotation:
|
||||||
|
|
||||||
Any flow that responds to a message from another flow must be annotated with the ``@InitiatedBy`` annotation.
|
|
||||||
``@InitiatedBy`` takes the class of the flow it is responding to as its single parameter.
|
|
||||||
|
|
||||||
So in our example, we would have:
|
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
@ -100,74 +123,145 @@ So in our example, we would have:
|
|||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class Initiator(): FlowLogic<Unit>() {
|
class Initiator(): FlowLogic<Unit>() { }
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
@InitiatedBy(Initiator::class)
|
|
||||||
class Responder(val otherParty: Party) : FlowLogic<Unit>() {
|
|
||||||
|
|
||||||
.. sourcecode:: java
|
.. sourcecode:: java
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
public static class Initiator extends FlowLogic<Unit> {
|
public static class Initiator extends FlowLogic<Unit> { }
|
||||||
|
|
||||||
...
|
Meanwhile, any flow that responds to a message from another flow must be annotated with the ``@InitiatedBy`` annotation.
|
||||||
|
``@InitiatedBy`` takes the class of the flow it is responding to as its single parameter:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
|
@InitiatedBy(Initiator::class)
|
||||||
|
class Responder(val otherParty: Party) : FlowLogic<Unit>() { }
|
||||||
|
|
||||||
|
.. sourcecode:: java
|
||||||
|
|
||||||
@InitiatedBy(Initiator.class)
|
@InitiatedBy(Initiator.class)
|
||||||
public static class Responder extends FlowLogic<Void> {
|
public static class Responder extends FlowLogic<Void> { }
|
||||||
|
|
||||||
Additionally, any flow that is started by a ``SchedulableState`` must be annotated with the ``@SchedulableFlow``
|
Additionally, any flow that is started by a ``SchedulableState`` must be annotated with the ``@SchedulableFlow``
|
||||||
annotation.
|
annotation.
|
||||||
|
|
||||||
ServiceHub
|
Call
|
||||||
----------
|
----
|
||||||
Within ``FlowLogic.call``, the flow developer has access to the node's ``ServiceHub``, which provides access to the
|
Each ``FlowLogic`` subclass must override ``FlowLogic.call()``, which describes the actions it will take as part of
|
||||||
various services the node provides. See :doc:`api-service-hub` for information about the services the ``ServiceHub``
|
the flow. For example, the actions of the initiator's side of the flow would be defined in ``Initiator.call``, and the
|
||||||
offers.
|
actions of the responder's side of the flow would be defined in ``Responder.call``.
|
||||||
|
|
||||||
Some common tasks performed using the ``ServiceHub`` are:
|
In order for nodes to be able to run multiple flows concurrently, and to allow flows to survive node upgrades and
|
||||||
|
restarts, flows need to be checkpointable and serializable to disk. This is achieved by marking ``FlowLogic.call()``,
|
||||||
* Looking up your own identity or the identity of a counterparty using the ``networkMapCache``
|
as well as any function invoked from within ``FlowLogic.call()``, with an ``@Suspendable`` annotation.
|
||||||
* Identifying the providers of a given service (e.g. a notary service) using the ``networkMapCache``
|
|
||||||
* Retrieving states to use in a transaction using the ``vaultService``
|
|
||||||
* Retrieving attachments and past transactions to use in a transaction using the ``storageService``
|
|
||||||
* Creating a timestamp using the ``clock``
|
|
||||||
* Signing a transaction using the ``keyManagementService``
|
|
||||||
|
|
||||||
Common flow tasks
|
|
||||||
-----------------
|
|
||||||
There are a number of common tasks that you will need to perform within ``FlowLogic.call`` in order to agree ledger
|
|
||||||
updates. This section details the API for the most common tasks.
|
|
||||||
|
|
||||||
Retrieving information about other nodes
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
We use the network map to retrieve information about other nodes on the network:
|
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
val networkMap = serviceHub.networkMapCache
|
class Initiator(val counterparty: Party): FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
val allNodes = networkMap.partyNodes
|
override fun call() { }
|
||||||
val allNotaryNodes = networkMap.notaryNodes
|
}
|
||||||
val randomNotaryNode = networkMap.getAnyNotary()
|
|
||||||
|
|
||||||
val alice = networkMap.getNodeByLegalName(X500Name("CN=Alice,O=Alice,L=London,C=GB"))
|
|
||||||
val bob = networkMap.getNodeByLegalIdentityKey(bobsKey)
|
|
||||||
|
|
||||||
.. sourcecode:: java
|
.. sourcecode:: java
|
||||||
|
|
||||||
final NetworkMapCache networkMap = getServiceHub().getNetworkMapCache();
|
public static class InitiatorFlow extends FlowLogic<Void> {
|
||||||
|
private final Party counterparty;
|
||||||
|
|
||||||
final List<NodeInfo> allNodes = networkMap.getPartyNodes();
|
public Initiator(Party counterparty) {
|
||||||
final List<NodeInfo> allNotaryNodes = networkMap.getNotaryNodes();
|
this.counterparty = counterparty;
|
||||||
final Party randomNotaryNode = networkMap.getAnyNotary(null);
|
}
|
||||||
|
|
||||||
final NodeInfo alice = networkMap.getNodeByLegalName(new X500Name("CN=Alice,O=Alice,L=London,C=GB"));
|
@Suspendable
|
||||||
final NodeInfo bob = networkMap.getNodeByLegalIdentityKey(bobsKey);
|
@Override
|
||||||
|
public Void call() throws FlowException { }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceHub
|
||||||
|
----------
|
||||||
|
Within ``FlowLogic.call``, the flow developer has access to the node's ``ServiceHub``, which provides access to the
|
||||||
|
various services the node provides. We will use the ``ServiceHub`` extensively in the examples that follow. You can
|
||||||
|
also see :doc:`api-service-hub` for information about the services the ``ServiceHub`` offers.
|
||||||
|
|
||||||
|
Common flow tasks
|
||||||
|
-----------------
|
||||||
|
There are a number of common tasks that you will need to perform within ``FlowLogic.call`` in order to agree ledger
|
||||||
|
updates. This section details the API for common tasks.
|
||||||
|
|
||||||
|
Transaction building
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
The majority of the work performed during a flow will be to build, verify and sign a transaction. We cover this in
|
||||||
|
:doc:`api-transactions`.
|
||||||
|
|
||||||
|
Retrieving information about other nodes
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
We can retrieve information about other nodes on the network and the services they offer using
|
||||||
|
``ServiceHub.networkMapCache``.
|
||||||
|
|
||||||
|
Notaries
|
||||||
|
~~~~~~~~
|
||||||
|
Remember that a transaction generally needs a notary to:
|
||||||
|
|
||||||
|
* Prevent double-spends if the transaction has inputs
|
||||||
|
* Serve as a timestamping authority if the transaction has a time-window
|
||||||
|
|
||||||
|
There are several ways to retrieve a notary from the network map:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 1
|
||||||
|
:end-before: DOCEND 1
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART 1
|
||||||
|
:end-before: DOCEND 1
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
Specific counterparties
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
We can also use the network map to retrieve a specific counterparty:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 2
|
||||||
|
:end-before: DOCEND 2
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART 2
|
||||||
|
:end-before: DOCEND 2
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
Specific services
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
Finally, we can use the map to identify nodes providing a specific service (e.g. a regulator or an oracle):
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 3
|
||||||
|
:end-before: DOCEND 3
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART 3
|
||||||
|
:end-before: DOCEND 3
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
Communication between parties
|
Communication between parties
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
@ -180,50 +274,121 @@ Communication between parties
|
|||||||
* ``sendAndReceive(receiveType: Class<R>, otherParty: Party, payload: Any)``
|
* ``sendAndReceive(receiveType: Class<R>, otherParty: Party, payload: Any)``
|
||||||
* Sends the ``payload`` object to the ``otherParty``, and receives an object of type ``receiveType`` back
|
* Sends the ``payload`` object to the ``otherParty``, and receives an object of type ``receiveType`` back
|
||||||
|
|
||||||
Each ``FlowLogic`` subclass can be annotated to respond to messages from a given *counterparty* flow using the
|
Send
|
||||||
``@InitiatedBy`` annotation. When a node first receives a message from a given ``FlowLogic.call()`` invocation, it
|
~~~~
|
||||||
responds as follows:
|
We can send arbitrary data to a counterparty:
|
||||||
|
|
||||||
* The node checks whether they have a ``FlowLogic`` subclass that is registered to respond to the ``FlowLogic`` that
|
|
||||||
is sending the message:
|
|
||||||
|
|
||||||
a. If yes, the node starts an instance of this ``FlowLogic`` by invoking ``FlowLogic.call()``
|
|
||||||
b. Otherwise, the node ignores the message
|
|
||||||
|
|
||||||
* The counterparty steps through their ``FlowLogic.call()`` method until they encounter a call to ``receive()``, at
|
|
||||||
which point they process the message from the initiator
|
|
||||||
|
|
||||||
Upon calling ``receive()``/``sendAndReceive()``, the ``FlowLogic`` is suspended until it receives a response.
|
|
||||||
|
|
||||||
UntrustworthyData
|
|
||||||
~~~~~~~~~~~~~~~~~
|
|
||||||
``send()`` and ``sendAndReceive()`` return a payload wrapped in an ``UntrustworthyData`` instance. This is a
|
|
||||||
reminder that any data received off the wire is untrustworthy and must be verified.
|
|
||||||
|
|
||||||
We verify the ``UntrustworthyData`` and retrieve its payload by calling ``unwrap``:
|
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. sourcecode:: kotlin
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 4
|
||||||
|
:end-before: DOCEND 4
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
val partSignedTx = receive<SignedTransaction>(otherParty).unwrap { partSignedTx ->
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
val wireTx = partSignedTx.verifySignatures(keyPair.public, notaryPubKey)
|
:language: java
|
||||||
wireTx.toLedgerTransaction(serviceHub).verify()
|
:start-after: DOCSTART 4
|
||||||
partSignedTx
|
:end-before: DOCEND 4
|
||||||
}
|
:dedent: 12
|
||||||
|
|
||||||
.. sourcecode:: java
|
If this is the first ``send``, the counterparty will either:
|
||||||
|
|
||||||
final SignedTransaction partSignedTx = receive(SignedTransaction.class, otherParty)
|
1. Ignore the message if they are not registered to respond to messages from this flow.
|
||||||
.unwrap(tx -> {
|
2. Start the flow they have registered to respond to this flow, and run the flow until the first call to ``receive``,
|
||||||
try {
|
at which point they process the message. In other words, we are assuming that the counterparty is registered to
|
||||||
final WireTransaction wireTx = tx.verifySignatures(keyPair.getPublic(), notaryPubKey);
|
respond to this flow, and has a corresponding ``receive`` call.
|
||||||
wireTx.toLedgerTransaction(getServiceHub()).verify();
|
|
||||||
} catch (SignatureException ex) {
|
Receive
|
||||||
throw new FlowException(tx.getId() + " failed signature checks", ex);
|
~~~~~~~
|
||||||
}
|
We can also wait to receive arbitrary data of a specific type from a counterparty. Again, this implies a corresponding
|
||||||
return tx;
|
``send`` call in the counterparty's flow. A few scenarios:
|
||||||
});
|
|
||||||
|
* We never receive a message back. In the current design, the flow is paused until the node's owner kills the flow.
|
||||||
|
* Instead of sending a message back, the counterparty throws a ``FlowException``. This exception is propagated back
|
||||||
|
to us, and we can use the error message to establish what happened.
|
||||||
|
* We receive a message back, but it's of the wrong type. In this case, a ``FlowException`` is thrown.
|
||||||
|
* We receive back a message of the correct type. All is good.
|
||||||
|
|
||||||
|
Upon calling ``receive`` (or ``sendAndReceive``), the ``FlowLogic`` is suspended until it receives a response.
|
||||||
|
|
||||||
|
We receive the data wrapped in an ``UntrustworthyData`` instance. This is a reminder that the data we receive may not
|
||||||
|
be what it appears to be! We must unwrap the ``UntrustworthyData`` using a lambda:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 5
|
||||||
|
:end-before: DOCEND 5
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART 5
|
||||||
|
:end-before: DOCEND 5
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
We're not limited to sending to and receiving from a single counterparty. A flow can send messages to as many parties
|
||||||
|
as it likes, and each party can invoke a different response flow:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 6
|
||||||
|
:end-before: DOCEND 6
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART 6
|
||||||
|
:end-before: DOCEND 6
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
SendAndReceive
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
We can also use a single call to send data to a counterparty and wait to receive data of a specific type back. The
|
||||||
|
type of data sent doesn't need to match the type of the data received back:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 7
|
||||||
|
:end-before: DOCEND 7
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART 7
|
||||||
|
:end-before: DOCEND 7
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
Counterparty response
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Suppose we're now on the ``Responder`` side of the flow. We just received the following series of messages from the
|
||||||
|
``Initiator``:
|
||||||
|
|
||||||
|
1. They sent us an ``Any`` instance
|
||||||
|
2. They waited to receive an ``Integer`` instance back
|
||||||
|
3. They sent a ``String`` instance and waited to receive a ``Boolean`` instance back
|
||||||
|
|
||||||
|
Our side of the flow must mirror these calls. We could do this as follows:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 8
|
||||||
|
:end-before: DOCEND 8
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART 8
|
||||||
|
:end-before: DOCEND 8
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
Subflows
|
Subflows
|
||||||
--------
|
--------
|
||||||
@ -236,27 +401,118 @@ Corda provides a number of built-in flows that should be used for handling commo
|
|||||||
* ``NotaryChangeFlow``, which should be used to change a state's notary
|
* ``NotaryChangeFlow``, which should be used to change a state's notary
|
||||||
|
|
||||||
These flows are designed to be used as building blocks in your own flows. You invoke them by calling
|
These flows are designed to be used as building blocks in your own flows. You invoke them by calling
|
||||||
``FlowLogic.subFlow`` from within your flow's ``call`` method. Here is an example from ``TwoPartyDealFlow.kt``:
|
``FlowLogic.subFlow`` from within your flow's ``call`` method. Let's look at three very common examples.
|
||||||
|
|
||||||
|
FinalityFlow
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
``FinalityFlow`` allows us to notarise the transaction and get it recorded in the vault of the participants of all
|
||||||
|
the transaction's states:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. literalinclude:: ../../core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: DOCSTART 1
|
:start-after: DOCSTART 9
|
||||||
:end-before: DOCEND 1
|
:end-before: DOCEND 9
|
||||||
:dedent: 12
|
:dedent: 12
|
||||||
|
|
||||||
In this example, we are starting a ``CollectSignaturesFlow``, passing in a partially signed transaction, and
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
receiving back a fully-signed version of the same transaction.
|
:language: java
|
||||||
|
:start-after: DOCSTART 9
|
||||||
|
:end-before: DOCEND 9
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
Subflows in our example flow
|
We can also choose to send the transaction to additional parties who aren't one of the state's participants:
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
In practice, many of the actions in our example flow would be automated using subflows:
|
|
||||||
|
|
||||||
* Parts 2-4 of ``Initiator.call`` should be automated by invoking ``CollectSignaturesFlow``
|
.. container:: codeset
|
||||||
* Part 5 of ``Initiator.call`` should be automated by invoking ``FinalityFlow``
|
|
||||||
* Part 1 of ``Responder.call`` should be automated by invoking ``SignTransactionFlow``
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
* Part 2 of ``Responder.call`` will be handled automatically when the counterparty invokes ``FinalityFlow``
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 10
|
||||||
|
:end-before: DOCEND 10
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART 10
|
||||||
|
:end-before: DOCEND 10
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
Only one party has to call ``FinalityFlow`` for a given transaction to be recorded by all participants. It does
|
||||||
|
**not** need to be called by each participant individually.
|
||||||
|
|
||||||
|
CollectSignaturesFlow/SignTransactionFlow
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
The list of parties who need to sign a transaction is dictated by the transaction's commands. Once we've signed a
|
||||||
|
transaction ourselves, we can automatically gather the signatures of the other required signers using
|
||||||
|
``CollectSignaturesFlow``:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 15
|
||||||
|
:end-before: DOCEND 15
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART 15
|
||||||
|
:end-before: DOCEND 15
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
Each required signer will need to respond by invoking its own ``SignTransactionFlow`` subclass to check the
|
||||||
|
transaction and provide their signature if they are satisfied:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 16
|
||||||
|
:end-before: DOCEND 16
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART 16
|
||||||
|
:end-before: DOCEND 16
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
ResolveTransactionsFlow
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
Verifying a transaction will also verify every transaction in the transaction's dependency chain. So if we receive a
|
||||||
|
transaction from a counterparty and it has any dependencies, we'd need to download all of these dependencies
|
||||||
|
using``ResolveTransactionsFlow`` before verifying it:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 13
|
||||||
|
:end-before: DOCEND 13
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART 13
|
||||||
|
:end-before: DOCEND 13
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
We can also resolve a `StateRef` dependency chain:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 14
|
||||||
|
:end-before: DOCEND 14
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART 14
|
||||||
|
:end-before: DOCEND 14
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
FlowException
|
FlowException
|
||||||
-------------
|
-------------
|
||||||
@ -283,18 +539,38 @@ There are many scenarios in which throwing a ``FlowException`` would be appropri
|
|||||||
* The transaction does not match the parameters of the deal as discussed
|
* The transaction does not match the parameters of the deal as discussed
|
||||||
* You are reneging on a deal
|
* You are reneging on a deal
|
||||||
|
|
||||||
Suspending flows
|
ProgressTracker
|
||||||
----------------
|
---------------
|
||||||
In order for nodes to be able to run multiple flows concurrently, and to allow flows to survive node upgrades and
|
We can give our flow a progress tracker. This allows us to see the flow's progress visually in our node's CRaSH shell.
|
||||||
restarts, flows need to be checkpointable and serializable to disk.
|
|
||||||
|
|
||||||
This is achieved by marking any function invoked from within ``FlowLogic.call()`` with an ``@Suspendable`` annotation.
|
To provide a progress tracker, we have to override ``FlowLogic.progressTracker`` in our flow:
|
||||||
|
|
||||||
We can see an example in ``CollectSignaturesFlow``:
|
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. literalinclude:: ../../core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: DOCSTART 1
|
:start-after: DOCSTART 17
|
||||||
:end-before: DOCEND 1
|
:end-before: DOCEND 17
|
||||||
|
:dedent: 8
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART 17
|
||||||
|
:end-before: DOCEND 17
|
||||||
|
:dedent: 8
|
||||||
|
|
||||||
|
We then update the progress tracker's current step as we progress through the flow as follows:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 18
|
||||||
|
:end-before: DOCEND 18
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART 18
|
||||||
|
:end-before: DOCEND 18
|
||||||
|
:dedent: 12
|
@ -7,4 +7,5 @@ Building a CorDapp
|
|||||||
cordapp-overview
|
cordapp-overview
|
||||||
writing-cordapps
|
writing-cordapps
|
||||||
api-index
|
api-index
|
||||||
|
flow-cookbook
|
||||||
cheat-sheet
|
cheat-sheet
|
@ -0,0 +1,498 @@
|
|||||||
|
package net.corda.docs;
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import net.corda.contracts.asset.Cash;
|
||||||
|
import net.corda.core.contracts.*;
|
||||||
|
import net.corda.core.contracts.TransactionType.General;
|
||||||
|
import net.corda.core.contracts.TransactionType.NotaryChange;
|
||||||
|
import net.corda.core.crypto.SecureHash;
|
||||||
|
import net.corda.core.flows.*;
|
||||||
|
import net.corda.core.identity.Party;
|
||||||
|
import net.corda.core.node.services.ServiceType;
|
||||||
|
import net.corda.core.node.services.Vault;
|
||||||
|
import net.corda.core.node.services.Vault.Page;
|
||||||
|
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria;
|
||||||
|
import net.corda.core.transactions.LedgerTransaction;
|
||||||
|
import net.corda.core.transactions.SignedTransaction;
|
||||||
|
import net.corda.core.transactions.TransactionBuilder;
|
||||||
|
import net.corda.core.transactions.WireTransaction;
|
||||||
|
import net.corda.core.utilities.ProgressTracker;
|
||||||
|
import net.corda.core.utilities.ProgressTracker.Step;
|
||||||
|
import net.corda.core.utilities.UntrustworthyData;
|
||||||
|
import net.corda.flows.CollectSignaturesFlow;
|
||||||
|
import net.corda.flows.FinalityFlow;
|
||||||
|
import net.corda.flows.ResolveTransactionsFlow;
|
||||||
|
import net.corda.flows.SignTransactionFlow;
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||||
|
import static net.corda.core.utilities.TestConstants.getDUMMY_PUBKEY_1;
|
||||||
|
|
||||||
|
// We group our two flows inside a singleton object to indicate that they work
|
||||||
|
// together.
|
||||||
|
public class FlowCookbookJava {
|
||||||
|
// ``InitiatorFlow`` is our first flow, and will communicate with
|
||||||
|
// ``ResponderFlow``, below.
|
||||||
|
// We mark ``InitiatorFlow`` as an ``InitiatingFlow``, allowing it to be
|
||||||
|
// started directly by the node.
|
||||||
|
@InitiatingFlow
|
||||||
|
// We also mark ``InitiatorFlow`` as ``StartableByRPC``, allowing the
|
||||||
|
// node's owner to start the flow via RPC.
|
||||||
|
@StartableByRPC
|
||||||
|
// Every flow must subclass ``FlowLogic``. The generic indicates the
|
||||||
|
// flow's return type.
|
||||||
|
public static class InitiatorFlow extends FlowLogic<Void> {
|
||||||
|
|
||||||
|
private final boolean arg1;
|
||||||
|
private final int arg2;
|
||||||
|
private final Party counterparty;
|
||||||
|
|
||||||
|
public InitiatorFlow(boolean arg1, int arg2, Party counterparty) {
|
||||||
|
this.arg1 = arg1;
|
||||||
|
this.arg2 = arg2;
|
||||||
|
this.counterparty = counterparty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*----------------------------------
|
||||||
|
* WIRING UP THE PROGRESS TRACKER *
|
||||||
|
----------------------------------*/
|
||||||
|
// Giving our flow a progress tracker allows us to see the flow's
|
||||||
|
// progress visually in our node's CRaSH shell.
|
||||||
|
// DOCSTART 17
|
||||||
|
private static final Step ID_OTHER_NODES = new Step("Identifying other nodes on the network.");
|
||||||
|
private static final Step SENDING_AND_RECEIVING_DATA = new Step("Sending data between parties.");
|
||||||
|
private static final Step EXTRACTING_VAULT_STATES = new Step("Extracting states from the vault.");
|
||||||
|
private static final Step OTHER_TX_COMPONENTS = new Step("Gathering a transaction's other components.");
|
||||||
|
private static final Step TX_BUILDING = new Step("Building a transaction.");
|
||||||
|
private static final Step TX_SIGNING = new Step("Signing a transaction.");
|
||||||
|
private static final Step TX_VERIFICATION = new Step("Verifying a transaction.");
|
||||||
|
private static final Step SIGS_GATHERING = new Step("Gathering a transaction's signatures.") {
|
||||||
|
// Wiring up a child progress tracker allows us to see the
|
||||||
|
// subflow's progress steps in our flow's progress tracker.
|
||||||
|
@Override public ProgressTracker childProgressTracker() {
|
||||||
|
return CollectSignaturesFlow.Companion.tracker();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private static final Step FINALISATION = new Step("Finalising a transaction.") {
|
||||||
|
@Override public ProgressTracker childProgressTracker() {
|
||||||
|
return FinalityFlow.Companion.tracker();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final ProgressTracker progressTracker = new ProgressTracker(
|
||||||
|
ID_OTHER_NODES,
|
||||||
|
SENDING_AND_RECEIVING_DATA,
|
||||||
|
EXTRACTING_VAULT_STATES,
|
||||||
|
OTHER_TX_COMPONENTS,
|
||||||
|
TX_BUILDING,
|
||||||
|
TX_SIGNING,
|
||||||
|
TX_VERIFICATION,
|
||||||
|
SIGS_GATHERING,
|
||||||
|
FINALISATION
|
||||||
|
);
|
||||||
|
// DOCEND 17
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
@Override
|
||||||
|
public Void call() throws FlowException {
|
||||||
|
// We'll be using a dummy public key for demonstration purposes.
|
||||||
|
// These are built in to Corda, and are generally used for writing
|
||||||
|
// tests.
|
||||||
|
PublicKey dummyPubKey = getDUMMY_PUBKEY_1();
|
||||||
|
|
||||||
|
/*---------------------------
|
||||||
|
* IDENTIFYING OTHER NODES *
|
||||||
|
---------------------------*/
|
||||||
|
// DOCSTART 18
|
||||||
|
progressTracker.setCurrentStep(ID_OTHER_NODES);
|
||||||
|
// DOCEND 18
|
||||||
|
|
||||||
|
// A transaction generally needs a notary:
|
||||||
|
// - To prevent double-spends if the transaction has inputs
|
||||||
|
// - To serve as a timestamping authority if the transaction has a time-window
|
||||||
|
// We retrieve a notary from the network map.
|
||||||
|
// DOCSTART 1
|
||||||
|
Party specificNotary = getServiceHub().getNetworkMapCache().getNotary(new X500Name("CN=Notary Service,O=R3,OU=corda,L=London,C=UK"));
|
||||||
|
Party anyNotary = getServiceHub().getNetworkMapCache().getAnyNotary(null);
|
||||||
|
// Unlike the first two methods, ``getNotaryNodes`` returns a
|
||||||
|
// ``List<NodeInfo>``. We have to extract the notary identity of
|
||||||
|
// the node we want.
|
||||||
|
Party firstNotary = getServiceHub().getNetworkMapCache().getNotaryNodes().get(0).getNotaryIdentity();
|
||||||
|
// DOCEND 1
|
||||||
|
|
||||||
|
// We may also need to identify a specific counterparty.
|
||||||
|
// Again, we do so using the network map.
|
||||||
|
// DOCSTART 2
|
||||||
|
Party namedCounterparty = getServiceHub().getNetworkMapCache().getNodeByLegalName(new X500Name("CN=NodeA,O=NodeA,L=London,C=UK")).getLegalIdentity();
|
||||||
|
Party keyedCounterparty = getServiceHub().getNetworkMapCache().getNodeByLegalIdentityKey(dummyPubKey).getLegalIdentity();
|
||||||
|
Party firstCounterparty = getServiceHub().getNetworkMapCache().getPartyNodes().get(0).getLegalIdentity();
|
||||||
|
// DOCEND 2
|
||||||
|
|
||||||
|
// Finally, we can use the map to identify nodes providing a
|
||||||
|
// specific service (e.g. a regulator or an oracle).
|
||||||
|
// DOCSTART 3
|
||||||
|
Party regulator = getServiceHub().getNetworkMapCache().getNodesWithService(ServiceType.Companion.getRegulator()).get(0).getLegalIdentity();
|
||||||
|
// DOCEND 3
|
||||||
|
|
||||||
|
/*------------------------------
|
||||||
|
* SENDING AND RECEIVING DATA *
|
||||||
|
------------------------------*/
|
||||||
|
progressTracker.setCurrentStep(SENDING_AND_RECEIVING_DATA);
|
||||||
|
|
||||||
|
// We can send arbitrary data to a counterparty.
|
||||||
|
// If this is the first ``send``, the counterparty will either:
|
||||||
|
// 1. Ignore the message if they are not registered to respond
|
||||||
|
// to messages from this flow.
|
||||||
|
// 2. Start the flow they have registered to respond to this flow,
|
||||||
|
// and run the flow until the first call to ``receive``, at
|
||||||
|
// which point they process the message.
|
||||||
|
// In other words, we are assuming that the counterparty is
|
||||||
|
// registered to respond to this flow, and has a corresponding
|
||||||
|
// ``receive`` call.
|
||||||
|
// DOCSTART 4
|
||||||
|
send(counterparty, new Object());
|
||||||
|
// DOCEND 4
|
||||||
|
|
||||||
|
// We can wait to receive arbitrary data of a specific type from a
|
||||||
|
// counterparty. Again, this implies a corresponding ``send`` call
|
||||||
|
// in the counterparty's flow. A few scenarios:
|
||||||
|
// - We never receive a message back. In the current design, the
|
||||||
|
// flow is paused until the node's owner kills the flow.
|
||||||
|
// - Instead of sending a message back, the counterparty throws a
|
||||||
|
// ``FlowException``. This exception is propagated back to us,
|
||||||
|
// and we can use the error message to establish what happened.
|
||||||
|
// - We receive a message back, but it's of the wrong type. In
|
||||||
|
// this case, a ``FlowException`` is thrown.
|
||||||
|
// - We receive back a message of the correct type. All is good.
|
||||||
|
//
|
||||||
|
// Upon calling ``receive()`` (or ``sendAndReceive()``), the
|
||||||
|
// ``FlowLogic`` is suspended until it receives a response.
|
||||||
|
//
|
||||||
|
// We receive the data wrapped in an ``UntrustworthyData``
|
||||||
|
// instance. This is a reminder that the data we receive may not
|
||||||
|
// be what it appears to be! We must unwrap the
|
||||||
|
// ``UntrustworthyData`` using a lambda.
|
||||||
|
// DOCSTART 5
|
||||||
|
UntrustworthyData<Integer> packet1 = receive(Integer.class, counterparty);
|
||||||
|
Integer integer = packet1.unwrap(data -> {
|
||||||
|
// Perform checking on the object received.
|
||||||
|
// T O D O: Check the received object.
|
||||||
|
// Return the object.
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
// DOCEND 5
|
||||||
|
|
||||||
|
// We can also use a single call to send data to a counterparty
|
||||||
|
// and wait to receive data of a specific type back. The type of
|
||||||
|
// data sent doesn't need to match the type of the data received
|
||||||
|
// back.
|
||||||
|
// DOCSTART 7
|
||||||
|
UntrustworthyData<Boolean> packet2 = sendAndReceive(Boolean.class, counterparty, "You can send and receive any class!");
|
||||||
|
Boolean bool = packet2.unwrap(data -> {
|
||||||
|
// Perform checking on the object received.
|
||||||
|
// T O D O: Check the received object.
|
||||||
|
// Return the object.
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
// DOCEND 7
|
||||||
|
|
||||||
|
// We're not limited to sending to and receiving from a single
|
||||||
|
// counterparty. A flow can send messages to as many parties as it
|
||||||
|
// likes, and each party can invoke a different response flow.
|
||||||
|
// DOCSTART 6
|
||||||
|
send(regulator, new Object());
|
||||||
|
UntrustworthyData<Object> packet3 = receive(Object.class, regulator);
|
||||||
|
// DOCEND 6
|
||||||
|
|
||||||
|
/*------------------------------------
|
||||||
|
* EXTRACTING STATES FROM THE VAULT *
|
||||||
|
------------------------------------*/
|
||||||
|
progressTracker.setCurrentStep(EXTRACTING_VAULT_STATES);
|
||||||
|
|
||||||
|
// Let's assume there are already some ``DummyState``s in our
|
||||||
|
// node's vault, stored there as a result of running past flows,
|
||||||
|
// and we want to consume them in a transaction. There are many
|
||||||
|
// ways to extract these states from our vault.
|
||||||
|
|
||||||
|
// For example, we would extract any unconsumed ``DummyState``s
|
||||||
|
// from our vault as follows:
|
||||||
|
VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED);
|
||||||
|
Page<DummyState> results = getServiceHub().getVaultQueryService().queryBy(DummyState.class, criteria);
|
||||||
|
List<StateAndRef<DummyState>> dummyStates = results.getStates();
|
||||||
|
|
||||||
|
// For a full list of the available ways of extracting states from
|
||||||
|
// the vault, see the Vault Query docs page.
|
||||||
|
|
||||||
|
// When building a transaction, input states are passed in as
|
||||||
|
// ``StateRef`` instances, which pair the hash of the transaction
|
||||||
|
// that generated the state with the state's index in the outputs
|
||||||
|
// of that transaction.
|
||||||
|
StateRef ourStateRef = new StateRef(SecureHash.sha256("DummyTransactionHash"), 0);
|
||||||
|
// A ``StateAndRef`` pairs a ``StateRef`` with the state it points to.
|
||||||
|
StateAndRef ourStateAndRef = getServiceHub().toStateAndRef(ourStateRef);
|
||||||
|
|
||||||
|
/*------------------------------------------
|
||||||
|
* GATHERING OTHER TRANSACTION COMPONENTS *
|
||||||
|
------------------------------------------*/
|
||||||
|
progressTracker.setCurrentStep(OTHER_TX_COMPONENTS);
|
||||||
|
|
||||||
|
// Output states are constructed from scratch.
|
||||||
|
DummyState ourOutput = new DummyState();
|
||||||
|
// Or as copies of other states with some properties changed.
|
||||||
|
DummyState ourOtherOutput = ourOutput.copy(77);
|
||||||
|
|
||||||
|
// Commands pair a ``CommandData`` instance with a list of
|
||||||
|
// public keys. To be valid, the transaction requires a signature
|
||||||
|
// matching every public key in all of the transaction's commands.
|
||||||
|
CommandData commandData = new DummyContract.Commands.Create();
|
||||||
|
PublicKey ourPubKey = getServiceHub().getLegalIdentityKey();
|
||||||
|
PublicKey counterpartyPubKey = counterparty.getOwningKey();
|
||||||
|
List<PublicKey> requiredSigners = ImmutableList.of(ourPubKey, counterpartyPubKey);
|
||||||
|
Command ourCommand = new Command(commandData, requiredSigners);
|
||||||
|
|
||||||
|
// ``CommandData`` can either be:
|
||||||
|
// 1. Of type ``TypeOnlyCommandData``, in which case it only
|
||||||
|
// serves to attach signers to the transaction and possibly
|
||||||
|
// fork the contract's verification logic.
|
||||||
|
TypeOnlyCommandData typeOnlyCommandData = new DummyContract.Commands.Create();
|
||||||
|
// 2. Include additional data which can be used by the contract
|
||||||
|
// during verification, alongside fulfilling the roles above
|
||||||
|
CommandData commandDataWithData = new Cash.Commands.Issue(12345678);
|
||||||
|
|
||||||
|
// Attachments are identified by their hash.
|
||||||
|
// The attachment with the corresponding hash must have been
|
||||||
|
// uploaded ahead of time via the node's RPC interface.
|
||||||
|
SecureHash ourAttachment = SecureHash.sha256("DummyAttachment");
|
||||||
|
|
||||||
|
// Time windows can have a start and end time, or be open at either end.
|
||||||
|
TimeWindow ourTimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX);
|
||||||
|
TimeWindow ourAfter = TimeWindow.fromOnly(Instant.MIN);
|
||||||
|
TimeWindow ourBefore = TimeWindow.untilOnly(Instant.MAX);
|
||||||
|
|
||||||
|
/*------------------------
|
||||||
|
* TRANSACTION BUILDING *
|
||||||
|
------------------------*/
|
||||||
|
progressTracker.setCurrentStep(TX_BUILDING);
|
||||||
|
|
||||||
|
// There are two types of transaction (notary-change and general),
|
||||||
|
// and therefore two types of transaction builder:
|
||||||
|
TransactionBuilder notaryChangeTxBuilder = new TransactionBuilder(NotaryChange.INSTANCE, specificNotary);
|
||||||
|
TransactionBuilder regTxBuilder = new TransactionBuilder(General.INSTANCE, specificNotary);
|
||||||
|
|
||||||
|
// We add items to the transaction builder using ``TransactionBuilder.withItems``:
|
||||||
|
regTxBuilder.withItems(
|
||||||
|
// Inputs, as ``StateRef``s that reference to the outputs of previous transactions
|
||||||
|
ourStateRef,
|
||||||
|
// Outputs, as ``ContractState``s
|
||||||
|
ourOutput,
|
||||||
|
// Commands, as ``Command``s
|
||||||
|
ourCommand
|
||||||
|
);
|
||||||
|
|
||||||
|
// We can also add items using methods for the individual components:
|
||||||
|
regTxBuilder.addInputState(ourStateAndRef);
|
||||||
|
regTxBuilder.addOutputState(ourOutput);
|
||||||
|
regTxBuilder.addCommand(ourCommand);
|
||||||
|
regTxBuilder.addAttachment(ourAttachment);
|
||||||
|
regTxBuilder.addTimeWindow(ourTimeWindow);
|
||||||
|
|
||||||
|
/*-----------------------
|
||||||
|
* TRANSACTION SIGNING *
|
||||||
|
-----------------------*/
|
||||||
|
progressTracker.setCurrentStep(TX_SIGNING);
|
||||||
|
|
||||||
|
// We finalise the transaction by signing it,
|
||||||
|
// converting it into a ``SignedTransaction``.
|
||||||
|
SignedTransaction onceSignedTx = getServiceHub().signInitialTransaction(regTxBuilder);
|
||||||
|
|
||||||
|
// If instead this was a ``SignedTransaction`` that we'd received
|
||||||
|
// from a counterparty and we needed to sign it, we would add our
|
||||||
|
// signature using:
|
||||||
|
SignedTransaction twiceSignedTx = getServiceHub().addSignature(onceSignedTx, dummyPubKey);
|
||||||
|
|
||||||
|
/*----------------------------
|
||||||
|
* TRANSACTION VERIFICATION *
|
||||||
|
----------------------------*/
|
||||||
|
progressTracker.setCurrentStep(TX_VERIFICATION);
|
||||||
|
|
||||||
|
// Verifying a transaction will also verify every transaction in
|
||||||
|
// the transaction's dependency chain. So if this was a
|
||||||
|
// transaction we'd received from a counterparty and it had any
|
||||||
|
// dependencies, we'd need to download all of these dependencies
|
||||||
|
// using``ResolveTransactionsFlow`` before verifying it.
|
||||||
|
// DOCSTART 13
|
||||||
|
subFlow(new ResolveTransactionsFlow(twiceSignedTx, counterparty));
|
||||||
|
// DOCEND 13
|
||||||
|
|
||||||
|
// We can also resolve a `StateRef` dependency chain.
|
||||||
|
// DOCSTART 14
|
||||||
|
subFlow(new ResolveTransactionsFlow(ImmutableSet.of(ourStateRef.getTxhash()), counterparty));
|
||||||
|
// DOCEND 14
|
||||||
|
|
||||||
|
// We verify a transaction using the following one-liner:
|
||||||
|
twiceSignedTx.getTx().toLedgerTransaction(getServiceHub()).verify();
|
||||||
|
|
||||||
|
// Let's break that down...
|
||||||
|
|
||||||
|
// A ``SignedTransaction`` is a pairing of a ``WireTransaction``
|
||||||
|
// with signatures over this ``WireTransaction``. We don't verify
|
||||||
|
// a signed transaction per se, but rather the ``WireTransaction``
|
||||||
|
// it contains.
|
||||||
|
WireTransaction wireTx = twiceSignedTx.getTx();
|
||||||
|
// Before we can verify the transaction, we need the
|
||||||
|
// ``ServiceHub`` to use our node's local storage to resolve the
|
||||||
|
// transaction's inputs and attachments into actual objects,
|
||||||
|
// rather than just references. We do this by converting the
|
||||||
|
// ``WireTransaction`` into a ``LedgerTransaction``.
|
||||||
|
LedgerTransaction ledgerTx = wireTx.toLedgerTransaction(getServiceHub());
|
||||||
|
// We can now verify the transaction.
|
||||||
|
ledgerTx.verify();
|
||||||
|
|
||||||
|
// We'll often want to perform our own additional verification
|
||||||
|
// too. Just because a transaction is valid based on the contract
|
||||||
|
// rules and requires our signature doesn't mean we have to
|
||||||
|
// sign it! We need to make sure the transaction represents an
|
||||||
|
// agreement we actually want to enter into.
|
||||||
|
DummyState outputState = (DummyState) wireTx.getOutputs().get(0).getData();
|
||||||
|
if (outputState.getMagicNumber() != 777) {
|
||||||
|
throw new FlowException("We expected a magic number of 777.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Of course, if you are not a required signer on the transaction,
|
||||||
|
// you have no power to decide whether it is valid or not. If it
|
||||||
|
// requires signatures from all the required signers and is
|
||||||
|
// contractually valid, it's a valid ledger update.
|
||||||
|
|
||||||
|
/*------------------------
|
||||||
|
* GATHERING SIGNATURES *
|
||||||
|
------------------------*/
|
||||||
|
progressTracker.setCurrentStep(SIGS_GATHERING);
|
||||||
|
|
||||||
|
// The list of parties who need to sign a transaction is dictated
|
||||||
|
// by the transaction's commands. Once we've signed a transaction
|
||||||
|
// ourselves, we can automatically gather the signatures of the
|
||||||
|
// other required signers using ``CollectSignaturesFlow``.
|
||||||
|
// The responder flow will need to call ``SignTransactionFlow``.
|
||||||
|
// DOCSTART 15
|
||||||
|
SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(twiceSignedTx, SIGS_GATHERING.childProgressTracker()));
|
||||||
|
// DOCEND 15
|
||||||
|
|
||||||
|
/*------------------------------
|
||||||
|
* FINALISING THE TRANSACTION *
|
||||||
|
------------------------------*/
|
||||||
|
progressTracker.setCurrentStep(FINALISATION);
|
||||||
|
|
||||||
|
// We notarise the transaction and get it recorded in the vault of
|
||||||
|
// the participants of all the transaction's states.
|
||||||
|
// DOCSTART 9
|
||||||
|
SignedTransaction notarisedTx1 = subFlow(new FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker())).get(0);
|
||||||
|
// DOCEND 9
|
||||||
|
// We can also choose to send it to additional parties who aren't one
|
||||||
|
// of the state's participants.
|
||||||
|
// DOCSTART 10
|
||||||
|
Set<Party> additionalParties = ImmutableSet.of(regulator, regulator);
|
||||||
|
SignedTransaction notarisedTx2 = subFlow(new FinalityFlow(ImmutableList.of(fullySignedTx), additionalParties, FINALISATION.childProgressTracker())).get(0);
|
||||||
|
// DOCEND 10
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ``ResponderFlow`` is our second flow, and will communicate with
|
||||||
|
// ``InitiatorFlow``.
|
||||||
|
// We mark ``ResponderFlow`` as an ``InitiatedByFlow``, meaning that it
|
||||||
|
// can only be started in response to a message from its initiating flow.
|
||||||
|
// That's ``InitiatorFlow`` in this case.
|
||||||
|
// Each node also has several flow pairs registered by default - see
|
||||||
|
// ``AbstractNode.installCoreFlows``.
|
||||||
|
@InitiatedBy(InitiatorFlow.class)
|
||||||
|
public static class ResponderFlow extends FlowLogic<Void> {
|
||||||
|
|
||||||
|
private final Party counterparty;
|
||||||
|
|
||||||
|
public ResponderFlow(Party counterparty) {
|
||||||
|
this.counterparty = counterparty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Step RECEIVING_AND_SENDING_DATA = new Step("Sending data between parties.");
|
||||||
|
private static final Step SIGNING = new Step("Responding to CollectSignaturesFlow.");
|
||||||
|
private static final Step FINALISATION = new Step("Finalising a transaction.");
|
||||||
|
|
||||||
|
private final ProgressTracker progressTracker = new ProgressTracker(
|
||||||
|
RECEIVING_AND_SENDING_DATA,
|
||||||
|
SIGNING,
|
||||||
|
FINALISATION
|
||||||
|
);
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
@Override
|
||||||
|
public Void call() throws FlowException {
|
||||||
|
// The ``ResponderFlow` has all the same APIs available. It looks
|
||||||
|
// up network information, sends and receives data, and constructs
|
||||||
|
// transactions in exactly the same way.
|
||||||
|
|
||||||
|
/*------------------------------
|
||||||
|
* SENDING AND RECEIVING DATA *
|
||||||
|
-----------------------------*/
|
||||||
|
progressTracker.setCurrentStep(RECEIVING_AND_SENDING_DATA);
|
||||||
|
|
||||||
|
// We need to respond to the messages sent by the initiator:
|
||||||
|
// 1. They sent us an ``Any`` instance
|
||||||
|
// 2. They waited to receive an ``Integer`` instance back
|
||||||
|
// 3. They sent a ``String`` instance and waited to receive a
|
||||||
|
// ``Boolean`` instance back
|
||||||
|
// Our side of the flow must mirror these calls.
|
||||||
|
// DOCSTART 8
|
||||||
|
Object obj = receive(Object.class, counterparty).unwrap(data -> data);
|
||||||
|
String string = sendAndReceive(String.class, counterparty, 99).unwrap(data -> data);
|
||||||
|
send(counterparty, true);
|
||||||
|
// DOCEND 8
|
||||||
|
|
||||||
|
/*-----------------------------------------
|
||||||
|
* RESPONDING TO COLLECT_SIGNATURES_FLOW *
|
||||||
|
-----------------------------------------*/
|
||||||
|
progressTracker.setCurrentStep(SIGNING);
|
||||||
|
|
||||||
|
// The responder will often need to respond to a call to
|
||||||
|
// ``CollectSignaturesFlow``. It does so my invoking its own
|
||||||
|
// ``SignTransactionFlow`` subclass.
|
||||||
|
// DOCSTART 16
|
||||||
|
class SignTxFlow extends SignTransactionFlow {
|
||||||
|
private SignTxFlow(Party otherParty, ProgressTracker progressTracker) {
|
||||||
|
super(otherParty, progressTracker);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void checkTransaction(SignedTransaction stx) {
|
||||||
|
requireThat(require -> {
|
||||||
|
// Any additional checking we see fit...
|
||||||
|
DummyState outputState = (DummyState) stx.getTx().getOutputs().get(0).getData();
|
||||||
|
assert (outputState.getMagicNumber() == 777);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subFlow(new SignTxFlow(counterparty, SignTransactionFlow.Companion.tracker()));
|
||||||
|
// DOCEND 16
|
||||||
|
|
||||||
|
/*------------------------------
|
||||||
|
* FINALISING THE TRANSACTION *
|
||||||
|
------------------------------*/
|
||||||
|
progressTracker.setCurrentStep(FINALISATION);
|
||||||
|
|
||||||
|
// Nothing to do here! As long as some other party calls
|
||||||
|
// ``FinalityFlow``, the recording of the transaction on our node
|
||||||
|
// we be handled automatically.
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,474 @@
|
|||||||
|
package net.corda.docs
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.contracts.asset.Cash
|
||||||
|
import net.corda.core.contracts.*
|
||||||
|
import net.corda.core.contracts.TransactionType.General
|
||||||
|
import net.corda.core.contracts.TransactionType.NotaryChange
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.flows.*
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.node.services.ServiceType
|
||||||
|
import net.corda.core.node.services.Vault.Page
|
||||||
|
import net.corda.core.node.services.queryBy
|
||||||
|
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
||||||
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
import net.corda.core.transactions.WireTransaction
|
||||||
|
import net.corda.core.utilities.DUMMY_PUBKEY_1
|
||||||
|
import net.corda.core.utilities.ProgressTracker
|
||||||
|
import net.corda.core.utilities.ProgressTracker.Step
|
||||||
|
import net.corda.core.utilities.UntrustworthyData
|
||||||
|
import net.corda.core.utilities.unwrap
|
||||||
|
import net.corda.flows.CollectSignaturesFlow
|
||||||
|
import net.corda.flows.FinalityFlow
|
||||||
|
import net.corda.flows.ResolveTransactionsFlow
|
||||||
|
import net.corda.flows.SignTransactionFlow
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
// We group our two flows inside a singleton object to indicate that they work
|
||||||
|
// together.
|
||||||
|
object FlowCookbook {
|
||||||
|
// ``InitiatorFlow`` is our first flow, and will communicate with
|
||||||
|
// ``ResponderFlow``, below.
|
||||||
|
// We mark ``InitiatorFlow`` as an ``InitiatingFlow``, allowing it to be
|
||||||
|
// started directly by the node.
|
||||||
|
@InitiatingFlow
|
||||||
|
// We also mark ``InitiatorFlow`` as ``StartableByRPC``, allowing the
|
||||||
|
// node's owner to start the flow via RPC.
|
||||||
|
@StartableByRPC
|
||||||
|
// Every flow must subclass ``FlowLogic``. The generic indicates the
|
||||||
|
// flow's return type.
|
||||||
|
class InitiatorFlow(val arg1: Boolean, val arg2: Int, val counterparty: Party) : FlowLogic<Unit>() {
|
||||||
|
|
||||||
|
/**---------------------------------
|
||||||
|
* WIRING UP THE PROGRESS TRACKER *
|
||||||
|
---------------------------------**/
|
||||||
|
// Giving our flow a progress tracker allows us to see the flow's
|
||||||
|
// progress visually in our node's CRaSH shell.
|
||||||
|
// DOCSTART 17
|
||||||
|
companion object {
|
||||||
|
object ID_OTHER_NODES : Step("Identifying other nodes on the network.")
|
||||||
|
object SENDING_AND_RECEIVING_DATA : Step("Sending data between parties.")
|
||||||
|
object EXTRACTING_VAULT_STATES : Step("Extracting states from the vault.")
|
||||||
|
object OTHER_TX_COMPONENTS : Step("Gathering a transaction's other components.")
|
||||||
|
object TX_BUILDING : Step("Building a transaction.")
|
||||||
|
object TX_SIGNING : Step("Signing a transaction.")
|
||||||
|
object TX_VERIFICATION : Step("Verifying a transaction.")
|
||||||
|
object SIGS_GATHERING : Step("Gathering a transaction's signatures.") {
|
||||||
|
// Wiring up a child progress tracker allows us to see the
|
||||||
|
// subflow's progress steps in our flow's progress tracker.
|
||||||
|
override fun childProgressTracker() = CollectSignaturesFlow.tracker()
|
||||||
|
}
|
||||||
|
object FINALISATION : Step("Finalising a transaction.") {
|
||||||
|
override fun childProgressTracker() = FinalityFlow.tracker()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tracker() = ProgressTracker(
|
||||||
|
ID_OTHER_NODES,
|
||||||
|
SENDING_AND_RECEIVING_DATA,
|
||||||
|
EXTRACTING_VAULT_STATES,
|
||||||
|
OTHER_TX_COMPONENTS,
|
||||||
|
TX_BUILDING,
|
||||||
|
TX_SIGNING,
|
||||||
|
TX_VERIFICATION,
|
||||||
|
SIGS_GATHERING,
|
||||||
|
FINALISATION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// DOCEND 17
|
||||||
|
|
||||||
|
override val progressTracker: ProgressTracker = tracker()
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
// We'll be using a dummy public key for demonstration purposes.
|
||||||
|
// These are built in to Corda, and are generally used for writing
|
||||||
|
// tests.
|
||||||
|
val dummyPubKey: PublicKey = DUMMY_PUBKEY_1
|
||||||
|
|
||||||
|
/**--------------------------
|
||||||
|
* IDENTIFYING OTHER NODES *
|
||||||
|
--------------------------**/
|
||||||
|
// DOCSTART 18
|
||||||
|
progressTracker.currentStep = ID_OTHER_NODES
|
||||||
|
// DOCEND 18
|
||||||
|
|
||||||
|
// A transaction generally needs a notary:
|
||||||
|
// - To prevent double-spends if the transaction has inputs
|
||||||
|
// - To serve as a timestamping authority if the transaction has a time-window
|
||||||
|
// We retrieve the notary from the network map.
|
||||||
|
// DOCSTART 1
|
||||||
|
val specificNotary: Party? = serviceHub.networkMapCache.getNotary(X500Name("CN=Notary Service,O=R3,OU=corda,L=London,C=UK"))
|
||||||
|
val anyNotary: Party? = serviceHub.networkMapCache.getAnyNotary()
|
||||||
|
// Unlike the first two methods, ``getNotaryNodes`` returns a
|
||||||
|
// ``List<NodeInfo>``. We have to extract the notary identity of
|
||||||
|
// the node we want.
|
||||||
|
val firstNotary: Party = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity
|
||||||
|
// DOCEND 1
|
||||||
|
|
||||||
|
// We may also need to identify a specific counterparty. Again, we
|
||||||
|
// do so using the network map.
|
||||||
|
// DOCSTART 2
|
||||||
|
val namedCounterparty: Party? = serviceHub.networkMapCache.getNodeByLegalName(X500Name("CN=NodeA,O=NodeA,L=London,C=UK"))?.legalIdentity
|
||||||
|
val keyedCounterparty: Party? = serviceHub.networkMapCache.getNodeByLegalIdentityKey(dummyPubKey)?.legalIdentity
|
||||||
|
val firstCounterparty: Party = serviceHub.networkMapCache.partyNodes[0].legalIdentity
|
||||||
|
// DOCEND 2
|
||||||
|
|
||||||
|
// Finally, we can use the map to identify nodes providing a
|
||||||
|
// specific service (e.g. a regulator or an oracle).
|
||||||
|
// DOCSTART 3
|
||||||
|
val regulator: Party = serviceHub.networkMapCache.getNodesWithService(ServiceType.regulator)[0].legalIdentity
|
||||||
|
// DOCEND 3
|
||||||
|
|
||||||
|
/**-----------------------------
|
||||||
|
* SENDING AND RECEIVING DATA *
|
||||||
|
-----------------------------**/
|
||||||
|
progressTracker.currentStep = SENDING_AND_RECEIVING_DATA
|
||||||
|
|
||||||
|
// We can send arbitrary data to a counterparty.
|
||||||
|
// If this is the first ``send``, the counterparty will either:
|
||||||
|
// 1. Ignore the message if they are not registered to respond
|
||||||
|
// to messages from this flow.
|
||||||
|
// 2. Start the flow they have registered to respond to this flow,
|
||||||
|
// and run the flow until the first call to ``receive``, at
|
||||||
|
// which point they process the message.
|
||||||
|
// In other words, we are assuming that the counterparty is
|
||||||
|
// registered to respond to this flow, and has a corresponding
|
||||||
|
// ``receive`` call.
|
||||||
|
// DOCSTART 4
|
||||||
|
send(counterparty, Any())
|
||||||
|
// DOCEND 4
|
||||||
|
|
||||||
|
// We can wait to receive arbitrary data of a specific type from a
|
||||||
|
// counterparty. Again, this implies a corresponding ``send`` call
|
||||||
|
// in the counterparty's flow. A few scenarios:
|
||||||
|
// - We never receive a message back. In the current design, the
|
||||||
|
// flow is paused until the node's owner kills the flow.
|
||||||
|
// - Instead of sending a message back, the counterparty throws a
|
||||||
|
// ``FlowException``. This exception is propagated back to us,
|
||||||
|
// and we can use the error message to establish what happened.
|
||||||
|
// - We receive a message back, but it's of the wrong type. In
|
||||||
|
// this case, a ``FlowException`` is thrown.
|
||||||
|
// - We receive back a message of the correct type. All is good.
|
||||||
|
//
|
||||||
|
// Upon calling ``receive()`` (or ``sendAndReceive()``), the
|
||||||
|
// ``FlowLogic`` is suspended until it receives a response.
|
||||||
|
//
|
||||||
|
// We receive the data wrapped in an ``UntrustworthyData``
|
||||||
|
// instance. This is a reminder that the data we receive may not
|
||||||
|
// be what it appears to be! We must unwrap the
|
||||||
|
// ``UntrustworthyData`` using a lambda.
|
||||||
|
// DOCSTART 5
|
||||||
|
val packet1: UntrustworthyData<Int> = receive<Int>(counterparty)
|
||||||
|
val int: Int = packet1.unwrap { data ->
|
||||||
|
// Perform checking on the object received.
|
||||||
|
// T O D O: Check the received object.
|
||||||
|
// Return the object.
|
||||||
|
data
|
||||||
|
}
|
||||||
|
// DOCEND 5
|
||||||
|
|
||||||
|
// We can also use a single call to send data to a counterparty
|
||||||
|
// and wait to receive data of a specific type back. The type of
|
||||||
|
// data sent doesn't need to match the type of the data received
|
||||||
|
// back.
|
||||||
|
// DOCSTART 7
|
||||||
|
val packet2: UntrustworthyData<Boolean> = sendAndReceive<Boolean>(counterparty, "You can send and receive any class!")
|
||||||
|
val boolean: Boolean = packet2.unwrap { data ->
|
||||||
|
// Perform checking on the object received.
|
||||||
|
// T O D O: Check the received object.
|
||||||
|
// Return the object.
|
||||||
|
data
|
||||||
|
}
|
||||||
|
// DOCEND 7
|
||||||
|
|
||||||
|
// We're not limited to sending to and receiving from a single
|
||||||
|
// counterparty. A flow can send messages to as many parties as it
|
||||||
|
// likes, and each party can invoke a different response flow.
|
||||||
|
// DOCSTART 6
|
||||||
|
send(regulator, Any())
|
||||||
|
val packet3: UntrustworthyData<Any> = receive<Any>(regulator)
|
||||||
|
// DOCEND 6
|
||||||
|
|
||||||
|
/**-----------------------------------
|
||||||
|
* EXTRACTING STATES FROM THE VAULT *
|
||||||
|
-----------------------------------**/
|
||||||
|
progressTracker.currentStep = EXTRACTING_VAULT_STATES
|
||||||
|
|
||||||
|
// Let's assume there are already some ``DummyState``s in our
|
||||||
|
// node's vault, stored there as a result of running past flows,
|
||||||
|
// and we want to consume them in a transaction. There are many
|
||||||
|
// ways to extract these states from our vault.
|
||||||
|
|
||||||
|
// For example, we would extract any unconsumed ``DummyState``s
|
||||||
|
// from our vault as follows:
|
||||||
|
val criteria: VaultQueryCriteria = VaultQueryCriteria() // default is UNCONSUMED
|
||||||
|
val results: Page<DummyState> = serviceHub.vaultQueryService.queryBy<DummyState>(criteria)
|
||||||
|
val dummyStates: List<StateAndRef<DummyState>> = results.states
|
||||||
|
|
||||||
|
// For a full list of the available ways of extracting states from
|
||||||
|
// the vault, see the Vault Query docs page.
|
||||||
|
|
||||||
|
// When building a transaction, input states are passed in as
|
||||||
|
// ``StateRef`` instances, which pair the hash of the transaction
|
||||||
|
// that generated the state with the state's index in the outputs
|
||||||
|
// of that transaction.
|
||||||
|
val ourStateRef: StateRef = StateRef(SecureHash.sha256("DummyTransactionHash"), 0)
|
||||||
|
// A ``StateAndRef`` pairs a ``StateRef`` with the state it points to.
|
||||||
|
val ourStateAndRef: StateAndRef<DummyState> = serviceHub.toStateAndRef<DummyState>(ourStateRef)
|
||||||
|
|
||||||
|
/**-----------------------------------------
|
||||||
|
* GATHERING OTHER TRANSACTION COMPONENTS *
|
||||||
|
-----------------------------------------**/
|
||||||
|
progressTracker.currentStep = OTHER_TX_COMPONENTS
|
||||||
|
|
||||||
|
// Output states are constructed from scratch.
|
||||||
|
val ourOutput: DummyState = DummyState()
|
||||||
|
// Or as copies of other states with some properties changed.
|
||||||
|
val ourOtherOutput: DummyState = ourOutput.copy(magicNumber = 77)
|
||||||
|
|
||||||
|
// Commands pair a ``CommandData`` instance with a list of
|
||||||
|
// public keys. To be valid, the transaction requires a signature
|
||||||
|
// matching every public key in all of the transaction's commands.
|
||||||
|
val commandData: CommandData = DummyContract.Commands.Create()
|
||||||
|
val ourPubKey: PublicKey = serviceHub.legalIdentityKey
|
||||||
|
val counterpartyPubKey: PublicKey = counterparty.owningKey
|
||||||
|
val requiredSigners: List<PublicKey> = listOf(ourPubKey, counterpartyPubKey)
|
||||||
|
val ourCommand: Command = Command(commandData, requiredSigners)
|
||||||
|
|
||||||
|
// ``CommandData`` can either be:
|
||||||
|
// 1. Of type ``TypeOnlyCommandData``, in which case it only
|
||||||
|
// serves to attach signers to the transaction and possibly
|
||||||
|
// fork the contract's verification logic.
|
||||||
|
val typeOnlyCommandData: TypeOnlyCommandData = DummyContract.Commands.Create()
|
||||||
|
// 2. Include additional data which can be used by the contract
|
||||||
|
// during verification, alongside fulfilling the roles above
|
||||||
|
val commandDataWithData: CommandData = Cash.Commands.Issue(nonce = 12345678)
|
||||||
|
|
||||||
|
// Attachments are identified by their hash.
|
||||||
|
// The attachment with the corresponding hash must have been
|
||||||
|
// uploaded ahead of time via the node's RPC interface.
|
||||||
|
val ourAttachment: SecureHash = SecureHash.sha256("DummyAttachment")
|
||||||
|
|
||||||
|
// Time windows can have a start and end time, or be open at either end.
|
||||||
|
val ourTimeWindow: TimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX)
|
||||||
|
val ourAfter: TimeWindow = TimeWindow.fromOnly(Instant.MIN)
|
||||||
|
val ourBefore: TimeWindow = TimeWindow.untilOnly(Instant.MAX)
|
||||||
|
|
||||||
|
/**-----------------------
|
||||||
|
* TRANSACTION BUILDING *
|
||||||
|
-----------------------**/
|
||||||
|
progressTracker.currentStep = TX_BUILDING
|
||||||
|
|
||||||
|
// There are two types of transaction (notary-change and general),
|
||||||
|
// and therefore two types of transaction builder:
|
||||||
|
val notaryChangeTxBuilder: TransactionBuilder = TransactionBuilder(NotaryChange, specificNotary)
|
||||||
|
val regTxBuilder: TransactionBuilder = TransactionBuilder(General, specificNotary)
|
||||||
|
|
||||||
|
// We add items to the transaction builder using ``TransactionBuilder.withItems``:
|
||||||
|
regTxBuilder.withItems(
|
||||||
|
// Inputs, as ``StateRef``s that reference to the outputs of previous transactions
|
||||||
|
ourStateRef,
|
||||||
|
// Outputs, as ``ContractState``s
|
||||||
|
ourOutput,
|
||||||
|
// Commands, as ``Command``s
|
||||||
|
ourCommand
|
||||||
|
)
|
||||||
|
|
||||||
|
// We can also add items using methods for the individual components:
|
||||||
|
regTxBuilder.addInputState(ourStateAndRef)
|
||||||
|
regTxBuilder.addOutputState(ourOutput)
|
||||||
|
regTxBuilder.addCommand(ourCommand)
|
||||||
|
regTxBuilder.addAttachment(ourAttachment)
|
||||||
|
regTxBuilder.addTimeWindow(ourTimeWindow)
|
||||||
|
|
||||||
|
/**----------------------
|
||||||
|
* TRANSACTION SIGNING *
|
||||||
|
----------------------**/
|
||||||
|
progressTracker.currentStep = TX_SIGNING
|
||||||
|
|
||||||
|
// We finalise the transaction by signing it, converting it into a
|
||||||
|
// ``SignedTransaction``.
|
||||||
|
val onceSignedTx: SignedTransaction = serviceHub.signInitialTransaction(regTxBuilder)
|
||||||
|
|
||||||
|
// If instead this was a ``SignedTransaction`` that we'd received
|
||||||
|
// from a counterparty and we needed to sign it, we would add our
|
||||||
|
// signature using:
|
||||||
|
val twiceSignedTx: SignedTransaction = serviceHub.addSignature(onceSignedTx, dummyPubKey)
|
||||||
|
|
||||||
|
// In practice, however, the process of gathering every signature
|
||||||
|
// but the first can be automated using ``CollectSignaturesFlow``.
|
||||||
|
// See the "Gathering Signatures" section below.
|
||||||
|
|
||||||
|
/**---------------------------
|
||||||
|
* TRANSACTION VERIFICATION *
|
||||||
|
---------------------------**/
|
||||||
|
progressTracker.currentStep = TX_VERIFICATION
|
||||||
|
|
||||||
|
// Verifying a transaction will also verify every transaction in
|
||||||
|
// the transaction's dependency chain. So if this was a
|
||||||
|
// transaction we'd received from a counterparty and it had any
|
||||||
|
// dependencies, we'd need to download all of these dependencies
|
||||||
|
// using``ResolveTransactionsFlow`` before verifying it.
|
||||||
|
// DOCSTART 13
|
||||||
|
subFlow(ResolveTransactionsFlow(twiceSignedTx, counterparty))
|
||||||
|
// DOCEND 13
|
||||||
|
|
||||||
|
// We can also resolve a `StateRef` dependency chain.
|
||||||
|
// DOCSTART 14
|
||||||
|
subFlow(ResolveTransactionsFlow(setOf(ourStateRef.txhash), counterparty))
|
||||||
|
// DOCEND 14
|
||||||
|
|
||||||
|
// We verify a transaction using the following one-liner:
|
||||||
|
twiceSignedTx.tx.toLedgerTransaction(serviceHub).verify()
|
||||||
|
|
||||||
|
// Let's break that down...
|
||||||
|
|
||||||
|
// A ``SignedTransaction`` is a pairing of a ``WireTransaction``
|
||||||
|
// with signatures over this ``WireTransaction``. We don't verify
|
||||||
|
// a signed transaction per se, but rather the ``WireTransaction``
|
||||||
|
// it contains.
|
||||||
|
val wireTx: WireTransaction = twiceSignedTx.tx
|
||||||
|
// Before we can verify the transaction, we need the
|
||||||
|
// ``ServiceHub`` to use our node's local storage to resolve the
|
||||||
|
// transaction's inputs and attachments into actual objects,
|
||||||
|
// rather than just references. We do this by converting the
|
||||||
|
// ``WireTransaction`` into a ``LedgerTransaction``.
|
||||||
|
val ledgerTx: LedgerTransaction = wireTx.toLedgerTransaction(serviceHub)
|
||||||
|
// We can now verify the transaction.
|
||||||
|
ledgerTx.verify()
|
||||||
|
|
||||||
|
// We'll often want to perform our own additional verification
|
||||||
|
// too. Just because a transaction is valid based on the contract
|
||||||
|
// rules and requires our signature doesn't mean we have to
|
||||||
|
// sign it! We need to make sure the transaction represents an
|
||||||
|
// agreement we actually want to enter into.
|
||||||
|
val outputState: DummyState = wireTx.outputs.single().data as DummyState
|
||||||
|
if (outputState.magicNumber == 777) {
|
||||||
|
// ``FlowException`` is a special exception type. It will be
|
||||||
|
// propagated back to any counterparty flows waiting for a
|
||||||
|
// message from this flow, notifying them that the flow has
|
||||||
|
// failed.
|
||||||
|
throw FlowException("We expected a magic number of 777.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Of course, if you are not a required signer on the transaction,
|
||||||
|
// you have no power to decide whether it is valid or not. If it
|
||||||
|
// requires signatures from all the required signers and is
|
||||||
|
// contractually valid, it's a valid ledger update.
|
||||||
|
|
||||||
|
/**-----------------------
|
||||||
|
* GATHERING SIGNATURES *
|
||||||
|
-----------------------**/
|
||||||
|
progressTracker.currentStep = SIGS_GATHERING
|
||||||
|
|
||||||
|
// The list of parties who need to sign a transaction is dictated
|
||||||
|
// by the transaction's commands. Once we've signed a transaction
|
||||||
|
// ourselves, we can automatically gather the signatures of the
|
||||||
|
// other required signers using ``CollectSignaturesFlow``.
|
||||||
|
// The responder flow will need to call ``SignTransactionFlow``.
|
||||||
|
// DOCSTART 15
|
||||||
|
val fullySignedTx: SignedTransaction = subFlow(CollectSignaturesFlow(twiceSignedTx, SIGS_GATHERING.childProgressTracker()))
|
||||||
|
// DOCEND 15
|
||||||
|
|
||||||
|
/**-----------------------------
|
||||||
|
* FINALISING THE TRANSACTION *
|
||||||
|
-----------------------------**/
|
||||||
|
progressTracker.currentStep = FINALISATION
|
||||||
|
|
||||||
|
// We notarise the transaction and get it recorded in the vault of
|
||||||
|
// the participants of all the transaction's states.
|
||||||
|
// DOCSTART 9
|
||||||
|
val notarisedTx1: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker())).single()
|
||||||
|
// DOCEND 9
|
||||||
|
// We can also choose to send it to additional parties who aren't one
|
||||||
|
// of the state's participants.
|
||||||
|
// DOCSTART 10
|
||||||
|
val additionalParties: Set<Party> = setOf(regulator)
|
||||||
|
val notarisedTx2: SignedTransaction = subFlow(FinalityFlow(listOf(fullySignedTx), additionalParties, FINALISATION.childProgressTracker())).single()
|
||||||
|
// DOCEND 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ``ResponderFlow`` is our second flow, and will communicate with
|
||||||
|
// ``InitiatorFlow``.
|
||||||
|
// We mark ``ResponderFlow`` as an ``InitiatedByFlow``, meaning that it
|
||||||
|
// can only be started in response to a message from its initiating flow.
|
||||||
|
// That's ``InitiatorFlow`` in this case.
|
||||||
|
// Each node also has several flow pairs registered by default - see
|
||||||
|
// ``AbstractNode.installCoreFlows``.
|
||||||
|
@InitiatedBy(InitiatorFlow::class)
|
||||||
|
class ResponderFlow(val counterparty: Party) : FlowLogic<Unit>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
object RECEIVING_AND_SENDING_DATA : Step("Sending data between parties.")
|
||||||
|
object SIGNING : Step("Responding to CollectSignaturesFlow.")
|
||||||
|
object FINALISATION : Step("Finalising a transaction.")
|
||||||
|
|
||||||
|
fun tracker() = ProgressTracker(
|
||||||
|
RECEIVING_AND_SENDING_DATA,
|
||||||
|
SIGNING,
|
||||||
|
FINALISATION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val progressTracker: ProgressTracker = tracker()
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
// The ``ResponderFlow` has all the same APIs available. It looks
|
||||||
|
// up network information, sends and receives data, and constructs
|
||||||
|
// transactions in exactly the same way.
|
||||||
|
|
||||||
|
/**-----------------------------
|
||||||
|
* SENDING AND RECEIVING DATA *
|
||||||
|
-----------------------------**/
|
||||||
|
progressTracker.currentStep = RECEIVING_AND_SENDING_DATA
|
||||||
|
|
||||||
|
// We need to respond to the messages sent by the initiator:
|
||||||
|
// 1. They sent us an ``Any`` instance
|
||||||
|
// 2. They waited to receive an ``Integer`` instance back
|
||||||
|
// 3. They sent a ``String`` instance and waited to receive a
|
||||||
|
// ``Boolean`` instance back
|
||||||
|
// Our side of the flow must mirror these calls.
|
||||||
|
// DOCSTART 8
|
||||||
|
val any: Any = receive<Any>(counterparty).unwrap { data -> data }
|
||||||
|
val string: String = sendAndReceive<String>(counterparty, 99).unwrap { data -> data }
|
||||||
|
send(counterparty, true)
|
||||||
|
// DOCEND 8
|
||||||
|
|
||||||
|
/**----------------------------------------
|
||||||
|
* RESPONDING TO COLLECT_SIGNATURES_FLOW *
|
||||||
|
----------------------------------------**/
|
||||||
|
progressTracker.currentStep = SIGNING
|
||||||
|
|
||||||
|
// The responder will often need to respond to a call to
|
||||||
|
// ``CollectSignaturesFlow``. It does so my invoking its own
|
||||||
|
// ``SignTransactionFlow`` subclass.
|
||||||
|
// DOCSTART 16
|
||||||
|
val signTransactionFlow: SignTransactionFlow = object : SignTransactionFlow(counterparty) {
|
||||||
|
override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||||
|
// Any additional checking we see fit...
|
||||||
|
val outputState = stx.tx.outputs.single().data as DummyState
|
||||||
|
assert(outputState.magicNumber == 777)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subFlow(signTransactionFlow)
|
||||||
|
// DOCEND 16
|
||||||
|
|
||||||
|
/**-----------------------------
|
||||||
|
* FINALISING THE TRANSACTION *
|
||||||
|
-----------------------------**/
|
||||||
|
progressTracker.currentStep = FINALISATION
|
||||||
|
|
||||||
|
// Nothing to do here! As long as some other party calls
|
||||||
|
// ``FinalityFlow``, the recording of the transaction on our node
|
||||||
|
// we be handled automatically.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
docs/source/flow-cookbook.rst
Normal file
18
docs/source/flow-cookbook.rst
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.. highlight:: kotlin
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||||
|
|
||||||
|
Flow cookbook
|
||||||
|
=============
|
||||||
|
|
||||||
|
This flow showcases how to use Corda's API, in both Java and Kotlin.
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||||
|
:language: kotlin
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||||
|
:language: java
|
Loading…
x
Reference in New Issue
Block a user