Applies tutorial fixes back onto M14.

This commit is contained in:
Joel Dudley 2017-08-16 12:04:53 +01:00 committed by GitHub
parent 129e5088cf
commit 8cedce735f
9 changed files with 110 additions and 151 deletions

View File

@ -67,7 +67,7 @@ transfer them or redeem them for cash. One way to enforce this behaviour would b
* Its value must be non-negative * Its value must be non-negative
* The lender and the borrower cannot be the same entity * The lender and the borrower cannot be the same entity
* The IOU's borrower must sign the transaction * The IOU's lender must sign the transaction
We can picture this transaction as follows: We can picture this transaction as follows:
@ -122,7 +122,6 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi
package com.template.contract; package com.template.contract;
import com.google.common.collect.ImmutableSet;
import com.template.state.IOUState; import com.template.state.IOUState;
import net.corda.core.contracts.AuthenticatedObject; import net.corda.core.contracts.AuthenticatedObject;
import net.corda.core.contracts.CommandData; import net.corda.core.contracts.CommandData;
@ -155,7 +154,7 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi
check.using("The lender and the borrower cannot be the same entity.", lender != borrower); check.using("The lender and the borrower cannot be the same entity.", lender != borrower);
// Constraints on the signers. // Constraints on the signers.
check.using("There must only be one signer.", ImmutableSet.of(command.getSigners()).size() == 1); check.using("There must only be one signer.", command.getSigners().size() == 1);
check.using("The signer must be the lender.", command.getSigners().contains(lender.getOwningKey())); check.using("The signer must be the lender.", command.getSigners().contains(lender.getOwningKey()));
return null; return null;
@ -179,7 +178,7 @@ The first thing we add to our contract is a *command*. Commands serve two functi
example, a transaction proposing the creation of an IOU could have to satisfy different constraints to one redeeming example, a transaction proposing the creation of an IOU could have to satisfy different constraints to one redeeming
an IOU an IOU
* They allow us to define the required signers for the transaction. For example, IOU creation might require signatures * They allow us to define the required signers for the transaction. For example, IOU creation might require signatures
from the borrower alone, whereas the transfer of an IOU might require signatures from both the IOU's borrower and lender from the lender only, whereas the transfer of an IOU might require signatures from both the IOU's borrower and lender
Our contract has one command, a ``Create`` command. All commands must implement the ``CommandData`` interface. Our contract has one command, a ``Create`` command. All commands must implement the ``CommandData`` interface.
@ -219,7 +218,7 @@ following are true:
* The transaction has inputs * The transaction has inputs
* The transaction doesn't have exactly one output * The transaction doesn't have exactly one output
* The IOU itself is invalid * The IOU itself is invalid
* The transaction doesn't require the borrower's signature * The transaction doesn't require the lender's signature
Command constraints Command constraints
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
@ -270,7 +269,7 @@ We've now written an ``IOUContract`` constraining the evolution of each ``IOUSta
* Creating an ``IOUState`` requires an issuance transaction with no inputs, a single ``IOUState`` output, and a * Creating an ``IOUState`` requires an issuance transaction with no inputs, a single ``IOUState`` output, and a
``Create`` command ``Create`` command
* The ``IOUState`` created by the issuance transaction must have a non-negative value, and the lender and borrower * The ``IOUState`` created by the issuance transaction must have a non-negative value, and the lender and borrower
must be different entities. must be different entities
Before we move on, make sure you go back and modify ``IOUState`` to point to the new ``IOUContract`` class. Before we move on, make sure you go back and modify ``IOUState`` to point to the new ``IOUContract`` class.

View File

