40 KiB
Writing the flow
A flow describes the sequence of steps for agreeing a specific ledger update. By installing new flows on our node, we allow the node to handle new business processes.
We'll have to define two flows to issue an IOUState
onto
the ledger:
- One to be run by the node initiating the creation of the IOU
- One to be run by the node responding to an IOU creation request
Let's start writing our flows. We'll do this by modifying either
TemplateFlow.java
or TemplateFlow.kt
.
FlowLogic
Each flow is implemented as a FlowLogic
subclass. You
define the steps taken by the flow by overriding
FlowLogic.call
.
We will define two FlowLogic
instances communicating as
a pair. The first will be called Initiator
, and will be run
by the sender of the IOU. The other will be called
Acceptor
, and will be run by the recipient. We group them
together using a class (in Java) or a singleton object (in Kotlin) to
show that they are conceptually related.
Overwrite the existing template code with the following:
package com.template
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
object IOUFlow {
@InitiatingFlow
@StartableByRPC
class Initiator(val iouValue: Int,
val otherParty: Party): FlowLogic<SignedTransaction>() {
/** The progress tracker provides checkpoints indicating the progress of the flow to observers. */
override val progressTracker = ProgressTracker()
/** The flow logic is encapsulated within the call() method. */
@Suspendable
override fun call(): SignedTransaction { }
}
@InitiatedBy(Initiator::class)
class Acceptor(val otherParty: Party) : FlowLogic<Unit>() {
@Suspendable
override fun call() { }
}
}
package com.template;
import co.paralleluniverse.fibers.Suspendable;
import net.corda.core.flows.*;
import net.corda.core.identity.Party;
import net.corda.core.transactions.SignedTransaction;
import net.corda.core.utilities.ProgressTracker;
public class IOUFlow {
@InitiatingFlow
@StartableByRPC
public static class Initiator extends FlowLogic<SignedTransaction> {
private final Integer iouValue;
private final Party otherParty;
/** The progress tracker provides checkpoints indicating the progress of the flow to observers. */
private final ProgressTracker progressTracker = new ProgressTracker();
public Initiator(Integer iouValue, Party otherParty) {
this.iouValue = iouValue;
this.otherParty = otherParty;
}
/** The flow logic is encapsulated within the call() method. */
@Suspendable
@Override
public SignedTransaction call() throws FlowException { }
}
@InitiatedBy(Initiator.class)
public static class Acceptor extends FlowLogic<Void> {
private final Party otherParty;
public Acceptor(Party otherParty) {
this.otherParty = otherParty;
}
@Suspendable
@Override
public Void call() throws FlowException { }
}
}
We can see that we have two FlowLogic
subclasses, each
overriding FlowLogic.call
. There's a few things to
note:
FlowLogic.call
has a return type that matches the type parameter passed toFlowLogic
- this is the return type of running the flow- The
FlowLogic
subclasses can have constructor parameters, which can be used as arguments toFlowLogic.call
FlowLogic.call
is annotated@Suspendable
- this means that the flow will be check-pointed and serialised to disk when it encounters a long-running operation, allowing your node to move on to running other flows. Forgetting this annotation out will lead to some very weird error messages- There are also a few more annotations, on the
FlowLogic
subclasses themselves:@InitiatingFlow
means that this flow can be started directly by the nodeStartableByRPC
allows the node owner to start this flow via an RPC call@InitiatedBy(myClass: Class)
means that this flow will only start in response to a message sent by another node running themyClass
flow
Flow outline
Now that we've defined our FlowLogic
subclasses, what
are the steps we need to take to issue a new IOU onto the ledger?
On the initiator side, we need to:
- Create a valid transaction proposal for the creation of a new IOU
- Verify the transaction
- Sign the transaction ourselves
- Gather the acceptor's signature
- Optionally get the transaction notarised, to:
- Protect against double-spends for transactions with inputs
- Timestamp transactions that have a
TimeWindow
- Record the transaction in our vault
- Send the transaction to the acceptor so that they can record it too
On the acceptor side, we need to:
- Receive the partially-signed transaction from the initiator
- Verify its contents and signatures
- Append our signature and send it back to the initiator
- Wait to receive back the transaction from the initiator
- Record the transaction in our vault
Subflows
Although our flow requirements look complex, we can delegate to existing flows to handle many of these tasks. A flow that is invoked within the context of a larger flow to handle a repeatable task is called a subflow.
In our initiator flow, we can automate step 4 by invoking
SignTransactionFlow
, and we can automate steps 5, 6 and 7
using FinalityFlow
. Meanwhile, the entirety of the
acceptor's flow can be automated using
CollectSignaturesFlow
.
All we need to do is write the steps to handle the initiator creating and signing the proposed transaction.
Writing the initiator's flow
Let's work through the steps of the initiator's flow one-by-one.
Building the transaction
We'll approach building the transaction in three steps:
- Creating a transaction builder
- Creating the transaction's components
- Adding the components to the builder
TransactionBuilder
To start building the proposed transaction, we need a
TransactionBuilder
. This is a mutable transaction class to
which we can add inputs, outputs, commands, and any other components the
transaction needs.
We create a TransactionBuilder
in
Initiator.call
as follows:
// Additional import.
import net.corda.core.transactions.TransactionBuilder
...
@Suspendable
override fun call(): SignedTransaction {
// We create a transaction builder
val txBuilder = TransactionBuilder()
val notaryIdentity = serviceHub.networkMapCache.getAnyNotary()
.notary = notaryIdentity
txBuilder}
// Additional import.
import net.corda.core.transactions.TransactionBuilder;
...
@Suspendable
@Override
public SignedTransaction call() throws FlowException {
// We create a transaction builder
final TransactionBuilder txBuilder = new TransactionBuilder();
final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null);
.setNotary(notary);
txBuilder}
In the first line, we create a TransactionBuilder
. We
will also want our transaction to have a notary, in order to prevent
double-spends. In the second line, we retrieve the identity of the
notary who will be notarising our transaction and add it to the
builder.
You can see that the notary's identity is being retrieved from the
node's ServiceHub
. Whenever we need information within a
flow - whether it's about our own node, its contents, or the rest of the
network - we use the node's ServiceHub
. In particular,
ServiceHub.networkMapCache
provides information about the
other nodes on the network and the services that they offer.
Transaction components
Now that we have our TransactionBuilder
, we need to
create its components. Remember that we're trying to build the following
transaction:
- scale
-
15% :align: center
So we'll need the following:
- The output
IOUState
- A
Create
command listing both the IOU's sender and recipient as signers
We create these components as follows:
// Additional import.
import net.corda.core.contracts.Command
...
@Suspendable
override fun call(): SignedTransaction {
// We create a transaction builder
val txBuilder = TransactionBuilder()
val notaryIdentity = serviceHub.networkMapCache.getAnyNotary()
.notary = notaryIdentity
txBuilder
// We create the transaction's components.
val ourIdentity = serviceHub.myInfo.legalIdentity
val iou = IOUState(iouValue, ourIdentity, otherParty)
val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey })
}
// Additional imports.
import com.google.common.collect.ImmutableList;
import net.corda.core.contracts.Command;
import java.security.PublicKey;
import java.util.List;
...
@Suspendable
@Override
public SignedTransaction call() throws FlowException {
// We create a transaction builder
final TransactionBuilder txBuilder = new TransactionBuilder();
final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null);
.setNotary(notary);
txBuilder
// We create the transaction's components.
final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity();
final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty);
final List<PublicKey> signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey());
final Command txCommand = new Command(new IOUContract.Create(), signers);
}
To build the state, we start by retrieving our own identity (again,
we get this information from the ServiceHub
, via
ServiceHub.myInfo
). We then build the
IOUState
, using our identity, the IOUContract
,
and the IOU value and counterparty from the FlowLogic
's
constructor parameters.
We also create the command, which pairs the
IOUContract.Create
command with the public keys of
ourselves and the counterparty. If this command is included in the
transaction, both ourselves and the counterparty will be required
signers.
Adding the components
Finally, we add the items to the transaction using the
TransactionBuilder.withItems
method:
@Suspendable
override fun call(): SignedTransaction {
// We create a transaction builder
val txBuilder = TransactionBuilder()
val notaryIdentity = serviceHub.networkMapCache.getAnyNotary()
.notary = notaryIdentity
txBuilder
// We create the transaction's components.
val ourIdentity = serviceHub.myInfo.legalIdentity
val iou = IOUState(iouValue, ourIdentity, otherParty)
val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey })
// Adding the item's to the builder.
.withItems(iou, txCommand)
txBuilder}
@Suspendable
@Override
public SignedTransaction call() throws FlowException {
// We create a transaction builder
final TransactionBuilder txBuilder = new TransactionBuilder();
final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null);
.setNotary(notary);
txBuilder
// We create the transaction's components.
final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity();
final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty);
final List<PublicKey> signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey());
final Command txCommand = new Command(new IOUContract.Create(), signers);
// Adding the item's to the builder.
.withItems(iou, txCommand);
txBuilder}
TransactionBuilder.withItems
takes a vararg of:
- ContractState objects, which are added to the builder as output states
- StateRef objects (references to the outputs of previous transactions), which are added to the builder as input state references
- Command objects, which are added to the builder as commands
It will modify the TransactionBuilder
in-place to add
these components to it.
Verifying the transaction
We've now built our proposed transaction. Before we sign it, we should check that it represents a valid ledger update proposal by verifying the transaction, which will execute each of the transaction's contracts:
@Suspendable
override fun call(): SignedTransaction {
// We create a transaction builder
val txBuilder = TransactionBuilder()
val notaryIdentity = serviceHub.networkMapCache.getAnyNotary()
.notary = notaryIdentity
txBuilder
// We create the transaction's components.
val ourIdentity = serviceHub.myInfo.legalIdentity
val iou = IOUState(iouValue, ourIdentity, otherParty)
val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey })
// Adding the item's to the builder.
.withItems(iou, txCommand)
txBuilder
// Verifying the transaction.
.toWireTransaction().toLedgerTransaction(serviceHub).verify()
txBuilder}
@Suspendable
@Override
public SignedTransaction call() throws FlowException {
// We create a transaction builder
final TransactionBuilder txBuilder = new TransactionBuilder();
final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null);
.setNotary(notary);
txBuilder
// We create the transaction's components.
final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity();
final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty);
final List<PublicKey> signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey());
final Command txCommand = new Command(new IOUContract.Create(), signers);
// Adding the item's to the builder.
.withItems(iou, txCommand);
txBuilder
// Verifying the transaction.
.toWireTransaction().toLedgerTransaction(getServiceHub()).verify();
txBuilder}
To verify the transaction, we must:
- Convert the builder into an immutable
WireTransaction
- Convert the
WireTransaction
into aLedgerTransaction
using theServiceHub
. This step resolves the transaction's input state references and attachment references into actual states and attachments (in case their contents are needed to verify the transaction - Call
LedgerTransaction.verify
to test whether the transaction is valid based on the contract of every input and output state in the transaction
If the verification fails, we have built an invalid transaction. Our
flow will then end, throwing a
TransactionVerificationException
.
Signing the transaction
Now that we are satisfied that our transaction proposal is valid, we sign it. Once the transaction is signed, no-one will be able to modify the transaction without invalidating our signature. This effectively makes the transaction immutable.
@Suspendable
override fun call(): SignedTransaction {
// We create a transaction builder
val txBuilder = TransactionBuilder()
val notaryIdentity = serviceHub.networkMapCache.getAnyNotary()
.notary = notaryIdentity
txBuilder
// We create the transaction's components.
val ourIdentity = serviceHub.myInfo.legalIdentity
val iou = IOUState(iouValue, ourIdentity, otherParty)
val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey })
// Adding the item's to the builder.
.withItems(iou, txCommand)
txBuilder
// Verifying the transaction.
.toWireTransaction().toLedgerTransaction(serviceHub).verify()
txBuilder
// Signing the transaction.
val partSignedTx = serviceHub.signInitialTransaction(txBuilder)
}
@Suspendable
@Override
public SignedTransaction call() throws FlowException {
// We create a transaction builder
final TransactionBuilder txBuilder = new TransactionBuilder();
final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null);
.setNotary(notary);
txBuilder
// We create the transaction's components.
final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity();
final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty);
final List<PublicKey> signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey());
final Command txCommand = new Command(new IOUContract.Create(), signers);
// Adding the item's to the builder.
.withItems(iou, txCommand);
txBuilder
// Verifying the transaction.
.toWireTransaction().toLedgerTransaction(getServiceHub()).verify();
txBuilder
// Signing the transaction.
final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder);
}
The call to ServiceHub.signInitialTransaction
returns a
SignedTransaction
- an object that pairs the transaction
itself with a list of signatures over that transaction.
We can now safely send the builder to our counterparty. If the counterparty tries to modify the transaction, the transaction's hash will change, our digital signature will no longer be valid, and the transaction will not be accepted as a valid ledger update.
Gathering counterparty signatures
The final step in order to create a valid transaction proposal is to
collect the counterparty's signature. As discussed, we can automate this
process by invoking the built-in CollectSignaturesFlow
:
// Additional import.
import net.corda.flows.CollectSignaturesFlow
...
@Suspendable
override fun call(): SignedTransaction {
// We create a transaction builder
val txBuilder = TransactionBuilder()
val notaryIdentity = serviceHub.networkMapCache.getAnyNotary()
.notary = notaryIdentity
txBuilder
// We create the transaction's components.
val ourIdentity = serviceHub.myInfo.legalIdentity
val iou = IOUState(iouValue, ourIdentity, otherParty)
val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey })
// Adding the item's to the builder.
.withItems(iou, txCommand)
txBuilder
// Verifying the transaction.
.toWireTransaction().toLedgerTransaction(serviceHub).verify()
txBuilder
// Signing the transaction.
val partSignedTx = serviceHub.signInitialTransaction(txBuilder)
// Gathering the signatures.
val signedTx = subFlow(CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.tracker()))
}
// Additional import.
import net.corda.flows.CollectSignaturesFlow;
...
@Suspendable
@Override
public SignedTransaction call() throws FlowException {
// We create a transaction builder
final TransactionBuilder txBuilder = new TransactionBuilder();
final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null);
.setNotary(notary);
txBuilder
// We create the transaction's components.
final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity();
final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty);
final List<PublicKey> signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey());
final Command txCommand = new Command(new IOUContract.Create(), signers);
// Adding the item's to the builder.
.withItems(iou, txCommand);
txBuilder
// Verifying the transaction.
.toWireTransaction().toLedgerTransaction(getServiceHub()).verify();
txBuilder
// Signing the transaction.
final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder);
// Gathering the signatures.
final SignedTransaction signedTx = subFlow(
new CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.Companion.tracker()));
}
CollectSignaturesFlow
gathers signatures from every
participant listed on the transaction, and returns a
SignedTransaction
with all the required signatures.
Finalising the transaction
We now have a valid transaction signed by all the required parties.
All that's left to do is to have it notarised and recorded by all the
relevant parties. From then on, it will become a permanent part of the
ledger. Again, instead of handling this process manually, we'll use a
built-in flow called FinalityFlow
:
// Additional import.
import net.corda.flows.FinalityFlow
...
@Suspendable
override fun call(): SignedTransaction {
// We create a transaction builder
val txBuilder = TransactionBuilder()
val notaryIdentity = serviceHub.networkMapCache.getAnyNotary()
.notary = notaryIdentity
txBuilder
// We create the transaction's components.
val ourIdentity = serviceHub.myInfo.legalIdentity
val iou = IOUState(iouValue, ourIdentity, otherParty)
val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey })
// Adding the item's to the builder.
.withItems(iou, txCommand)
txBuilder
// Verifying the transaction.
.toWireTransaction().toLedgerTransaction(serviceHub).verify()
txBuilder
// Signing the transaction.
val partSignedTx = serviceHub.signInitialTransaction(txBuilder)
// Gathering the signatures.
val signedTx = subFlow(CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.tracker()))
// Finalising the transaction.
return subFlow(FinalityFlow(signedTx)).single()
}
// Additional import.
import net.corda.flows.FinalityFlow;
...
@Suspendable
@Override
public SignedTransaction call() throws FlowException {
// We create a transaction builder
final TransactionBuilder txBuilder = new TransactionBuilder();
final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null);
.setNotary(notary);
txBuilder
// We create the transaction's components.
final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity();
final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty);
final List<PublicKey> signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey());
final Command txCommand = new Command(new IOUContract.Create(), signers);
// Adding the item's to the builder.
.withItems(iou, txCommand);
txBuilder
// Verifying the transaction.
.toWireTransaction().toLedgerTransaction(getServiceHub()).verify();
txBuilder
// Signing the transaction.
final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder);
// Gathering the signatures.
final SignedTransaction signedTx = subFlow(
new CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.Companion.tracker()));
// Finalising the transaction.
return subFlow(new FinalityFlow(signedTx)).get(0);
}
FinalityFlow
completely automates the process of:
- Notarising the transaction
- Recording it in our vault
- Sending it to the counterparty for them to record as well
FinalityFlow
also returns a list of the notarised
transactions. We extract the single item from this list and return
it.
That completes the initiator side of the flow.
Writing the acceptor's flow
The acceptor's side of the flow is much simpler. We need to:
- Receive a signed transaction from the counterparty
- Verify the transaction
- Sign the transaction
- Send the updated transaction back to the counterparty
As we just saw, the process of building and finalising the transaction will be completely handled by the initiator flow.
SignTransactionFlow
We can automate all four steps of the acceptor's flow by invoking
SignTransactionFlow
. SignTransactionFlow
is a
flow that is registered by default on every node to respond to messages
from CollectSignaturesFlow
(which is invoked by the
initiator flow).
As SignTransactionFlow
is an abstract class, we have to
subclass it and override
SignTransactionFlow.checkTransaction
:
// Additional import.
import net.corda.flows.SignTransactionFlow
...
@InitiatedBy(Initiator::class)
class Acceptor(val otherParty: Party) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
// Stage 1 - Verifying and signing the transaction.
(object : SignTransactionFlow(otherParty, tracker()) {
subFlowoverride fun checkTransaction(stx: SignedTransaction) {
// Define custom verification logic here.
}
})
}
}
// Additional import.
import net.corda.flows.SignTransactionFlow;
...
@InitiatedBy(Initiator.class)
public static class Acceptor extends FlowLogic<Void> {
private final Party otherParty;
public Acceptor(Party otherParty) {
this.otherParty = otherParty;
}
@Suspendable
@Override
public Void call() throws FlowException {
// Stage 1 - Verifying and signing the transaction.
class signTxFlow extends SignTransactionFlow {
private signTxFlow(Party otherParty, ProgressTracker progressTracker) {
super(otherParty, progressTracker);
}
@Override
protected void checkTransaction(SignedTransaction signedTransaction) {
// Define custom verification logic here.
}
}
subFlow(new signTxFlow(otherParty, SignTransactionFlow.Companion.tracker()));
return null;
}
}
SignTransactionFlow
already checks the transaction's
signatures, and whether the transaction is contractually valid. The
purpose of SignTransactionFlow.checkTransaction
is to
define any additional verification of the transaction that we wish to
perform before we sign it. For example, we may want to:
- Check that the transaction contains an
IOUState
- Check that the IOU's value isn't too high
Well done! You've finished the flows!
Flow tests
As with contracts, deploying nodes to manually test flows is not efficient. Instead, we can use Corda's flow-test DSL to quickly test our flows. The flow-test DSL works by creating a network of lightweight, "mock" node implementations on which we run our flows.
The first thing we need to do is create this mock network. Open
either test/kotlin/com/template/flow/FlowTests.kt
or
test/java/com/template/contract/ContractTests.java
, and
overwrite the existing code with:
package com.template
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.getOrThrow
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetwork.MockNode
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class IOUFlowTests {
lateinit var net: MockNetwork
lateinit var a: MockNode
lateinit var b: MockNode
lateinit var c: MockNode
@Before
fun setup() {
= MockNetwork()
net val nodes = net.createSomeNodes(2)
= nodes.partyNodes[0]
a = nodes.partyNodes[1]
b .registerInitiatedFlow(IOUFlow.Acceptor::class.java)
b()
net.runNetwork
}
@Afterfun tearDown() {
.stopNodes()
net}
}
package com.template;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
import net.corda.core.contracts.ContractState;
import net.corda.core.contracts.TransactionState;
import net.corda.core.contracts.TransactionVerificationException;
import net.corda.core.transactions.SignedTransaction;
import net.corda.testing.node.MockNetwork;
import net.corda.testing.node.MockNetwork.BasketOfNodes;
import net.corda.testing.node.MockNetwork.MockNode;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.util.List;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.assertEquals;
public class IOUFlowTests {
private MockNetwork net;
private MockNode a;
private MockNode b;
@Before
public void setup() {
= new MockNetwork();
net = net.createSomeNodes(2);
BasketOfNodes nodes = nodes.getPartyNodes().get(0);
a = nodes.getPartyNodes().get(1);
b .registerInitiatedFlow(IOUFlow.Acceptor.class);
b.runNetwork();
net}
@After
public void tearDown() {
.stopNodes();
net}
@Rule
public final ExpectedException exception = ExpectedException.none();
}
This creates an in-memory network with mocked-out components. The
network has two nodes, plus network map and notary nodes. We register
any responder flows (in our case, IOUFlow.Acceptor
) on our
nodes as well.
Our first test will be to check that the flow rejects invalid IOUs:
@Test
fun `flow rejects invalid IOUs`() {
val flow = IOUFlow.Initiator(-1, b.info.legalIdentity)
val future = a.services.startFlow(flow).resultFuture
.runNetwork()
net
// The IOUContract specifies that IOUs cannot have negative values.
<TransactionVerificationException> {future.getOrThrow()}
assertFailsWith}
@Test
public void flowRejectsInvalidIOUs() throws Exception {
.Initiator flow = new IOUFlow.Initiator(-1, b.info.getLegalIdentity());
IOUFlow<SignedTransaction> future = a.getServices().startFlow(flow).getResultFuture();
ListenableFuture.runNetwork();
net
.expectCause(instanceOf(TransactionVerificationException.class));
exception.get();
future}
This code causes node A to run the IOUFlow.Initiator
flow. The call to MockNetwork.runNetwork
is required to
simulate the running of a real network.
We then assert that because we passed in a negative IOU value to the
flow's constructor, the flow should fail with a
TransactionVerificationException
. In other words, we are
asserting that at some point in flow, the transaction is verified
(remember that IOUContract
forbids negative value IOUs),
causing the flow to fail.
Because flows need to be instrumented by a library called Quasar that allows
the flows to be checkpointed and serialized to disk, you need to run
these tests using the provided Run Flow Tests - Java
or
Run Flow Tests - Kotlin
run-configurations.
Here is the full suite of tests we'll use for the
IOUFlow
:
@Test
fun `flow rejects invalid IOUs`() {
val flow = IOUFlow.Initiator(-1, b.info.legalIdentity)
val future = a.services.startFlow(flow).resultFuture
.runNetwork()
net
// The IOUContract specifies that IOUs cannot have negative values.
<TransactionVerificationException> {future.getOrThrow()}
assertFailsWith}
@Test
fun `SignedTransaction returned by the flow is signed by the initiator`() {
val flow = IOUFlow.Initiator(1, b.info.legalIdentity)
val future = a.services.startFlow(flow).resultFuture
.runNetwork()
net
val signedTx = future.getOrThrow()
.verifySignatures(b.services.legalIdentityKey)
signedTx}
@Test
fun `SignedTransaction returned by the flow is signed by the acceptor`() {
val flow = IOUFlow.Initiator(1, b.info.legalIdentity)
val future = a.services.startFlow(flow).resultFuture
.runNetwork()
net
val signedTx = future.getOrThrow()
.verifySignatures(a.services.legalIdentityKey)
signedTx}
@Test
fun `flow records a transaction in both parties' vaults`() {
val flow = IOUFlow.Initiator(1, b.info.legalIdentity)
val future = a.services.startFlow(flow).resultFuture
.runNetwork()
netval signedTx = future.getOrThrow()
// We check the recorded transaction in both vaults.
for (node in listOf(a, b)) {
(signedTx, node.storage.validatedTransactions.getTransaction(signedTx.id))
assertEquals}
}
@Test
fun `recorded transaction has no inputs and a single output, the input IOU`() {
val flow = IOUFlow.Initiator(1, b.info.legalIdentity)
val future = a.services.startFlow(flow).resultFuture
.runNetwork()
netval signedTx = future.getOrThrow()
// We check the recorded transaction in both vaults.
for (node in listOf(a, b)) {
val recordedTx = node.storage.validatedTransactions.getTransaction(signedTx.id)
val txOutputs = recordedTx!!.tx.outputs
(txOutputs.size == 1)
assert
val recordedState = txOutputs[0].data as IOUState
(recordedState.value, 1)
assertEquals(recordedState.sender, a.info.legalIdentity)
assertEquals(recordedState.recipient, b.info.legalIdentity)
assertEquals}
}
@Test
public void flowRejectsInvalidIOUs() throws Exception {
.Initiator flow = new IOUFlow.Initiator(-1, b.info.getLegalIdentity());
IOUFlow<SignedTransaction> future = a.getServices().startFlow(flow).getResultFuture();
ListenableFuture.runNetwork();
net
.expectCause(instanceOf(TransactionVerificationException.class));
exception.get();
future}
@Test
public void signedTransactionReturnedByTheFlowIsSignedByTheInitiator() throws Exception {
.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity());
IOUFlow<SignedTransaction> future = a.getServices().startFlow(flow).getResultFuture();
ListenableFuture.runNetwork();
net
= future.get();
SignedTransaction signedTx .verifySignatures(b.getServices().getLegalIdentityKey());
signedTx}
@Test
public void signedTransactionReturnedByTheFlowIsSignedByTheAcceptor() throws Exception {
.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity());
IOUFlow<SignedTransaction> future = a.getServices().startFlow(flow).getResultFuture();
ListenableFuture.runNetwork();
net
= future.get();
SignedTransaction signedTx .verifySignatures(a.getServices().getLegalIdentityKey());
signedTx}
@Test
public void flowRecordsATransactionInBothPartiesVaults() throws Exception {
.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity());
IOUFlow<SignedTransaction> future = a.getServices().startFlow(flow).getResultFuture();
ListenableFuture.runNetwork();
net= future.get();
SignedTransaction signedTx
for (MockNode node : ImmutableList.of(a, b)) {
assertEquals(signedTx, node.storage.getValidatedTransactions().getTransaction(signedTx.getId()));
}
}
@Test
public void recordedTransactionHasNoInputsAndASingleOutputTheInputIOU() throws Exception {
.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity());
IOUFlow<SignedTransaction> future = a.getServices().startFlow(flow).getResultFuture();
ListenableFuture.runNetwork();
net= future.get();
SignedTransaction signedTx
for (MockNode node : ImmutableList.of(a, b)) {
= node.storage.getValidatedTransactions().getTransaction(signedTx.getId());
SignedTransaction recordedTx List<TransactionState<ContractState>> txOutputs = recordedTx.getTx().getOutputs();
assert(txOutputs.size() == 1);
= (IOUState) txOutputs.get(0).getData();
IOUState recordedState assert(recordedState.getValue() == 1);
assertEquals(recordedState.getSender(), a.info.getLegalIdentity());
assertEquals(recordedState.getRecipient(), b.info.getLegalIdentity());
}
}
Run these tests and make sure they all pass. If they do, its very likely that we have a working CorDapp.
Progress so far
We now have a flow that we can kick off on our node to completely
automate the process of issuing an IOU onto the ledger. Under the hood,
this flow takes the form of two communicating FlowLogic
subclasses.
We now have a complete CorDapp, made up of:
- The
IOUState
, representing IOUs on the ledger - The
IOUContract
, controlling the evolution ofIOUState
objects over time - The
IOUFlow
, which transforms the creation of a new IOU on the ledger into a push-button process
The final step is to spin up some nodes and test our CorDapp.