mirror of
https://github.com/corda/corda.git
synced 2025-02-21 01:42:24 +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`.
|
||||
|
||||
.. contents::
|
||||
|
||||
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
|
||||
* 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
|
||||
---------
|
||||
In practice, a flow is implemented as one or more communicating ``FlowLogic`` subclasses. Each ``FlowLogic`` subclass
|
||||
must override ``FlowLogic.call()``, which describes the actions it will take as part of the flow.
|
||||
In practice, a flow is implemented as one or more communicating ``FlowLogic`` subclasses. The ``FlowLogic``
|
||||
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``
|
||||
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``.
|
||||
.. container:: codeset
|
||||
|
||||
.. 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
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
---------------------
|
||||
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
|
||||
``@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:
|
||||
``@StartableByRPC`` annotation:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
@ -100,74 +123,145 @@ So in our example, we would have:
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class Initiator(): FlowLogic<Unit>() {
|
||||
|
||||
...
|
||||
|
||||
@InitiatedBy(Initiator::class)
|
||||
class Responder(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
class Initiator(): FlowLogic<Unit>() { }
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
@InitiatingFlow
|
||||
@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)
|
||||
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``
|
||||
annotation.
|
||||
|
||||
ServiceHub
|
||||
----------
|
||||
Within ``FlowLogic.call``, the flow developer has access to the node's ``ServiceHub``, which provides access to the
|
||||
various services the node provides. See :doc:`api-service-hub` for information about the services the ``ServiceHub``
|
||||
offers.
|
||||
Call
|
||||
----
|
||||
Each ``FlowLogic`` subclass must override ``FlowLogic.call()``, which describes the actions it will take as part of
|
||||
the flow. For example, 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``.
|
||||
|
||||
Some common tasks performed using the ``ServiceHub`` are:
|
||||
|
||||
* Looking up your own identity or the identity of a counterparty using the ``networkMapCache``
|
||||
* 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:
|
||||
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()``,
|
||||
as well as any function invoked from within ``FlowLogic.call()``, with an ``@Suspendable`` annotation.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
val networkMap = serviceHub.networkMapCache
|
||||
|
||||
val allNodes = networkMap.partyNodes
|
||||
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)
|
||||
class Initiator(val counterparty: Party): FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() { }
|
||||
}
|
||||
|
||||
.. sourcecode:: java
|
||||
|
||||
final NetworkMapCache networkMap = getServiceHub().getNetworkMapCache();
|
||||
public static class InitiatorFlow extends FlowLogic<Void> {
|
||||
private final Party counterparty;
|
||||
|
||||
final List<NodeInfo> allNodes = networkMap.getPartyNodes();
|
||||
final List<NodeInfo> allNotaryNodes = networkMap.getNotaryNodes();
|
||||
final Party randomNotaryNode = networkMap.getAnyNotary(null);
|
||||
public Initiator(Party counterparty) {
|
||||
this.counterparty = counterparty;
|
||||
}
|
||||
|
||||
final NodeInfo alice = networkMap.getNodeByLegalName(new X500Name("CN=Alice,O=Alice,L=London,C=GB"));
|
||||
final NodeInfo bob = networkMap.getNodeByLegalIdentityKey(bobsKey);
|
||||
@Suspendable
|
||||
@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
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@ -180,50 +274,121 @@ Communication between parties
|
||||
* ``sendAndReceive(receiveType: Class<R>, otherParty: Party, payload: Any)``
|
||||
* 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
|
||||
``@InitiatedBy`` annotation. When a node first receives a message from a given ``FlowLogic.call()`` invocation, it
|
||||
responds as follows:
|
||||
|
||||
* 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``:
|
||||
Send
|
||||
~~~~
|
||||
We can send arbitrary data to a counterparty:
|
||||
|
||||
.. 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 ->
|
||||
val wireTx = partSignedTx.verifySignatures(keyPair.public, notaryPubKey)
|
||||
wireTx.toLedgerTransaction(serviceHub).verify()
|
||||
partSignedTx
|
||||
}
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 4
|
||||
: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)
|
||||
.unwrap(tx -> {
|
||||
try {
|
||||
final WireTransaction wireTx = tx.verifySignatures(keyPair.getPublic(), notaryPubKey);
|
||||
wireTx.toLedgerTransaction(getServiceHub()).verify();
|
||||
} catch (SignatureException ex) {
|
||||
throw new FlowException(tx.getId() + " failed signature checks", ex);
|
||||
}
|
||||
return tx;
|
||||
});
|
||||
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.
|
||||
|
||||
Receive
|
||||
~~~~~~~
|
||||
We can also 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:
|
||||
|
||||
.. 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
|
||||
--------
|
||||
@ -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
|
||||
|
||||
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
|
||||
|
||||
.. 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
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
:start-after: DOCSTART 9
|
||||
:end-before: DOCEND 9
|
||||
:dedent: 12
|
||||
|
||||
In this example, we are starting a ``CollectSignaturesFlow``, passing in a partially signed transaction, and
|
||||
receiving back a fully-signed version of the same transaction.
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java
|
||||
:language: java
|
||||
:start-after: DOCSTART 9
|
||||
:end-before: DOCEND 9
|
||||
:dedent: 12
|
||||
|
||||
Subflows in our example flow
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
In practice, many of the actions in our example flow would be automated using subflows:
|
||||
We can also choose to send the transaction to additional parties who aren't one of the state's participants:
|
||||
|
||||
* Parts 2-4 of ``Initiator.call`` should be automated by invoking ``CollectSignaturesFlow``
|
||||
* Part 5 of ``Initiator.call`` should be automated by invoking ``FinalityFlow``
|
||||
* Part 1 of ``Responder.call`` should be automated by invoking ``SignTransactionFlow``
|
||||
* Part 2 of ``Responder.call`` will be handled automatically when the counterparty invokes ``FinalityFlow``
|
||||
.. container:: codeset
|
||||
|
||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt
|
||||
: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
|
||||
-------------
|
||||
@ -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
|
||||
* You are reneging on a deal
|
||||
|
||||
Suspending flows
|
||||
----------------
|
||||
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.
|
||||
ProgressTracker
|
||||
---------------
|
||||
We can give our flow a progress tracker. This allows us to see the flow's progress visually in our node's CRaSH shell.
|
||||
|
||||
This is achieved by marking any function invoked from within ``FlowLogic.call()`` with an ``@Suspendable`` annotation.
|
||||
|
||||
We can see an example in ``CollectSignaturesFlow``:
|
||||
To provide a progress tracker, we have to override ``FlowLogic.progressTracker`` in our flow:
|
||||
|
||||
.. 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
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
:start-after: DOCSTART 17
|
||||
: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
|
||||
writing-cordapps
|
||||
api-index
|
||||
flow-cookbook
|
||||
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