@ -16,27 +16,25 @@ Kotlin) file. We won't be using it, and it will cause build errors unless we rem
Deploying our CorDapp Deploying our CorDapp
--------------------- ---------------------
Let's take a look at the nodes we're going to deploy. Open the project's build file under ``java-source/build.gradle`` Let's take a look at the nodes we're going to deploy. Open the project's ``build.gradle`` file and scroll down to the
or ``kotlin-source/build.gradle`` and scroll down to the ``task deployNodes`` section. This section defines four ``task deployNodes`` section. This section defines three nodes - the Controller, NodeA, and NodeB:
nodes - the Controller, and NodeA, NodeB and NodeC:
.. container:: codeset .. container:: codeset
.. code-block:: kotlin .. code-block:: kotlin
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
directory "./build/nodes" directory "./build/nodes"
networkMap "CN=Controller,O=R3,OU=corda,L=London,C=GB" networkMap "CN=Controller,O=R3,OU=corda,L=London,C=UK"
node { node {
name "CN=Controller,O=R3,OU=corda,L=London,C=GB" name "CN=Controller,O=R3,OU=corda,L=London,C=UK"
advertisedServices = ["corda.notary.validating"] advertisedServices = ["corda.notary.validating"]
p2pPort 10002 p2pPort 10002
rpcPort 10003 rpcPort 10003
webPort 10004
cordapps = [] cordapps = []
} }
node { node {
name "CN=NodeA,O=NodeA,L=London,C=GB" name "CN=NodeA,O=NodeA,L=London,C=UK"
advertisedServices = [] advertisedServices = []
p2pPort 10005 p2pPort 10005
rpcPort 10006 rpcPort 10006
@ -53,15 +51,6 @@ nodes - the Controller, and NodeA, NodeB and NodeC:
cordapps = [] cordapps = []
rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] rpcUsers = [[ user: "user1", "password": "test", "permissions": []]]
} }
node {
name "CN=NodeC,O=NodeC,L=Paris,C=FR"
advertisedServices = []
p2pPort 10011
rpcPort 10012
webPort 10013
cordapps = []
rpcUsers = [[ user: "user1", "password": "test", "permissions": []]]
}
} }
We have three standard nodes, plus a special Controller node that is running the network map service, and is also We have three standard nodes, plus a special Controller node that is running the network map service, and is also
@ -85,8 +74,8 @@ We can do that now by running the following commands from the root of the projec
Running the nodes Running the nodes
----------------- -----------------
Running ``deployNodes`` will build the nodes under both ``java-source/build/nodes`` and ``kotlin-source/build/nodes``. Running ``deployNodes`` will build the nodes under ``build/nodes``. If we navigate to one of these folders, we'll see
If we navigate to one of these folders, we'll see four node folder. Each node folder has the following structure: the three node folders. Each node folder has the following structure:
.. code:: python .. code:: python
@ -102,17 +91,11 @@ Let's start the nodes by running the following commands from the root of the pro
.. code:: python .. code:: python
// On Windows for a Java CorDapp // On Windows
java-source/build/nodes/runnodes.bat build/nodes/runnodes.bat
// On Windows for a Kotlin CorDapp // On Mac
kotlin-source/build/nodes/runnodes.bat build/nodes/runnodes
// On Mac for a Java CorDapp
java-source/build/nodes/runnodes
// On Mac for a Kotlin CorDapp
kotlin-source/build/nodes/runnodes
This will start a terminal window for each node, and an additional terminal window for each node's webserver - eight This will start a terminal window for each node, and an additional terminal window for each node's webserver - eight
terminal windows in all. Give each node a moment to start - you'll know it's ready when its terminal windows displays terminal windows in all. Give each node a moment to start - you'll know it's ready when its terminal windows displays
@ -143,10 +126,8 @@ We want to create an IOU of 100 with Node B. We start the ``IOUFlow`` by typing:
start IOUFlow iouValue: 99, otherParty: "NodeB" start IOUFlow iouValue: 99, otherParty: "NodeB"
Node A and Node B will automatically agree an IOU. Node A and Node B will automatically agree an IOU. If the flow worked, it should have led to the recording of a new IOU
in the vaults of both Node A and Node B.
If the flow worked, it should have led to the recording of a new IOU in the vaults of both Node A and Node B. Equally
importantly, Node C - although it sits on the same network - should not be aware of this transaction.
We can check the flow has worked by using an RPC operation to check the contents of each node's vault. Typing ``run`` We can check the flow has worked by using an RPC operation to check the contents of each node's vault. Typing ``run``
will display a list of the available commands. We can examine the contents of a node's vault by running: will display a list of the available commands. We can examine the contents of a node's vault by running:
@ -183,13 +164,6 @@ The vaults of Node A and Node B should both display the following output:
index: 0 index: 0
second: "(observable)" second: "(observable)"
But the vault of Node C should output nothing!
.. code:: python
first: []
second: "(observable)"
Conclusion Conclusion
---------- ----------
We have written a simple CorDapp that allows IOUs to be issued onto the ledger. Like all CorDapps, our We have written a simple CorDapp that allows IOUs to be issued onto the ledger. Like all CorDapps, our

