mirror of
https://github.com/corda/corda.git
synced 2025-02-04 10:11:14 +00:00
Hello, World! CorDapp tutorial
This commit is contained in:
parent
bfd02f5d78
commit
73fbcea0e3
738
docs/source/hello-world-contract.rst
Normal file
738
docs/source/hello-world-contract.rst
Normal file
@ -0,0 +1,738 @@
|
|||||||
|
.. highlight:: kotlin
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||||
|
|
||||||
|
Writing the contract
|
||||||
|
====================
|
||||||
|
|
||||||
|
In Corda, the ledger is updated via transactions. Each transaction is a proposal to mark zero or more existing
|
||||||
|
states as historic (the inputs), while creating zero or more new states (the outputs).
|
||||||
|
|
||||||
|
It's easy to imagine that most CorDapps will want to impose some constraints on how their states evolve over time:
|
||||||
|
|
||||||
|
* A cash CorDapp would not want to allow users to create transactions that generate money out of thin air (at least
|
||||||
|
without the involvement of a central bank or commercial bank)
|
||||||
|
* A loan CorDapp might not want to allow the creation of negative-valued loans
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The Contract interface
|
||||||
|
----------------------
|
||||||
|
Just as every Corda state must implement the ``ContractState`` interface, every contract must implement the
|
||||||
|
``Contract`` interface:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. code-block:: kotlin
|
||||||
|
|
||||||
|
interface Contract {
|
||||||
|
// Implements the contract constraints in code.
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
fun verify(tx: TransactionForContract)
|
||||||
|
|
||||||
|
// Expresses the contract constraints as legal prose.
|
||||||
|
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``
|
||||||
|
|
||||||
|
We can see that ``Contract`` expresses its constraints in two ways:
|
||||||
|
|
||||||
|
* In legal prose, through a hash referencing a legal contract that expresses the contract's constraints in legal prose
|
||||||
|
* In code, through a ``verify`` function that takes a transaction as input, and:
|
||||||
|
|
||||||
|
* Throws an ``IllegalArgumentException`` if it rejects the transaction proposal
|
||||||
|
* Returns silently if it accepts the transaction proposal
|
||||||
|
|
||||||
|
Controlling IOU evolution
|
||||||
|
-------------------------
|
||||||
|
What would a good contract for an ``IOUState`` look like? There is no right or wrong answer - it depends on how you
|
||||||
|
want your CorDapp to behave.
|
||||||
|
|
||||||
|
For our CorDapp, let's impose the constraint that we only want to allow the creation of IOUs. We don't want nodes to
|
||||||
|
transfer them or redeem them for cash. One way to enforce this behaviour would be by imposing the following constraints:
|
||||||
|
|
||||||
|
* A transaction involving IOUs must consume zero inputs, and create one output of type ``IOUState``
|
||||||
|
* The transaction should also include a ``Create`` command, indicating the transaction's intent (more on commands
|
||||||
|
shortly)
|
||||||
|
* 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
|
||||||
|
|
||||||
|
We can picture this transaction as follows:
|
||||||
|
|
||||||
|
.. image:: resources/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:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. code-block:: kotlin
|
||||||
|
|
||||||
|
package com.template
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// The legal contract reference - we'll leave this as a dummy hash for now.
|
||||||
|
override val legalContractReference = SecureHash.sha256("Prose contract.")
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: java
|
||||||
|
|
||||||
|
package com.template;
|
||||||
|
|
||||||
|
import net.corda.core.contracts.CommandData;
|
||||||
|
import net.corda.core.contracts.Contract;
|
||||||
|
import net.corda.core.crypto.SecureHash;
|
||||||
|
|
||||||
|
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 {}
|
||||||
|
|
||||||
|
// The legal contract reference - we'll leave this as a dummy hash for now.
|
||||||
|
private final SecureHash legalContractReference = SecureHash.sha256("Prose contract.");
|
||||||
|
@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.
|
||||||
|
|
||||||
|
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):
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. code-block:: kotlin
|
||||||
|
|
||||||
|
interface CommandData
|
||||||
|
|
||||||
|
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``.
|
||||||
|
|
||||||
|
Remember that our goal in writing the ``verify`` function is to write a function that, given a transaction:
|
||||||
|
|
||||||
|
* Throws an ``IllegalArgumentException`` if the transaction is considered invalid
|
||||||
|
* Does **not** throw an exception if the transaction is considered valid
|
||||||
|
|
||||||
|
In deciding whether the transaction is valid, the ``verify`` function only has access to the contents of the
|
||||||
|
transaction:
|
||||||
|
|
||||||
|
* ``tx.inputs``, which lists the inputs
|
||||||
|
* ``tx.outputs``, which lists the outputs
|
||||||
|
* ``tx.commands``, which lists the commands and their associated signers
|
||||||
|
|
||||||
|
Although we won't use them here, the ``verify`` function also has access to the transaction's attachments,
|
||||||
|
time-windows, notary and hash.
|
||||||
|
|
||||||
|
Based on the constraints enumerated above, we'll write a ``verify`` function that rejects a transaction if any of the
|
||||||
|
following are true:
|
||||||
|
|
||||||
|
* The transaction doesn't include a ``Create`` command
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
Command constraints
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
To test for the presence of the ``Create`` command, we can use Corda's ``requireSingleCommand`` function:
|
||||||
|
|
||||||
|
.. 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
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
.. 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:
|
||||||
|
|
||||||
|
* If the condition on the right-hand side doesn't evaluate to true...
|
||||||
|
* ...throw an ``IllegalArgumentException`` with the message on the left-hand side
|
||||||
|
|
||||||
|
As before, the act of throwing this exception would cause transaction verification to fail.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Progress so far
|
||||||
|
---------------
|
||||||
|
We've now written an ``IOUContract`` constraining the evolution of each ``IOUState`` over time:
|
||||||
|
|
||||||
|
* 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
|
||||||
|
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.
|
1020
docs/source/hello-world-flow.rst
Normal file
1020
docs/source/hello-world-flow.rst
Normal file
File diff suppressed because it is too large
Load Diff
12
docs/source/hello-world-index.rst
Normal file
12
docs/source/hello-world-index.rst
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Hello, World!
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
hello-world-introduction
|
||||||
|
hello-world-template
|
||||||
|
hello-world-state
|
||||||
|
hello-world-contract
|
||||||
|
hello-world-flow
|
||||||
|
hello-world-running
|
63
docs/source/hello-world-introduction.rst
Normal file
63
docs/source/hello-world-introduction.rst
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
Introduction
|
||||||
|
============
|
||||||
|
|
||||||
|
By this point, :doc:`your dev environment should be set up <getting-set-up>`, you've run
|
||||||
|
:doc:`your first CorDapp <tutorial-cordapp>`, and you're familiar with Corda's :doc:`key concepts <key-concepts>`. What
|
||||||
|
comes next?
|
||||||
|
|
||||||
|
If you're a developer, the next step is to write your own CorDapp. Each CorDapp takes the form of a plugin that is
|
||||||
|
installed on one or more Corda nodes, and gives them the ability to conduct some new process - anything from
|
||||||
|
issuing a debt instrument to making a restaurant booking.
|
||||||
|
|
||||||
|
Our use-case
|
||||||
|
------------
|
||||||
|
Our CorDapp will seek to model IOUs on ledger. An IOU – short for “I Owe yoU” – records the fact that one person owes
|
||||||
|
another a given amount of money. We can imagine that this is potentially sensitive information that we'd only want to
|
||||||
|
communicate on a need-to-know basis. This is one of the areas where Corda excels - allowing a small set of parties to
|
||||||
|
agree on a fact without needing to share this fact with everyone else on the network, as you do with most other
|
||||||
|
blockchain platforms.
|
||||||
|
|
||||||
|
To serve any useful function, a CorDapp needs three core elements:
|
||||||
|
|
||||||
|
* **One or more states** – the shared facts that will be agreed upon and stored on the ledger
|
||||||
|
* **One or more contracts** – the rules governing how these states can evolve over time
|
||||||
|
* **One or more flows** – the step-by-step process for carrying out a ledger update
|
||||||
|
|
||||||
|
Our IOU CorDapp is no exception. It will have the following elements:
|
||||||
|
|
||||||
|
State
|
||||||
|
^^^^^
|
||||||
|
The states will be IOUStates, with each instance representing a single IOU. We can visualize an IOUState as follows:
|
||||||
|
|
||||||
|
.. image:: resources/tutorial-state.png
|
||||||
|
:scale: 25%
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
Contract
|
||||||
|
^^^^^^^^
|
||||||
|
Our contract will be the IOUContract, imposing rules on the evolution of IOUs over time:
|
||||||
|
|
||||||
|
* Only the creation of new IOUs will be allowed
|
||||||
|
* Transferring existing IOUs or paying off an IOU with cash will not be allowed
|
||||||
|
|
||||||
|
However, we can easily extend our CorDapp to handle additional use-cases later on.
|
||||||
|
|
||||||
|
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
|
||||||
|
following steps:
|
||||||
|
|
||||||
|
.. image:: resources/tutorial-flow.png
|
||||||
|
:scale: 25%
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
In traditional distributed ledger systems, where all data is broadcast to every network participant, you don’t even
|
||||||
|
think about this step – you simply package up your ledger update and send it out into the world. But in Corda, where
|
||||||
|
privacy is a core focus, flows are used to carefully control who sees what during the process of agreeing a
|
||||||
|
ledger update.
|
||||||
|
|
||||||
|
Progress so far
|
||||||
|
---------------
|
||||||
|
We've sketched out a simple CorDapp that will allow nodes to confidentially agree the creation of new IOUs.
|
||||||
|
|
||||||
|
Next, we'll be taking a look at the template project we'll be using as a base for our work.
|
208
docs/source/hello-world-running.rst
Normal file
208
docs/source/hello-world-running.rst
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
.. highlight:: kotlin
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||||
|
|
||||||
|
Running our CorDapp
|
||||||
|
===================
|
||||||
|
|
||||||
|
Now that we've written a CorDapp, it's time to test it by running it on some real Corda nodes.
|
||||||
|
|
||||||
|
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``
|
||||||
|
or ``kotlin-source/build.gradle`` and scroll down to the ``task deployNodes`` section. This section defines four
|
||||||
|
nodes - the Controller, and NodeA, NodeB and NodeC:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. code-block:: kotlin
|
||||||
|
|
||||||
|
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||||
|
directory "./build/nodes"
|
||||||
|
networkMap "CN=Controller,O=R3,OU=corda,L=London,C=UK"
|
||||||
|
node {
|
||||||
|
name "CN=Controller,O=R3,OU=corda,L=London,C=UK"
|
||||||
|
advertisedServices = ["corda.notary.validating"]
|
||||||
|
p2pPort 10002
|
||||||
|
rpcPort 10003
|
||||||
|
webPort 10004
|
||||||
|
cordapps = []
|
||||||
|
}
|
||||||
|
node {
|
||||||
|
name "CN=NodeA,O=NodeA,L=London,C=UK"
|
||||||
|
advertisedServices = []
|
||||||
|
p2pPort 10005
|
||||||
|
rpcPort 10006
|
||||||
|
webPort 10007
|
||||||
|
cordapps = []
|
||||||
|
rpcUsers = [[ user: "user1", "password": "test", "permissions": []]]
|
||||||
|
}
|
||||||
|
node {
|
||||||
|
name "CN=NodeB,O=NodeB,L=New York,C=US"
|
||||||
|
advertisedServices = []
|
||||||
|
p2pPort 10008
|
||||||
|
rpcPort 10009
|
||||||
|
webPort 10010
|
||||||
|
cordapps = []
|
||||||
|
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
|
||||||
|
advertising a validating notary service. Feel free to add additional node definitions here to expand the size of the
|
||||||
|
test network.
|
||||||
|
|
||||||
|
We can run this ``deployNodes`` task using Gradle. For each node definition, Gradle will:
|
||||||
|
|
||||||
|
* Package the project's source files into a CorDapp jar
|
||||||
|
* Create a new node in ``build/nodes`` with our CorDapp already installed
|
||||||
|
|
||||||
|
We can do that now by running the following commands from the root of the project:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
// On Windows
|
||||||
|
gradlew clean deployNodes
|
||||||
|
|
||||||
|
// On Mac
|
||||||
|
./gradlew clean deployNodes
|
||||||
|
|
||||||
|
Running the nodes
|
||||||
|
-----------------
|
||||||
|
Running ``deployNodes`` will build the nodes under both ``java-source/build/nodes`` and ``kotlin-source/build/nodes``.
|
||||||
|
If we navigate to one of these folders, we'll see four node folder. Each node folder has the following structure:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
.
|
||||||
|
// The runnable node
|
||||||
|
|____corda.jar
|
||||||
|
// The node's webserver
|
||||||
|
|____corda-webserver.jar
|
||||||
|
|____dependencies
|
||||||
|
// The node's configuration file
|
||||||
|
|____node.conf
|
||||||
|
|____plugins
|
||||||
|
// Our IOU CorDapp
|
||||||
|
|____java/kotlin-source-0.1.jar
|
||||||
|
|
||||||
|
Let's start the nodes by running the following commands from the root of the project:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
// On Windows for a Java CorDapp
|
||||||
|
java-source/build/nodes/runnodes.bat
|
||||||
|
|
||||||
|
// On Windows for a Kotlin CorDapp
|
||||||
|
kotlin-source/build/nodes/runnodes.bat
|
||||||
|
|
||||||
|
// 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
|
||||||
|
terminal windows in all. Give each node a moment to start - you'll know it's ready when its terminal windows displays
|
||||||
|
the message, "Welcome to the Corda interactive shell.".
|
||||||
|
|
||||||
|
.. image:: resources/running_node.png
|
||||||
|
:scale: 25%
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
Interacting with the nodes
|
||||||
|
--------------------------
|
||||||
|
Now that our nodes are running, let's order one of them to create an IOU by kicking off our ``IOUFlow``. In a larger
|
||||||
|
app, we'd generally provide a web API sitting on top of our node. Here, for simplicity, we'll be interacting with the
|
||||||
|
node via its built-in CRaSH shell.
|
||||||
|
|
||||||
|
Go to the terminal window displaying the CRaSH shell of Node A. Typing ``help`` will display a list of the available
|
||||||
|
commands.
|
||||||
|
|
||||||
|
We want to create an IOU of 100 with Node B. We start the ``IOUFlow`` by typing:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
start IOUFlow arg0: 99, arg1: "CN=NodeB,O=NodeB,L=New York,C=US"
|
||||||
|
|
||||||
|
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. 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``
|
||||||
|
will display a list of the available commands. We can examine the contents of a node's vault by running:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
run vaultAndUpdates
|
||||||
|
|
||||||
|
And we can also examine a node's transaction storage, by running:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
run verifiedTransactions
|
||||||
|
|
||||||
|
The vaults of Node A and Node B should both display the following output:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
first:
|
||||||
|
- state:
|
||||||
|
data:
|
||||||
|
value: 99
|
||||||
|
sender: "CN=NodeA,O=NodeA,L=London,C=UK"
|
||||||
|
recipient: "CN=NodeB,O=NodeB,L=New York,C=US"
|
||||||
|
contract:
|
||||||
|
legalContractReference: "559322B95BCF7913E3113962DC3F3CBD71C818C66977721580C045DC41C813A5"
|
||||||
|
participants:
|
||||||
|
- "CN=NodeA,O=NodeA,L=London,C=UK"
|
||||||
|
- "CN=NodeB,O=NodeB,L=New York,C=US"
|
||||||
|
notary: "CN=Controller,O=R3,OU=corda,L=London,C=UK,OU=corda.notary.validating"
|
||||||
|
encumbrance: null
|
||||||
|
ref:
|
||||||
|
txhash: "656A1BF64D5AEEC6F6C944E287F34EF133336F5FC2C5BFB9A0BFAE25E826125F"
|
||||||
|
index: 0
|
||||||
|
second: "(observable)"
|
||||||
|
|
||||||
|
But the vault of Node C should output nothing!
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
first: []
|
||||||
|
second: "(observable)"
|
||||||
|
|
||||||
|
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>`_.
|
163
docs/source/hello-world-state.rst
Normal file
163
docs/source/hello-world-state.rst
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
.. highlight:: kotlin
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||||
|
|
||||||
|
Writing the state
|
||||||
|
=================
|
||||||
|
|
||||||
|
In Corda, shared facts on the ledger are represented as states. Our first task will be to define a new state type to
|
||||||
|
represent an IOU.
|
||||||
|
|
||||||
|
The ContractState interface
|
||||||
|
---------------------------
|
||||||
|
In Corda, any JVM class that implements the ``ContractState`` interface is a valid state. ``ContractState`` is
|
||||||
|
defined as follows:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. code-block:: kotlin
|
||||||
|
|
||||||
|
interface ContractState {
|
||||||
|
// The contract that imposes constraints on how this state can evolve over time.
|
||||||
|
val contract: Contract
|
||||||
|
|
||||||
|
// The list of entities considered to have a stake in this state.
|
||||||
|
val participants: List<AbstractParty>
|
||||||
|
}
|
||||||
|
|
||||||
|
The first thing you'll probably notice about this interface declaration is that its not written in Java or another
|
||||||
|
common language. The core Corda platform, including the interface declaration above, is entirely written in Kotlin.
|
||||||
|
|
||||||
|
Learning some Kotlin will be very useful for understanding how Corda works internally, and usually only takes an
|
||||||
|
experienced Java developer a day or so to pick up. However, learning Kotlin isn't essential. Because Kotlin code
|
||||||
|
compiles down to JVM bytecode, CorDapps written in other JVM languages can interoperate with Corda.
|
||||||
|
|
||||||
|
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
|
||||||
|
* ``participants``: the list of entities that have to approve state changes such as changing the state's notary or
|
||||||
|
upgrading the state's contract
|
||||||
|
|
||||||
|
Beyond this, our state is free to define any properties, methods, helpers or inner classes it requires to accurately
|
||||||
|
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``.
|
||||||
|
|
||||||
|
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 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
|
||||||
|
you wish to add them later, its as simple as adding an additional property to your class definition.
|
||||||
|
|
||||||
|
Defining IOUState
|
||||||
|
-----------------
|
||||||
|
Let's open ``TemplateState.java`` (for Java) or ``TemplateState.kt`` (for Kotlin) and update ``TemplateState`` to
|
||||||
|
define an ``IOUState``:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. code-block:: kotlin
|
||||||
|
|
||||||
|
package com.template
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
||||||
|
override val participants get() = listOf(sender, recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
.. code-block:: java
|
||||||
|
|
||||||
|
package com.template;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import net.corda.core.contracts.ContractState;
|
||||||
|
import net.corda.core.identity.AbstractParty;
|
||||||
|
import net.corda.core.identity.Party;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public IOUState(int value, Party sender, Party recipient, IOUContract contract) {
|
||||||
|
this.value = value;
|
||||||
|
this.sender = sender;
|
||||||
|
this.recipient = recipient;
|
||||||
|
this.contract = contract;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Party getSender() {
|
||||||
|
return sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Party getRecipient() {
|
||||||
|
return recipient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
// TODO: Once we've defined IOUContract, come back and update this.
|
||||||
|
public TemplateContract getContract() {
|
||||||
|
return contract;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AbstractParty> getParticipants() {
|
||||||
|
return ImmutableList.of(sender, recipient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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):
|
||||||
|
|
||||||
|
* ``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.
|
||||||
|
|
||||||
|
* 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 left ``IOUState``'s contract as ``TemplateContract`` for now. We'll update this once we've defined the
|
||||||
|
``IOUContract``.
|
||||||
|
|
||||||
|
Progress so far
|
||||||
|
---------------
|
||||||
|
We've defined an ``IOUState`` that can be used to represent IOUs as shared facts on the ledger. As we've seen, states in
|
||||||
|
Corda are simply JVM classes that implement the ``ContractState`` interface. They can have any additional properties and
|
||||||
|
methods you like.
|
||||||
|
|
||||||
|
Next, we'll be writing our ``IOUContract`` to control the evolution of these shared facts over time.
|
85
docs/source/hello-world-template.rst
Normal file
85
docs/source/hello-world-template.rst
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
.. highlight:: kotlin
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||||
|
|
||||||
|
The CorDapp Template
|
||||||
|
====================
|
||||||
|
|
||||||
|
When writing a new CorDapp, you’ll generally want to base it on the
|
||||||
|
`Cordapp Template <https://github.com/corda/cordapp-template>`_. The Cordapp Template allows you to quickly deploy
|
||||||
|
your CorDapp onto a local test network of dummy nodes to evaluate its functionality.
|
||||||
|
|
||||||
|
Note that there's no need to download and install Corda itself. As long as you're working from a stable Milestone
|
||||||
|
branch, the required libraries will be downloaded automatically from an online repository.
|
||||||
|
|
||||||
|
If you do wish to work from the latest snapshot, please follow the instructions
|
||||||
|
`here <https://docs.corda.net/tutorial-cordapp.html#using-a-snapshot-release>`_.
|
||||||
|
|
||||||
|
Downloading the template
|
||||||
|
------------------------
|
||||||
|
Open a terminal window in the directory where you want to download the CorDapp template, and run the following commands:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
# Clone the template from GitHub:
|
||||||
|
git clone https://github.com/corda/cordapp-template.git & cd cordapp-template
|
||||||
|
|
||||||
|
# Retrieve a list of the stable Milestone branches using:
|
||||||
|
git branch -a --list *release-M*
|
||||||
|
|
||||||
|
# Check out the Milestone branch with the latest version number:
|
||||||
|
git checkout release-M[*version number*] & git pull
|
||||||
|
|
||||||
|
Template structure
|
||||||
|
------------------
|
||||||
|
We can write our CorDapp in either Java or Kotlin, and will be providing the code in both languages throughout. If
|
||||||
|
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:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. code-block:: java
|
||||||
|
|
||||||
|
// 1. The state
|
||||||
|
java-source/src/main/java/com/template/state/TemplateState.java
|
||||||
|
|
||||||
|
// 2. The contract
|
||||||
|
java-source/src/main/java/com/template/contract/TemplateContract.java
|
||||||
|
|
||||||
|
// 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
|
||||||
|
kotlin-source/src/main/kotlin/com/template/state/TemplateState.kt
|
||||||
|
|
||||||
|
// 2. The contract
|
||||||
|
kotlin-source/src/main/kotlin/com/template/contract/TemplateContract.kt
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
We'll begin writing the CorDapp proper by writing the definition of the ``IOUState``.
|
BIN
docs/source/resources/running_node.png
Normal file
BIN
docs/source/resources/running_node.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 216 KiB |
BIN
docs/source/resources/tutorial-flow.png
Normal file
BIN
docs/source/resources/tutorial-flow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 275 KiB |
BIN
docs/source/resources/tutorial-state.png
Normal file
BIN
docs/source/resources/tutorial-state.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 204 KiB |
BIN
docs/source/resources/tutorial-transaction.png
Normal file
BIN
docs/source/resources/tutorial-transaction.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 250 KiB |
@ -4,6 +4,7 @@ Tutorials
|
|||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
|
hello-world-index
|
||||||
tutorial-contract
|
tutorial-contract
|
||||||
tutorial-contract-clauses
|
tutorial-contract-clauses
|
||||||
tutorial-test-dsl
|
tutorial-test-dsl
|
||||||
|
Loading…
x
Reference in New Issue
Block a user