mirror of
https://github.com/corda/corda.git
synced 2025-04-07 11:27:01 +00:00
Simplifies the Hello, World tutorial.
This commit is contained in:
parent
67ccf69dbb
commit
499f1920c7
@ -18,8 +18,8 @@ It's easy to imagine that most CorDapps will want to impose some constraints on
|
||||
* An asset-trading CorDapp would not want to allow users to finalise a trade without the agreement of their counterparty
|
||||
|
||||
In Corda, we impose constraints on what transactions are allowed using contracts. These contracts are very different
|
||||
to the smart contracts of other distributed ledger platforms. They do not represent the current state of the ledger.
|
||||
Instead, like a real-world contract, they simply impose rules on what kinds of agreements are allowed.
|
||||
to the smart contracts of other distributed ledger platforms. In Corda, contracts do not represent the current state of
|
||||
the ledger. Instead, like a real-world contract, they simply impose rules on what kinds of agreements are allowed.
|
||||
|
||||
Every state is associated with a contract. A transaction is invalid if it does not satisfy the contract of every
|
||||
input and output state in the transaction.
|
||||
@ -42,11 +42,7 @@ Just as every Corda state must implement the ``ContractState`` interface, every
|
||||
val legalContractReference: SecureHash
|
||||
}
|
||||
|
||||
A few more Kotlinisms here:
|
||||
|
||||
* ``fun`` declares a function
|
||||
* The syntax ``fun funName(arg1Name: arg1Type): returnType`` declares that ``funName`` takes an argument of type
|
||||
``arg1Type`` and returns a value of type ``returnType``
|
||||
You can read about function declarations in Kotlin `here <https://kotlinlang.org/docs/reference/functions.html>`_.
|
||||
|
||||
We can see that ``Contract`` expresses its constraints in two ways:
|
||||
|
||||
@ -70,85 +66,121 @@ transfer them or redeem them for cash. One way to enforce this behaviour would b
|
||||
* For the transactions's output IOU state:
|
||||
|
||||
* Its value must be non-negative
|
||||
* Its sender and its recipient cannot be the same entity
|
||||
* All the participants (i.e. both the sender and the recipient) must sign the transaction
|
||||
* The lender and the borrower cannot be the same entity
|
||||
* The IOU's borrower must sign the transaction
|
||||
|
||||
We can picture this transaction as follows:
|
||||
|
||||
.. image:: resources/tutorial-transaction.png
|
||||
:scale: 15%
|
||||
.. image:: resources/simple-tutorial-transaction.png
|
||||
:scale: 15%
|
||||
:align: center
|
||||
|
||||
Let's write a contract that enforces these constraints. We'll do this by modifying either ``TemplateContract.java`` or
|
||||
``TemplateContract.kt`` and updating ``TemplateContract`` to define an ``IOUContract``.
|
||||
|
||||
Defining IOUContract
|
||||
--------------------
|
||||
|
||||
The Create command
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
The first thing our contract needs is a *command*. Commands serve two purposes:
|
||||
|
||||
* They indicate the transaction's intent, allowing us to perform different verification given the situation
|
||||
|
||||
* For example, a transaction proposing the creation of an IOU could have to satisfy different constraints to one
|
||||
redeeming an IOU
|
||||
|
||||
* They allow us to define the required signers for the transaction
|
||||
|
||||
* For example, IOU creation might require signatures from both the sender and the recipient, whereas the transfer
|
||||
of an IOU might only require a signature from the IOUs current holder
|
||||
|
||||
Let's update the definition of ``TemplateContract`` (in ``TemplateContract.java`` or ``TemplateContract.kt``) to
|
||||
define an ``IOUContract`` with a ``Create`` command:
|
||||
Let's write a contract that enforces these constraints. We'll do this by modifying either ``TemplateContract.java`` or
|
||||
``TemplateContract.kt`` and updating ``TemplateContract`` to define an ``IOUContract``:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
package com.template
|
||||
package com.iou
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SecureHash.Companion.sha256
|
||||
|
||||
open class IOUContract : Contract {
|
||||
// Currently, verify() does no checking at all!
|
||||
override fun verify(tx: TransactionForContract) {}
|
||||
|
||||
// Our Create command.
|
||||
class Create : CommandData
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val command = tx.commands.requireSingleCommand<Create>()
|
||||
|
||||
requireThat {
|
||||
// Constraints on the shape of the transaction.
|
||||
"No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty())
|
||||
"There should be one output state of type IOUState." using (tx.outputs.size == 1)
|
||||
|
||||
// IOU-specific constraints.
|
||||
val out = tx.outputs.single() as IOUState
|
||||
"The IOU's value must be non-negative." using (out.value > 0)
|
||||
"The lender and the borrower cannot be the same entity." using (out.lender != out.borrower)
|
||||
|
||||
// Constraints on the signers.
|
||||
"There must only be one signer." using (command.signers.toSet().size == 1)
|
||||
"The signer must be the borrower." using (command.signers.contains(out.borrower.owningKey))
|
||||
}
|
||||
}
|
||||
|
||||
// The legal contract reference - we'll leave this as a dummy hash for now.
|
||||
override val legalContractReference = SecureHash.sha256("Prose contract.")
|
||||
override val legalContractReference = SecureHash.zeroHash
|
||||
}
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
package com.template;
|
||||
package com.iou;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import net.corda.core.contracts.AuthenticatedObject;
|
||||
import net.corda.core.contracts.CommandData;
|
||||
import net.corda.core.contracts.Contract;
|
||||
import net.corda.core.contracts.TransactionForContract;
|
||||
import net.corda.core.crypto.SecureHash;
|
||||
import net.corda.core.identity.Party;
|
||||
|
||||
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||
|
||||
public class IOUContract implements Contract {
|
||||
@Override
|
||||
// Currently, verify() does no checking at all!
|
||||
public void verify(TransactionForContract tx) {}
|
||||
|
||||
// Our Create command.
|
||||
public static class Create implements CommandData {}
|
||||
|
||||
@Override
|
||||
public void verify(TransactionForContract tx) {
|
||||
final AuthenticatedObject<Create> command = requireSingleCommand(tx.getCommands(), Create.class);
|
||||
|
||||
requireThat(check -> {
|
||||
// Constraints on the shape of the transaction.
|
||||
check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty());
|
||||
check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1);
|
||||
|
||||
// IOU-specific constraints.
|
||||
final IOUState out = (IOUState) tx.getOutputs().get(0);
|
||||
final Party lender = out.getLender();
|
||||
final Party borrower = out.getBorrower();
|
||||
check.using("The IOU's value must be non-negative.",out.getValue() > 0);
|
||||
check.using("The lender and the borrower cannot be the same entity.", lender != borrower);
|
||||
|
||||
// Constraints on the signers.
|
||||
check.using("There must only be one signer.", ImmutableSet.of(command.getSigners()).size() == 1);
|
||||
check.using("The signer must be the borrower.", command.getSigners().contains(borrower.getOwningKey()));
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
// The legal contract reference - we'll leave this as a dummy hash for now.
|
||||
private final SecureHash legalContractReference = SecureHash.sha256("Prose contract.");
|
||||
private final SecureHash legalContractReference = SecureHash.Companion.getZeroHash();
|
||||
@Override public final SecureHash getLegalContractReference() { return legalContractReference; }
|
||||
}
|
||||
|
||||
Aside from renaming ``TemplateContract`` to ``IOUContract``, we've also implemented the ``Create`` command. All
|
||||
commands must implement the ``CommandData`` interface.
|
||||
Let's walk through this code step by step.
|
||||
|
||||
The Create command
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
The first thing we add to our contract is a *command*. Commands serve two functions:
|
||||
|
||||
* They indicate the transaction's intent, allowing us to perform different verification given the situation. For
|
||||
example, a transaction proposing the creation of an IOU could have to satisfy different constraints to one redeeming
|
||||
an IOU
|
||||
* 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
|
||||
|
||||
Our contract has one command, a ``Create`` command. All commands must implement the ``CommandData`` interface.
|
||||
|
||||
The ``CommandData`` interface is a simple marker interface for commands. In fact, its declaration is only two words
|
||||
long (in Kotlin, interfaces do not require a body):
|
||||
long (Kotlin interfaces do not require a body):
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
@ -158,8 +190,8 @@ long (in Kotlin, interfaces do not require a body):
|
||||
|
||||
The verify logic
|
||||
^^^^^^^^^^^^^^^^
|
||||
We now need to define the actual contract constraints. For our IOU CorDapp, we won't concern ourselves with writing
|
||||
valid legal prose to enforce the IOU agreement in court. Instead, we'll focus on implementing ``verify``.
|
||||
Our contract also needs to define the actual contract constraints. For our IOU CorDapp, we won't concern ourselves with
|
||||
writing valid legal prose to enforce the IOU agreement in court. Instead, we'll focus on implementing ``verify``.
|
||||
|
||||
Remember that our goal in writing the ``verify`` function is to write a function that, given a transaction:
|
||||
|
||||
@ -183,84 +215,25 @@ following are true:
|
||||
* The transaction has inputs
|
||||
* The transaction doesn't have exactly one output
|
||||
* The IOU itself is invalid
|
||||
* The transaction doesn't require signatures from both the sender and the recipient
|
||||
|
||||
Let's work through these constraints one-by-one.
|
||||
* The transaction doesn't require the borrower's signature
|
||||
|
||||
Command constraints
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
To test for the presence of the ``Create`` command, we can use Corda's ``requireSingleCommand`` function:
|
||||
Our first constraint is around the transaction's commands. We use Corda's ``requireSingleCommand`` function to test for
|
||||
the presence of a single ``Create`` command. Here, ``requireSingleCommand`` performing a dual purpose:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val command = tx.commands.requireSingleCommand<Create>()
|
||||
}
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
// Additional imports.
|
||||
import net.corda.core.contracts.AuthenticatedObject;
|
||||
import net.corda.core.contracts.TransactionForContract;
|
||||
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
||||
|
||||
...
|
||||
|
||||
@Override
|
||||
public void verify(TransactionForContract tx) {
|
||||
final AuthenticatedObject<Create> command = requireSingleCommand(tx.getCommands(), Create.class);
|
||||
}
|
||||
|
||||
Here, ``requireSingleCommand`` performing a dual purpose:
|
||||
|
||||
* It's asserting that there is exactly one ``Create`` command in the transaction
|
||||
* It's extracting the command and returning it
|
||||
* Asserting that there is exactly one ``Create`` command in the transaction
|
||||
* Extracting the command and returning it
|
||||
|
||||
If the ``Create`` command isn't present, or if the transaction has multiple ``Create`` commands, contract
|
||||
verification will fail.
|
||||
|
||||
Transaction constraints
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
We also wanted our transaction to have no inputs and only a single output. One way to impose this constraint is as
|
||||
follows:
|
||||
We also want our transaction to have no inputs and only a single output - an issuance transaction.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val command = tx.commands.requireSingleCommand<Create>()
|
||||
|
||||
requireThat {
|
||||
// Constraints on the shape of the transaction.
|
||||
"No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty())
|
||||
"Only one output state should be created." using (tx.outputs.size == 1)
|
||||
}
|
||||
}
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
// Additional import.
|
||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||
|
||||
...
|
||||
|
||||
@Override
|
||||
public void verify(TransactionForContract tx) {
|
||||
final AuthenticatedObject<Create> command = requireSingleCommand(tx.getCommands(), Create.class);
|
||||
|
||||
requireThat(check -> {
|
||||
// Constraints on the shape of the transaction.
|
||||
check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty());
|
||||
check.using("Only one output state should be created.", tx.getOutputs().size() == 1);
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
Note the use of Corda's built-in ``requireThat`` function. ``requireThat`` provides a terse way to write the following:
|
||||
To impose this and the subsequent constraints, we are using Corda's built-in ``requireThat`` function. ``requireThat``
|
||||
provides a terse way to write the following:
|
||||
|
||||
* If the condition on the right-hand side doesn't evaluate to true...
|
||||
* ...throw an ``IllegalArgumentException`` with the message on the left-hand side
|
||||
@ -272,457 +245,18 @@ IOU constraints
|
||||
We want to impose two constraints on the ``IOUState`` itself:
|
||||
|
||||
* Its value must be non-negative
|
||||
* Its sender and its recipient cannot be the same entity
|
||||
* The lender and the borrower cannot be the same entity
|
||||
|
||||
We can impose these constraints in the same ``requireThat`` block as before:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val command = tx.commands.requireSingleCommand<Create>()
|
||||
|
||||
requireThat {
|
||||
// Constraints on the shape of the transaction.
|
||||
"No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty())
|
||||
"Only one output state should be created." using (tx.outputs.size == 1)
|
||||
|
||||
// IOU-specific constraints.
|
||||
val out = tx.outputs.single() as IOUState
|
||||
"The IOU's value must be non-negative." using (out.value > 0)
|
||||
"The sender and the recipient cannot be the same entity." using (out.sender != out.recipient)
|
||||
}
|
||||
}
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
@Override
|
||||
public void verify(TransactionForContract tx) {
|
||||
final AuthenticatedObject<Create> command = requireSingleCommand(tx.getCommands(), Create.class);
|
||||
|
||||
requireThat(check -> {
|
||||
// Constraints on the shape of the transaction.
|
||||
check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty());
|
||||
check.using("Only one output state should be created.", tx.getOutputs().size() == 1);
|
||||
|
||||
// IOU-specific constraints.
|
||||
final IOUState out = (IOUState) tx.getOutputs().get(0);
|
||||
check.using("The IOU's value must be non-negative.",out.getValue() > 0);
|
||||
check.using("The sender and the recipient cannot be the same entity.", out.getSender() != out.getRecipient());
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
We impose these constraints in the same ``requireThat`` block as before.
|
||||
|
||||
You can see that we're not restricted to only writing constraints in the ``requireThat`` block. We can also write
|
||||
other statements - in this case, we're extracting the transaction's single ``IOUState`` and assigning it to a variable.
|
||||
|
||||
Signer constraints
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
Our final constraint is that the required signers on the transaction are the sender and the recipient only. A
|
||||
transaction's required signers is equal to the union of all the signers listed on the commands. We can therefore
|
||||
extract the signers from the ``Create`` command we retrieved earlier.
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val command = tx.commands.requireSingleCommand<Create>()
|
||||
|
||||
requireThat {
|
||||
// Constraints on the shape of the transaction.
|
||||
"No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty())
|
||||
"Only one output state should be created." using (tx.outputs.size == 1)
|
||||
|
||||
// IOU-specific constraints.
|
||||
val out = tx.outputs.single() as IOUState
|
||||
"The IOU's value must be non-negative." using (out.value > 0)
|
||||
"The sender and the recipient cannot be the same entity." using (out.sender != out.recipient)
|
||||
|
||||
// Constraints on the signers.
|
||||
"All of the participants must be signers." using (command.signers.toSet() == out.participants.map { it.owningKey }.toSet())
|
||||
}
|
||||
}
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
// Additional imports.
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.security.PublicKey;
|
||||
import java.util.List;
|
||||
|
||||
...
|
||||
|
||||
@Override
|
||||
public void verify(TransactionForContract tx) {
|
||||
final AuthenticatedObject<Create> command = requireSingleCommand(tx.getCommands(), Create.class);
|
||||
|
||||
requireThat(check -> {
|
||||
// Constraints on the shape of the transaction.
|
||||
check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty());
|
||||
check.using("Only one output state should be created.", tx.getOutputs().size() == 1);
|
||||
|
||||
// IOU-specific constraints.
|
||||
final IOUState out = (IOUState) tx.getOutputs().get(0);
|
||||
final Party sender = out.getSender();
|
||||
final Party recipient = out.getRecipient();
|
||||
check.using("The IOU's value must be non-negative.",out.getValue() > 0);
|
||||
check.using("The sender and the recipient cannot be the same entity.", out.getSender() != out.getRecipient());
|
||||
|
||||
// Constraints on the signers.
|
||||
final Set<PublicKey> requiredSigners = Sets.newHashSet(sender.getOwningKey(), recipient.getOwningKey());
|
||||
final Set<PublicKey> signerSet = Sets.newHashSet(command.getSigners());
|
||||
check.using("All of the participants must be signers.", (signerSet.equals(requiredSigners)));
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
Checkpoint
|
||||
----------
|
||||
We've now defined the full contract logic of our ``IOUContract``. This contract means that transactions involving
|
||||
``IOUState`` states will have to fulfill strict constraints to become valid ledger updates.
|
||||
|
||||
Before we move on, let's go back and modify ``IOUState`` to point to the new ``IOUContract``:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
class IOUState(val value: Int,
|
||||
val sender: Party,
|
||||
val recipient: Party) : ContractState {
|
||||
override val contract: IOUContract = IOUContract()
|
||||
|
||||
override val participants get() = listOf(sender, recipient)
|
||||
}
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
public class IOUState implements ContractState {
|
||||
private final Integer value;
|
||||
private final Party sender;
|
||||
private final Party recipient;
|
||||
private final IOUContract contract = new IOUContract();
|
||||
|
||||
public IOUState(Integer value, Party sender, Party recipient) {
|
||||
this.value = value;
|
||||
this.sender = sender;
|
||||
this.recipient = recipient;
|
||||
}
|
||||
|
||||
public Integer getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Party getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
public Party getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IOUContract getContract() {
|
||||
return contract;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AbstractParty> getParticipants() {
|
||||
return ImmutableList.of(sender, recipient);
|
||||
}
|
||||
}
|
||||
|
||||
Transaction tests
|
||||
-----------------
|
||||
How can we ensure that we've defined our contract constraints correctly?
|
||||
|
||||
One option would be to deploy the CorDapp onto a set of nodes, and test it manually. However, this is a relatively
|
||||
slow process, and would take on the order of minutes to test each change.
|
||||
|
||||
Instead, we can test our contract logic using Corda's ``ledgerDSL`` transaction-testing framework. This will allow us
|
||||
to test our contract without the overhead of spinning up a set of nodes.
|
||||
|
||||
Open either ``test/kotlin/com/template/contract/ContractTests.kt`` or
|
||||
``test/java/com/template/contract/ContractTests.java``, and add the following as our first test:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
package com.template
|
||||
|
||||
import net.corda.testing.*
|
||||
import org.junit.Test
|
||||
|
||||
class IOUTransactionTests {
|
||||
@Test
|
||||
fun `transaction must include Create command`() {
|
||||
ledger {
|
||||
transaction {
|
||||
output { IOUState(1, MINI_CORP, MEGA_CORP) }
|
||||
fails()
|
||||
command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() }
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
package com.template;
|
||||
|
||||
import net.corda.core.identity.Party;
|
||||
import org.junit.Test;
|
||||
import java.security.PublicKey;
|
||||
import static net.corda.testing.CoreTestUtils.*;
|
||||
|
||||
public class IOUTransactionTests {
|
||||
static private final Party miniCorp = getMINI_CORP();
|
||||
static private final Party megaCorp = getMEGA_CORP();
|
||||
static private final PublicKey[] keys = new PublicKey[2];
|
||||
|
||||
{
|
||||
keys[0] = getMEGA_CORP_PUBKEY();
|
||||
keys[1] = getMINI_CORP_PUBKEY();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transactionMustIncludeCreateCommand() {
|
||||
ledger(ledgerDSL -> {
|
||||
ledgerDSL.transaction(txDSL -> {
|
||||
txDSL.output(new IOUState(1, miniCorp, megaCorp));
|
||||
txDSL.fails();
|
||||
txDSL.command(keys, IOUContract.Create::new);
|
||||
txDSL.verifies();
|
||||
return null;
|
||||
});
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
This test uses Corda's built-in ``ledgerDSL`` to:
|
||||
|
||||
* Create a fake transaction
|
||||
* Add inputs, outputs, commands, etc. (using the DSL's ``output``, ``input`` and ``command`` methods)
|
||||
* At any point, asserting that the transaction built so far is either contractually valid (by calling ``verifies``) or
|
||||
contractually invalid (by calling ``fails``)
|
||||
|
||||
In this instance:
|
||||
|
||||
* We initially create a transaction with an output but no command
|
||||
* We assert that this transaction is invalid (since the ``Create`` command is missing)
|
||||
* We then add the ``Create`` command
|
||||
* We assert that transaction is now valid
|
||||
|
||||
Here is the full set of tests we'll be using to test the ``IOUContract``:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
class IOUTransactionTests {
|
||||
@Test
|
||||
fun `transaction must include Create command`() {
|
||||
ledger {
|
||||
transaction {
|
||||
output { IOUState(1, MINI_CORP, MEGA_CORP) }
|
||||
fails()
|
||||
command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() }
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `transaction must have no inputs`() {
|
||||
ledger {
|
||||
transaction {
|
||||
input { IOUState(1, MINI_CORP, MEGA_CORP) }
|
||||
output { IOUState(1, MINI_CORP, MEGA_CORP) }
|
||||
command(MEGA_CORP_PUBKEY) { IOUContract.Create() }
|
||||
`fails with`("No inputs should be consumed when issuing an IOU.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `transaction must have one output`() {
|
||||
ledger {
|
||||
transaction {
|
||||
output { IOUState(1, MINI_CORP, MEGA_CORP) }
|
||||
output { IOUState(1, MINI_CORP, MEGA_CORP) }
|
||||
command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() }
|
||||
`fails with`("Only one output state should be created.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sender must sign transaction`() {
|
||||
ledger {
|
||||
transaction {
|
||||
output { IOUState(1, MINI_CORP, MEGA_CORP) }
|
||||
command(MINI_CORP_PUBKEY) { IOUContract.Create() }
|
||||
`fails with`("All of the participants must be signers.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `recipient must sign transaction`() {
|
||||
ledger {
|
||||
transaction {
|
||||
output { IOUState(1, MINI_CORP, MEGA_CORP) }
|
||||
command(MEGA_CORP_PUBKEY) { IOUContract.Create() }
|
||||
`fails with`("All of the participants must be signers.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sender is not recipient`() {
|
||||
ledger {
|
||||
transaction {
|
||||
output { IOUState(1, MEGA_CORP, MEGA_CORP) }
|
||||
command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() }
|
||||
`fails with`("The sender and the recipient cannot be the same entity.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cannot create negative-value IOUs`() {
|
||||
ledger {
|
||||
transaction {
|
||||
output { IOUState(-1, MINI_CORP, MEGA_CORP) }
|
||||
command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() }
|
||||
`fails with`("The IOU's value must be non-negative.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
public class IOUTransactionTests {
|
||||
static private final Party miniCorp = getMINI_CORP();
|
||||
static private final Party megaCorp = getMEGA_CORP();
|
||||
static private final PublicKey[] keys = new PublicKey[2];
|
||||
|
||||
{
|
||||
keys[0] = getMEGA_CORP_PUBKEY();
|
||||
keys[1] = getMINI_CORP_PUBKEY();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transactionMustIncludeCreateCommand() {
|
||||
ledger(ledgerDSL -> {
|
||||
ledgerDSL.transaction(txDSL -> {
|
||||
txDSL.output(new IOUState(1, miniCorp, megaCorp));
|
||||
txDSL.fails();
|
||||
txDSL.command(keys, IOUContract.Create::new);
|
||||
txDSL.verifies();
|
||||
return null;
|
||||
});
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transactionMustHaveNoInputs() {
|
||||
ledger(ledgerDSL -> {
|
||||
ledgerDSL.transaction(txDSL -> {
|
||||
txDSL.input(new IOUState(1, miniCorp, megaCorp));
|
||||
txDSL.output(new IOUState(1, miniCorp, megaCorp));
|
||||
txDSL.command(keys, IOUContract.Create::new);
|
||||
txDSL.failsWith("No inputs should be consumed when issuing an IOU.");
|
||||
return null;
|
||||
});
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transactionMustHaveOneOutput() {
|
||||
ledger(ledgerDSL -> {
|
||||
ledgerDSL.transaction(txDSL -> {
|
||||
txDSL.output(new IOUState(1, miniCorp, megaCorp));
|
||||
txDSL.output(new IOUState(1, miniCorp, megaCorp));
|
||||
txDSL.command(keys, IOUContract.Create::new);
|
||||
txDSL.failsWith("Only one output state should be created.");
|
||||
return null;
|
||||
});
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void senderMustSignTransaction() {
|
||||
ledger(ledgerDSL -> {
|
||||
ledgerDSL.transaction(txDSL -> {
|
||||
txDSL.output(new IOUState(1, miniCorp, megaCorp));
|
||||
PublicKey[] keys = new PublicKey[1];
|
||||
keys[0] = getMINI_CORP_PUBKEY();
|
||||
txDSL.command(keys, IOUContract.Create::new);
|
||||
txDSL.failsWith("All of the participants must be signers.");
|
||||
return null;
|
||||
});
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recipientMustSignTransaction() {
|
||||
ledger(ledgerDSL -> {
|
||||
ledgerDSL.transaction(txDSL -> {
|
||||
txDSL.output(new IOUState(1, miniCorp, megaCorp));
|
||||
PublicKey[] keys = new PublicKey[1];
|
||||
keys[0] = getMEGA_CORP_PUBKEY();
|
||||
txDSL.command(keys, IOUContract.Create::new);
|
||||
txDSL.failsWith("All of the participants must be signers.");
|
||||
return null;
|
||||
});
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void senderIsNotRecipient() {
|
||||
ledger(ledgerDSL -> {
|
||||
ledgerDSL.transaction(txDSL -> {
|
||||
txDSL.output(new IOUState(1, megaCorp, megaCorp));
|
||||
PublicKey[] keys = new PublicKey[1];
|
||||
keys[0] = getMEGA_CORP_PUBKEY();
|
||||
txDSL.command(keys, IOUContract.Create::new);
|
||||
txDSL.failsWith("The sender and the recipient cannot be the same entity.");
|
||||
return null;
|
||||
});
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cannotCreateNegativeValueIOUs() {
|
||||
ledger(ledgerDSL -> {
|
||||
ledgerDSL.transaction(txDSL -> {
|
||||
txDSL.output(new IOUState(-1, miniCorp, megaCorp));
|
||||
txDSL.command(keys, IOUContract.Create::new);
|
||||
txDSL.failsWith("The IOU's value must be non-negative.");
|
||||
return null;
|
||||
});
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Copy these tests into the ContractTests file, and run them to ensure that the ``IOUState`` and ``IOUContract`` are
|
||||
defined correctly. All the tests should pass.
|
||||
Finally, we require the borrower's signature on the transaction. A transaction's required signers is equal to the union
|
||||
of all the signers listed on the commands. We therefore extract the signers from the ``Create`` command we
|
||||
retrieved earlier.
|
||||
|
||||
Progress so far
|
||||
---------------
|
||||
@ -731,8 +265,10 @@ We've now written an ``IOUContract`` constraining the evolution of each ``IOUSta
|
||||
* An ``IOUState`` can only be created, not transferred or redeemed
|
||||
* Creating an ``IOUState`` requires an issuance transaction with no inputs, a single ``IOUState`` output, and a
|
||||
``Create`` command
|
||||
* The ``IOUState`` created by the issuance transaction must have a non-negative value, and its sender and recipient
|
||||
* The ``IOUState`` created by the issuance transaction must have a non-negative value, and the lender and borrower
|
||||
must be different entities.
|
||||
|
||||
The final step in the creation of our CorDapp will be to write the ``IOUFlow`` that will allow nodes to orchestrate
|
||||
the creation of a new ``IOUState`` on the ledger, while only sharing information on a need-to-know basis.
|
||||
Before we move on, make sure you go back and modify ``IOUState`` to point to the new ``IOUContract`` class.
|
||||
|
||||
The final step in the creation of our CorDapp will be to write the ``IOUFlow`` that will allow a node to orchestrate
|
||||
the creation of a new ``IOUState`` on the ledger, while only sharing information on a need-to-know basis.
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -44,10 +44,10 @@ However, we can easily extend our CorDapp to handle additional use-cases later o
|
||||
|
||||
Flow
|
||||
^^^^
|
||||
Our flow will be the IOUFlow. It will allow two nodes to orchestrate the creation of a new IOU on the ledger, via the
|
||||
Our flow will be the IOUFlow. It will allow a node to orchestrate the creation of a new IOU on the ledger, via the
|
||||
following steps:
|
||||
|
||||
.. image:: resources/tutorial-flow.png
|
||||
.. image:: resources/simple-tutorial-flow.png
|
||||
:scale: 25%
|
||||
:align: center
|
||||
|
||||
|
@ -86,16 +86,12 @@ If we navigate to one of these folders, we'll see four node folder. Each node fo
|
||||
.. code:: python
|
||||
|
||||
.
|
||||
// The runnable node
|
||||
|____corda.jar
|
||||
// The node's webserver
|
||||
|____corda-webserver.jar
|
||||
|____corda.jar // The runnable node
|
||||
|____corda-webserver.jar // The node's webserver
|
||||
|____dependencies
|
||||
// The node's configuration file
|
||||
|____node.conf
|
||||
|____node.conf // The node's configuration file
|
||||
|____plugins
|
||||
// Our IOU CorDapp
|
||||
|____java/kotlin-source-0.1.jar
|
||||
|____java/kotlin-source-0.1.jar // Our IOU CorDapp
|
||||
|
||||
Let's start the nodes by running the following commands from the root of the project:
|
||||
|
||||
@ -132,9 +128,15 @@ commands.
|
||||
|
||||
We want to create an IOU of 100 with Node B. We start the ``IOUFlow`` by typing:
|
||||
|
||||
.. code:: python
|
||||
.. container:: codeset
|
||||
|
||||
start IOUFlow arg0: 99, arg1: "CN=NodeB,O=NodeB,L=New York,C=US"
|
||||
.. code-block:: java
|
||||
|
||||
start IOUFlow arg0: 99, arg1: "NodeB"
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
start IOUFlow iouValue: 99, otherParty: "NodeB"
|
||||
|
||||
Node A and Node B will automatically agree an IOU.
|
||||
|
||||
@ -162,8 +164,8 @@ The vaults of Node A and Node B should both display the following output:
|
||||
- state:
|
||||
data:
|
||||
value: 99
|
||||
sender: "CN=NodeA,O=NodeA,L=London,C=GB"
|
||||
recipient: "CN=NodeB,O=NodeB,L=New York,C=US"
|
||||
lender: "CN=NodeA,O=NodeA,L=London,C=GB"
|
||||
borrower: "CN=NodeB,O=NodeB,L=New York,C=US"
|
||||
contract:
|
||||
legalContractReference: "559322B95BCF7913E3113962DC3F3CBD71C818C66977721580C045DC41C813A5"
|
||||
participants:
|
||||
@ -190,19 +192,26 @@ 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.
|
||||
* 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>`.
|
||||
There are a number of improvements we could make to this CorDapp:
|
||||
|
||||
* We could require signatures from the lender as well the borrower, to give both parties a say in the creation of a new
|
||||
``IOUState``
|
||||
* We should add unit tests, using the contract-test and flow-test frameworks
|
||||
* We should change ``IOUState.value`` from an integer to a proper amount of a given currency
|
||||
* We could add an API, to make it easier to interact with the CorDapp
|
||||
|
||||
We will explore some of these improvements in future tutorials. But 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>`_.
|
||||
`Discourse <https://discourse.corda.net/>`_, or `Stack Overflow <https://stackoverflow.com/questions/tagged/corda>`_.
|
||||
|
@ -38,11 +38,6 @@ If you do want to dive into Kotlin, there's an official
|
||||
`getting started guide <https://kotlinlang.org/docs/tutorials/>`_, and a series of
|
||||
`Kotlin Koans <https://kotlinlang.org/docs/tutorials/koans.html>`_.
|
||||
|
||||
If not, here's a quick primer on the Kotlinisms in the declaration of ``ContractState``:
|
||||
|
||||
* ``val`` declares a read-only property, similar to Java's ``final`` keyword
|
||||
* The syntax ``varName: varType`` declares ``varName`` as being of type ``varType``
|
||||
|
||||
We can see that the ``ContractState`` interface declares two properties:
|
||||
|
||||
* ``contract``: the contract controlling transactions involving this state
|
||||
@ -53,15 +48,15 @@ Beyond this, our state is free to define any properties, methods, helpers or inn
|
||||
represent a given class of shared facts on the ledger.
|
||||
|
||||
``ContractState`` also has several child interfaces that you may wish to implement depending on your state, such as
|
||||
``LinearState`` and ``OwnableState``.
|
||||
``LinearState`` and ``OwnableState``. See :doc:`api-states` for more information.
|
||||
|
||||
Modelling IOUs
|
||||
--------------
|
||||
How should we define the ``IOUState`` representing IOUs on the ledger? Beyond implementing the ``ContractState``
|
||||
interface, our ``IOUState`` will also need properties to track the relevant features of the IOU:
|
||||
|
||||
* The sender of the IOU
|
||||
* The IOU's recipient
|
||||
* The lender of the IOU
|
||||
* The borrower of the IOU
|
||||
* The value of the IOU
|
||||
|
||||
There are many more fields you could include, such as the IOU's currency. We'll abstract them away for now. If
|
||||
@ -76,23 +71,22 @@ define an ``IOUState``:
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
package com.template
|
||||
package com.iou
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.identity.Party
|
||||
|
||||
class IOUState(val value: Int,
|
||||
val sender: Party,
|
||||
val recipient: Party,
|
||||
// TODO: Once we've defined IOUContract, come back and update this.
|
||||
override val contract: TemplateContract = TemplateContract()) : ContractState {
|
||||
val lender: Party,
|
||||
val borrower: Party) : ContractState {
|
||||
override val contract: IOUContract = IOUContract()
|
||||
|
||||
override val participants get() = listOf(sender, recipient)
|
||||
override val participants get() = listOf(lender, borrower)
|
||||
}
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
package com.template;
|
||||
package com.iou;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.corda.core.contracts.ContractState;
|
||||
@ -103,53 +97,52 @@ define an ``IOUState``:
|
||||
|
||||
public class IOUState implements ContractState {
|
||||
private final int value;
|
||||
private final Party sender;
|
||||
private final Party recipient;
|
||||
// TODO: Once we've defined IOUContract, come back and update this.
|
||||
private final TemplateContract contract;
|
||||
private final Party lender;
|
||||
private final Party borrower;
|
||||
private final IOUContract contract = new IOUContract();
|
||||
|
||||
public IOUState(int value, Party sender, Party recipient, IOUContract contract) {
|
||||
public IOUState(int value, Party lender, Party borrower) {
|
||||
this.value = value;
|
||||
this.sender = sender;
|
||||
this.recipient = recipient;
|
||||
this.contract = contract;
|
||||
this.lender = lender;
|
||||
this.borrower = borrower;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Party getSender() {
|
||||
return sender;
|
||||
public Party getLender() {
|
||||
return lender;
|
||||
}
|
||||
|
||||
public Party getRecipient() {
|
||||
return recipient;
|
||||
public Party getBorrower() {
|
||||
return borrower;
|
||||
}
|
||||
|
||||
@Override
|
||||
// TODO: Once we've defined IOUContract, come back and update this.
|
||||
public TemplateContract getContract() {
|
||||
public IOUContract getContract() {
|
||||
return contract;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AbstractParty> getParticipants() {
|
||||
return ImmutableList.of(sender, recipient);
|
||||
return ImmutableList.of(lender, borrower);
|
||||
}
|
||||
}
|
||||
|
||||
We've made the following changes:
|
||||
|
||||
* We've renamed ``TemplateState`` to ``IOUState``
|
||||
* We've added properties for ``value``, ``sender`` and ``recipient`` (along with any getters and setters in Java):
|
||||
* We've added properties for ``value``, ``lender`` and ``borrower`` (along with any getters and setters in Java):
|
||||
|
||||
* ``value`` is just a standard int (in Java)/Int (in Kotlin), but ``sender`` and ``recipient`` are of type
|
||||
``Party``. ``Party`` is a built-in Corda type that represents an entity on the network.
|
||||
* ``value`` is just a standard int (in Java)/Int (in Kotlin)
|
||||
* ``lender`` and ``borrower`` are of type ``Party``. ``Party`` is a built-in Corda type that represents an entity on
|
||||
the network.
|
||||
|
||||
* We've overridden ``participants`` to return a list of the ``sender`` and ``recipient``
|
||||
* This means that actions such as changing the state's contract or its notary will require approval from both the
|
||||
``sender`` and the ``recipient``
|
||||
* We've overridden ``participants`` to return a list of the ``lender`` and ``borrower``
|
||||
|
||||
* Actions such as changing a state's contract or notary will require approval from all the ``participants``
|
||||
|
||||
We've left ``IOUState``'s contract as ``TemplateContract`` for now. We'll update this once we've defined the
|
||||
``IOUContract``.
|
||||
|
@ -38,7 +38,7 @@ We can write our CorDapp in either Java or Kotlin, and will be providing the cod
|
||||
you want to write the CorDapp in Java, you'll be modifying the files under ``java-source``. If you prefer to use
|
||||
Kotlin, you'll be modifying the files under ``kotlin-source``.
|
||||
|
||||
To implement our IOU CorDapp, we'll only need to modify five files:
|
||||
To implement our IOU CorDapp, we'll only need to modify three files:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
@ -53,13 +53,6 @@ To implement our IOU CorDapp, we'll only need to modify five files:
|
||||
// 3. The flow
|
||||
java-source/src/main/java/com/template/flow/TemplateFlow.java
|
||||
|
||||
// Tests for our contract and flow:
|
||||
// 1. The contract tests
|
||||
java-source/src/test/java/com/template/contract/ContractTests.java
|
||||
|
||||
// 2. The flow tests
|
||||
java-source/src/test/java/com/template/flow/FlowTests.java
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
// 1. The state
|
||||
@ -71,13 +64,6 @@ To implement our IOU CorDapp, we'll only need to modify five files:
|
||||
// 3. The flow
|
||||
kotlin-source/src/main/kotlin/com/template/flow/TemplateFlow.kt
|
||||
|
||||
// Tests for our contract and flow:
|
||||
// 1. The contract tests
|
||||
kotlin-source/src/test/kotlin/com/template/contract/ContractTests.kt
|
||||
|
||||
// 2. The flow tests
|
||||
kotlin-source/src/test/kotlin/com/template/flow/FlowTests.kt
|
||||
|
||||
Progress so far
|
||||
---------------
|
||||
We now have a template that we can build upon to define our IOU CorDapp.
|
||||
|
BIN
docs/source/resources/simple-tutorial-flow.png
Normal file
BIN
docs/source/resources/simple-tutorial-flow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 150 KiB |
BIN
docs/source/resources/simple-tutorial-transaction.png
Normal file
BIN
docs/source/resources/simple-tutorial-transaction.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 173 KiB |
Binary file not shown.
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 192 KiB |
Loading…
x
Reference in New Issue
Block a user