View File

@ -40,22 +40,25 @@ Open a terminal window in the directory where you want to download the CorDapp t
Template structure Template structure
------------------ ------------------
We can write our CorDapp in either Java or Kotlin, and will be providing the code in both languages throughout. To We can write our CorDapp in either Java or Kotlin, and will be providing the code in both languages throughout. To
implement our IOU CorDapp in Java, we'll only need to modify three files: implement our IOU CorDapp in Java, we'll need to modify three files. For Kotlin, we'll simply be modifying the
``App.kt`` file:
.. container:: codeset .. container:: codeset
.. code-block:: java .. code-block:: java
// 1. The state // 1. The state
java-source/src/main/java/com/template/state/TemplateState.java src/main/java/com/template/state/TemplateState.java
// 2. The contract // 2. The contract
java-source/src/main/java/com/template/contract/TemplateContract.java src/main/java/com/template/contract/TemplateContract.java
// 3. The flow // 3. The flow
java-source/src/main/java/com/template/flow/TemplateFlow.java src/main/java/com/template/flow/TemplateFlow.java
For Kotlin, we'll simply be modifying the ``App.kt`` file. .. code-block:: kotlin
src/main/kotlin/com/template/App.kt
Progress so far Progress so far
--------------- ---------------

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 208 KiB

View File

@ -11,23 +11,33 @@ Remember that each state references a contract. The contract imposes constraints
If the transaction does not obey the constraints of all the contracts of all its states, it cannot become a valid If the transaction does not obey the constraints of all the contracts of all its states, it cannot become a valid
ledger update. ledger update.
We need to modify our contract so that the lender's signature is required in any IOU creation transaction. This will We need to modify our contract so that the borrower's signature is required in any IOU creation transaction. This will
only require changing a single line of code. In ``IOUContract.java``/``IOUContract.kt``, update the final line of the only require changing a single line of code. In ``IOUContract.java``/``IOUContract.kt``, update the final two lines of
``requireThat`` block as follows: the ``requireThat`` block as follows:
.. container:: codeset .. container:: codeset
.. code-block:: kotlin .. code-block:: kotlin
// Constraints on the signers.
"There must be two signers." using (command.signers.toSet().size == 2)
"The borrower and lender must be signers." using (command.signers.containsAll(listOf( "The borrower and lender must be signers." using (command.signers.containsAll(listOf(
out.borrower.owningKey, out.lender.owningKey))) out.borrower.owningKey, out.lender.owningKey)))
.. code-block:: java .. code-block:: java
...
import com.google.common.collect.ImmutableList;
...
// Constraints on the signers.
check.using("There must be two signers.", command.getSigners().size() == 2);
check.using("The borrower and lender must be signers.", command.getSigners().containsAll( check.using("The borrower and lender must be signers.", command.getSigners().containsAll(
ImmutableList.of(borrower.getOwningKey(), lender.getOwningKey()))); ImmutableList.of(borrower.getOwningKey(), lender.getOwningKey())));
Progress so far Progress so far
--------------- ---------------
Our contract now imposes an additional constraint - the lender must also sign an IOU creation transaction. Next, we Our contract now imposes an additional constraint - the borrower must also sign an IOU creation transaction. Next, we
need to update ``IOUFlow`` so that it actually gathers the counterparty's signature as part of the flow. need to update ``IOUFlow`` so that it actually gathers the borrower's signature as part of the flow.

View File

@ -9,17 +9,17 @@ Updating the flow
To update the flow, we'll need to do two things: To update the flow, we'll need to do two things:
* Update the borrower's side of the flow to request the lender's signature * Update the lender's side of the flow to request the borrower's signature
* Create a flow for the lender to run in response to a signature request from the borrower * Create a flow for the borrower to run in response to a signature request from the lender
Updating the borrower's flow Updating the lender's flow
---------------------------- --------------------------
In the original CorDapp, we automated the process of notarising a transaction and recording it in every party's vault In the original CorDapp, we automated the process of notarising a transaction and recording it in every party's vault
by invoking a built-in flow called ``FinalityFlow`` as a subflow. We're going to use another pre-defined flow, called by invoking a built-in flow called ``FinalityFlow`` as a subflow. We're going to use another pre-defined flow, called
``CollectSignaturesFlow``, to gather the lender's signature. ``CollectSignaturesFlow``, to gather the borrower's signature.
We also need to add the lender's public key to the transaction's command, making the lender one of the required signers We also need to add the borrower's public key to the transaction's command, making the borrower one of the required
on the transaction. signers on the transaction.
In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows: In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows:
@ -27,6 +27,8 @@ In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows:
.. code-block:: kotlin .. code-block:: kotlin
...
// We add the items to the builder. // We add the items to the builder.
val state = IOUState(iouValue, me, otherParty) val state = IOUState(iouValue, me, otherParty)
val cmd = Command(IOUContract.Create(), listOf(me.owningKey, otherParty.owningKey)) val cmd = Command(IOUContract.Create(), listOf(me.owningKey, otherParty.owningKey))
@ -39,13 +41,21 @@ In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows:
val signedTx = serviceHub.signInitialTransaction(txBuilder) val signedTx = serviceHub.signInitialTransaction(txBuilder)
// Obtaining the counterparty's signature // Obtaining the counterparty's signature
val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx)) val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, CollectSignaturesFlow.tracker()))
// Finalising the transaction. // Finalising the transaction.
subFlow(FinalityFlow(fullySignedTx)) subFlow(FinalityFlow(fullySignedTx))
.. code-block:: java .. code-block:: java
...
import com.google.common.collect.ImmutableList;
import java.security.PublicKey;
import java.util.List;
...
// We add the items to the builder. // We add the items to the builder.
IOUState state = new IOUState(iouValue, me, otherParty); IOUState state = new IOUState(iouValue, me, otherParty);
List<PublicKey> requiredSigners = ImmutableList.of(me.getOwningKey(), otherParty.getOwningKey()); List<PublicKey> requiredSigners = ImmutableList.of(me.getOwningKey(), otherParty.getOwningKey());
@ -59,62 +69,39 @@ In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows:
final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);
// Obtaining the counterparty's signature // Obtaining the counterparty's signature
final SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(signedTx, null)); final SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(signedTx, CollectSignaturesFlow.Companion.tracker()));
// Finalising the transaction. // Finalising the transaction.
subFlow(new FinalityFlow(fullySignedTx)); subFlow(new FinalityFlow(fullySignedTx));
To make the lender a required signer, we simply add the lender's public key to the list of signers on the command. return null;
To make the borrower a required signer, we simply add the borrower's public key to the list of signers on the command.
``CollectSignaturesFlow``, meanwhile, takes a transaction signed by the flow initiator, and returns a transaction ``CollectSignaturesFlow``, meanwhile, takes a transaction signed by the flow initiator, and returns a transaction
signed by all the transaction's other required signers. We then pass this fully-signed transaction into signed by all the transaction's other required signers. We then pass this fully-signed transaction into
``FinalityFlow``. ``FinalityFlow``.
The lender's flow Creating the borrower's flow
----------------- ----------------------------
Reorganising our class
^^^^^^^^^^^^^^^^^^^^^^
Before we define the lender's flow, let's reorganise ``IOUFlow.java``/``IOUFlow.kt`` a little bit:
* Rename ``IOUFlow`` to ``Initiator``
* In Java, make the ``Initiator`` class static, rename its constructor to match the new name, and move the definition
inside an enclosing ``IOUFlow`` class
* In Kotlin, move the definition of ``Initiator`` class inside an enclosing ``IOUFlow`` singleton object
We will end up with the following structure:
.. container:: codeset
.. code-block:: kotlin
object IOUFlow {
@InitiatingFlow
@StartableByRPC
class Initiator(val iouValue: Int,
val otherParty: Party) : FlowLogic<Unit>() {
.. code-block:: java
public class IOUFlow {
@InitiatingFlow
@StartableByRPC
public static class Initiator extends FlowLogic<Void> {
Writing the lender's flow
^^^^^^^^^^^^^^^^^^^^^^^^^
We're now ready to write the lender's flow, which will respond to the borrower's attempt to gather our signature. We're now ready to write the lender's flow, which will respond to the borrower's attempt to gather our signature.
In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file in Kotlin, add the following class:
Inside the ``IOUFlow`` class/singleton object, add the following class:
.. container:: codeset .. container:: codeset
.. code-block:: kotlin .. code-block:: kotlin
@InitiatedBy(Initiator::class) ...
class Acceptor(val otherParty: Party) : FlowLogic<Unit>() {
import net.corda.core.transactions.SignedTransaction
...
@InitiatedBy(IOUFlow::class)
class IOUFlowResponder(val otherParty: Party) : FlowLogic<Unit>() {
@Suspendable @Suspendable
override fun call() { override fun call() {
val signTransactionFlow = object : SignTransactionFlow(otherParty) { val signTransactionFlow = object : SignTransactionFlow(otherParty, SignTransactionFlow.tracker()) {
override fun checkTransaction(stx: SignedTransaction) = requireThat { override fun checkTransaction(stx: SignedTransaction) = requireThat {
val output = stx.tx.outputs.single().data val output = stx.tx.outputs.single().data
"This must be an IOU transaction." using (output is IOUState) "This must be an IOU transaction." using (output is IOUState)
@ -129,12 +116,26 @@ Inside the ``IOUFlow`` class/singleton object, add the following class:
.. code-block:: java .. code-block:: java
@InitiatedBy(Initiator.class) package com.template.flow;
public static class Acceptor extends FlowLogic<Void> {
import co.paralleluniverse.fibers.Suspendable;
import com.template.state.IOUState;
import net.corda.core.contracts.ContractState;
import net.corda.core.flows.FlowException;
import net.corda.core.flows.FlowLogic;
import net.corda.core.flows.InitiatedBy;
import net.corda.core.flows.SignTransactionFlow;
import net.corda.core.identity.Party;
import net.corda.core.transactions.SignedTransaction;
import net.corda.core.utilities.ProgressTracker;
import static net.corda.core.contracts.ContractsDSL.requireThat;
@InitiatedBy(IOUFlow.class)
public class IOUFlowResponder extends FlowLogic<Void> {
private final Party otherParty; private final Party otherParty;
public Acceptor(Party otherParty) { public IOUFlowResponder(Party otherParty) {
this.otherParty = otherParty; this.otherParty = otherParty;
} }
@ -142,8 +143,8 @@ Inside the ``IOUFlow`` class/singleton object, add the following class:
@Override @Override
public Void call() throws FlowException { public Void call() throws FlowException {
class signTxFlow extends SignTransactionFlow { class signTxFlow extends SignTransactionFlow {
private signTxFlow(Party otherParty) { private signTxFlow(Party otherParty, ProgressTracker progressTracker) {
super(otherParty, null); super(otherParty, progressTracker);
} }
@Override @Override
@ -158,18 +159,19 @@ Inside the ``IOUFlow`` class/singleton object, add the following class:
} }
} }
subFlow(new signTxFlow(otherParty)); subFlow(new signTxFlow(otherParty, SignTransactionFlow.Companion.tracker()));
return null; return null;
} }
} }
As with the ``Initiator``, our ``Acceptor`` flow is a ``FlowLogic`` subclass where we've overridden ``FlowLogic.call``. As with the ``IOUFlow``, our ``IOUFlowResponder`` flow is a ``FlowLogic`` subclass where we've overridden
``FlowLogic.call``.
The flow is annotated with ``InitiatedBy(Initiator.class)``, which means that your node will invoke ``Acceptor.call`` The flow is annotated with ``InitiatedBy(IOUFlow.class)``, which means that your node will invoke
when it receives a message from a instance of ``Initiator`` running on another node. What will this message from the ``IOUFlowResponder.call`` when it receives a message from a instance of ``Initiator`` running on another node. What
``Initiator`` be? If we look at the definition of ``CollectSignaturesFlow``, we can see that we'll be sent a will this message from the ``IOUFlow`` be? If we look at the definition of ``CollectSignaturesFlow``, we can see that
``SignedTransaction``, and are expected to send back our signature over that transaction. we'll be sent a ``SignedTransaction``, and are expected to send back our signature over that transaction.
We could handle this manually. However, there is also a pre-defined flow called ``SignTransactionFlow`` that can handle We could handle this manually. However, there is also a pre-defined flow called ``SignTransactionFlow`` that can handle
this process for us automatically. ``SignTransactionFlow`` is an abstract class, and we must subclass it and override this process for us automatically. ``SignTransactionFlow`` is an abstract class, and we must subclass it and override
@ -179,7 +181,7 @@ Once we've defined the subclass, we invoke it using ``FlowLogic.subFlow``, and t
and the lender's flow is conducted automatically. and the lender's flow is conducted automatically.
CheckTransactions CheckTransactions
~~~~~~~~~~~~~~~~~ ^^^^^^^^^^^^^^^^^
``SignTransactionFlow`` will automatically verify the transaction and its signatures before signing it. However, just ``SignTransactionFlow`` will automatically verify the transaction and its signatures before signing it. However, just
because a transaction is valid doesn't mean we necessarily want to sign. What if we don't want to deal with the because a transaction is valid doesn't mean we necessarily want to sign. What if we don't want to deal with the
counterparty in question, or the value is too high, or we're not happy with the transaction's structure? counterparty in question, or the value is too high, or we're not happy with the transaction's structure?
@ -200,4 +202,4 @@ We can now run our updated CorDapp, using the instructions :doc:`here <hello-wor
Our CorDapp now requires agreement from both the lender and the borrower before an IOU can be created on the ledger. Our CorDapp now requires agreement from both the lender and the borrower before an IOU can be created on the ledger.
This prevents either the lender or the borrower from unilaterally updating the ledger in a way that only benefits This prevents either the lender or the borrower from unilaterally updating the ledger in a way that only benefits
themselves. themselves.

View File

@ -6,5 +6,4 @@ Two-party flows
tut-two-party-introduction tut-two-party-introduction
tut-two-party-contract tut-two-party-contract
tut-two-party-flow tut-two-party-flow
tut-two-party-running

View File

@ -12,14 +12,14 @@ elements:
* An ``IOUContract``, controlling the evolution of IOUs over time * An ``IOUContract``, controlling the evolution of IOUs over time
* An ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger * An ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger
However, in our original CorDapp, only the IOU's borrower was required to sign transactions issuing IOUs. The lender However, in our original CorDapp, only the IOU's lender was required to sign transactions issuing IOUs. The borrower
had no say in whether the issuance of the IOU was a valid ledger update or not. had no say in whether the issuance of the IOU was a valid ledger update or not.
In this tutorial, we'll update our code so that the borrower requires the lender's agreement before they can issue an In this tutorial, we'll update our code so that the lender requires the borrower's agreement before they can issue an
IOU onto the ledger. We'll need to make two changes: IOU onto the ledger. We'll need to make two changes:
* The ``IOUContract`` will need to be updated so that transactions involving an ``IOUState`` will require the lender's * The ``IOUContract`` will need to be updated so that transactions involving an ``IOUState`` will require the borrower's
signature (as well as the borrower's) to become valid ledger updates signature (as well as the lender's) to become valid ledger updates
* The ``IOUFlow`` will need to be updated to allow for the gathering of the lender's signature * The ``IOUFlow`` will need to be updated to allow for the gathering of the borrower's signature
We'll start by updating the contract. We'll start by updating the contract.

View File

@ -1,28 +0,0 @@
Running our CorDapp
===================
Conclusion
----------
We have written a simple CorDapp that allows IOUs to be issued onto the ledger. Like all CorDapps, our
CorDapp is made up of three key parts:
* The ``IOUState``, representing IOUs on the ledger
* The ``IOUContract``, controlling the evolution of IOUs over time
* The ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger.
Together, these three parts completely determine how IOUs are created and evolved on the ledger.
Next steps
----------
You should now be ready to develop your own CorDapps. There's
`a more fleshed-out version of the IOU CorDapp <https://github.com/corda/cordapp-tutorial>`_
with an API and web front-end, and a set of example CorDapps in
`the main Corda repo <https://github.com/corda/corda>`_, under ``samples``. An explanation of how to run these
samples :doc:`here <running-the-demos>`.
As you write CorDapps, you can learn more about the API available :doc:`here <api>`.
If you get stuck at any point, please reach out on `Slack <https://slack.corda.net/>`_,
`Discourse <https://discourse.corda.net/>`_, or `Stack Overflow <https://stackoverflow.com/questions/tagged/corda>`_.