mirror of
https://github.com/corda/corda.git
synced 2024-12-22 06:17:55 +00:00
Better docs of CorDapp structure and node interaction (#3761)
* Clean-up. Instructions on how template would be modified for production. * Change page titles to make it clearer make they contain. * Simple example of how to connect to node via RPC. Explanation of how to interact with node via RPC. * Bigger warning about deprecated webserver. Makes it clear that CordaRPCClient is THE way to interact with a node. * Review from Clinton. * Separating template info from general info.
This commit is contained in:
parent
911aa1381b
commit
5d39f2bb46
@ -1,27 +1,52 @@
|
|||||||
Client RPC
|
.. highlight:: kotlin
|
||||||
==========
|
.. raw:: html
|
||||||
|
|
||||||
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||||
|
|
||||||
|
Interacting with a node
|
||||||
|
=======================
|
||||||
|
|
||||||
.. contents::
|
.. contents::
|
||||||
|
|
||||||
Overview
|
Overview
|
||||||
--------
|
--------
|
||||||
Corda provides a client library that allows you to easily write clients in a JVM-compatible language to interact
|
You should interact with your node using the `CordaRPCClient`_ library. This library that allows you to easily
|
||||||
with a running node. The library connects to the node using a message queue protocol and then provides a simple RPC
|
write clients in a JVM-compatible language to interact with a running node. The library connects to the node using a
|
||||||
interface to interact with the node. You make calls on a Java object as normal, and the marshalling back and forth is
|
message queue protocol and then provides a simple RPC interface to interact with the node. You make calls on a JVM
|
||||||
handled for you.
|
object as normal, and the marshalling back and forth is handled for you.
|
||||||
|
|
||||||
The starting point for the client library is the `CordaRPCClient`_ class. `CordaRPCClient`_ provides a ``start`` method
|
.. warning:: The built-in Corda webserver is deprecated and unsuitable for production use. If you want to interact with
|
||||||
that returns a `CordaRPCConnection`_. A `CordaRPCConnection`_ allows you to access an implementation of the
|
your node via HTTP, you will need to stand up your own webserver, then create an RPC connection between your node
|
||||||
`CordaRPCOps`_ interface with ``proxy`` in Kotlin or ``getProxy()`` in Java. The observables that are returned by RPC
|
and this webserver using the `CordaRPCClient`_ library. You can find an example of how to do this
|
||||||
operations can be subscribed to in order to receive an ongoing stream of updates from the node. More detail on this
|
`here <https://github.com/corda/spring-webserver>`_.
|
||||||
functionality is provided in the docs for the ``proxy`` method.
|
|
||||||
|
Connecting to a node via RPC
|
||||||
|
----------------------------
|
||||||
|
`CordaRPCClient`_ provides a ``start`` method that takes the node's RPC address and returns a `CordaRPCConnection`_.
|
||||||
|
`CordaRPCConnection`_ provides a ``proxy`` method that takes an RPC username and password and returns a `CordaRPCOps`_
|
||||||
|
object that you can use to interact with the node.
|
||||||
|
|
||||||
|
Here is an example of using `CordaRPCClient`_ to connect to a node and log the current time on its internal clock:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcExample.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: START 1
|
||||||
|
:end-before: END 1
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/java/net/corda/docs/ClientRpcExampleJava.java
|
||||||
|
:language: java
|
||||||
|
:start-after: START 1
|
||||||
|
:end-before: END 1
|
||||||
|
|
||||||
.. warning:: The returned `CordaRPCConnection`_ is somewhat expensive to create and consumes a small amount of
|
.. warning:: The returned `CordaRPCConnection`_ is somewhat expensive to create and consumes a small amount of
|
||||||
server side resources. When you're done with it, call ``close`` on it. Alternatively you may use the ``use``
|
server side resources. When you're done with it, call ``close`` on it. Alternatively you may use the ``use``
|
||||||
method on `CordaRPCClient`_ which cleans up automatically after the passed in lambda finishes. Don't create
|
method on `CordaRPCClient`_ which cleans up automatically after the passed in lambda finishes. Don't create
|
||||||
a new proxy for every call you make - reuse an existing one.
|
a new proxy for every call you make - reuse an existing one.
|
||||||
|
|
||||||
For a brief tutorial on using the RPC API, see :doc:`tutorial-clientrpc-api`.
|
For further information on using the RPC API, see :doc:`tutorial-clientrpc-api`.
|
||||||
|
|
||||||
RPC permissions
|
RPC permissions
|
||||||
---------------
|
---------------
|
||||||
@ -306,7 +331,7 @@ side as if it was thrown from inside the called RPC method. These exceptions can
|
|||||||
|
|
||||||
Connection management
|
Connection management
|
||||||
---------------------
|
---------------------
|
||||||
It is possible to not be able to connect to the server on the first attempt. In that case, the ``CordaRPCCLient.start()``
|
It is possible to not be able to connect to the server on the first attempt. In that case, the ``CordaRPCClient.start()``
|
||||||
method will throw an exception. The following code snippet is an example of how to write a simple retry mechanism for
|
method will throw an exception. The following code snippet is an example of how to write a simple retry mechanism for
|
||||||
such situations:
|
such situations:
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Node configuration
|
Configuring a node
|
||||||
==================
|
==================
|
||||||
|
|
||||||
.. contents::
|
.. contents::
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
Building a CorDapp
|
Building and installing a CorDapp
|
||||||
==================
|
=================================
|
||||||
|
|
||||||
.. contents::
|
.. contents::
|
||||||
|
|
||||||
|
@ -3,20 +3,38 @@ What is a CorDapp?
|
|||||||
|
|
||||||
CorDapps (Corda Distributed Applications) are distributed applications that run on the Corda platform. The goal of a
|
CorDapps (Corda Distributed Applications) are distributed applications that run on the Corda platform. The goal of a
|
||||||
CorDapp is to allow nodes to reach agreement on updates to the ledger. They achieve this goal by defining flows that
|
CorDapp is to allow nodes to reach agreement on updates to the ledger. They achieve this goal by defining flows that
|
||||||
Corda node owners can invoke through RPC calls:
|
Corda node owners can invoke over RPC:
|
||||||
|
|
||||||
.. image:: resources/node-diagram.png
|
.. image:: resources/node-diagram.png
|
||||||
|
:scale: 25%
|
||||||
|
:align: center
|
||||||
|
|
||||||
CorDapps are made up of the following key components:
|
CorDapp components
|
||||||
|
------------------
|
||||||
|
CorDapps take the form of a set of JAR files containing class definitions written in Java and/or Kotlin.
|
||||||
|
|
||||||
* States, defining the facts over which agreement is reached (see :doc:`Key Concepts - States <key-concepts-states>`)
|
These class definitions will commonly include the following elements:
|
||||||
|
|
||||||
|
* Flows: Define a routine for the node to run, usually to update the ledger
|
||||||
|
(see :doc:`Key Concepts - Flows <key-concepts-flows>`). They subclass ``FlowLogic``
|
||||||
|
* States: Define the facts over which agreement is reached (see :doc:`Key Concepts - States <key-concepts-states>`).
|
||||||
|
They implement the ``ContractState`` interface
|
||||||
* Contracts, defining what constitutes a valid ledger update (see
|
* Contracts, defining what constitutes a valid ledger update (see
|
||||||
:doc:`Key Concepts - Contracts <key-concepts-contracts>`)
|
:doc:`Key Concepts - Contracts <key-concepts-contracts>`). They implement the ``Contract`` interface
|
||||||
* Services, providing long-lived utilities within the node
|
* Services, providing long-lived utilities within the node. They subclass ``SingletonSerializationToken``
|
||||||
* Serialisation whitelists, restricting what types your node will receive off the wire
|
* Serialisation whitelists, restricting what types your node will receive off the wire. They implement the
|
||||||
|
``SerializationWhitelist`` interface
|
||||||
|
|
||||||
Each CorDapp is installed at the level of the individual node, rather than on the network itself. For example, a node
|
But the CorDapp JAR can also include other class definitions. These may include:
|
||||||
owner may choose to install the Bond Trading CorDapp, with the following components:
|
|
||||||
|
* APIs and static web content: These are served by Corda's built-in webserver. This webserver is not
|
||||||
|
production-ready, and should be used for testing purposes only
|
||||||
|
* Utility classes
|
||||||
|
|
||||||
|
An example
|
||||||
|
----------
|
||||||
|
Suppose a node owner wants their node to be able to trade bonds. They may choose to install a Bond Trading CorDapp with
|
||||||
|
the following components:
|
||||||
|
|
||||||
* A ``BondState``, used to represent bonds as shared facts on the ledger
|
* A ``BondState``, used to represent bonds as shared facts on the ledger
|
||||||
* A ``BondContract``, used to govern which ledger updates involving ``BondState`` states are valid
|
* A ``BondContract``, used to govern which ledger updates involving ``BondState`` states are valid
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
package net.corda.docs;
|
||||||
|
|
||||||
|
// START 1
|
||||||
|
import net.corda.client.rpc.CordaRPCClient;
|
||||||
|
import net.corda.client.rpc.CordaRPCConnection;
|
||||||
|
import net.corda.core.messaging.CordaRPCOps;
|
||||||
|
import net.corda.core.utilities.NetworkHostAndPort;
|
||||||
|
import org.apache.activemq.artemis.api.core.ActiveMQException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
class ExampleRpcClientJava {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ExampleRpcClient.class);
|
||||||
|
|
||||||
|
public static void main(String[] args) throws ActiveMQException, InterruptedException, ExecutionException {
|
||||||
|
if (args.length != 3) {
|
||||||
|
throw new IllegalArgumentException("Usage: TemplateClient <node address> <username> <password>");
|
||||||
|
}
|
||||||
|
final NetworkHostAndPort nodeAddress = NetworkHostAndPort.parse(args[0]);
|
||||||
|
String username = args[1];
|
||||||
|
String password = args[2];
|
||||||
|
|
||||||
|
final CordaRPCClient client = new CordaRPCClient(nodeAddress);
|
||||||
|
final CordaRPCConnection connection = client.start(username, password);
|
||||||
|
final CordaRPCOps cordaRPCOperations = connection.getProxy();
|
||||||
|
|
||||||
|
logger.info(cordaRPCOperations.currentNodeTime().toString());
|
||||||
|
|
||||||
|
connection.notifyServerAndClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// END 1
|
@ -0,0 +1,31 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package net.corda.docs
|
||||||
|
|
||||||
|
// START 1
|
||||||
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
|
import net.corda.core.utilities.NetworkHostAndPort.Companion.parse
|
||||||
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import org.slf4j.Logger
|
||||||
|
|
||||||
|
class ExampleRpcClient {
|
||||||
|
companion object {
|
||||||
|
val logger: Logger = loggerFor<ExampleRpcClient>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
require(args.size == 3) { "Usage: TemplateClient <node address> <username> <password>" }
|
||||||
|
val nodeAddress = parse(args[0])
|
||||||
|
val username = args[1]
|
||||||
|
val password = args[2]
|
||||||
|
|
||||||
|
val client = CordaRPCClient(nodeAddress)
|
||||||
|
val connection = client.start(username, password)
|
||||||
|
val cordaRPCOperations = connection.proxy
|
||||||
|
|
||||||
|
logger.info(cordaRPCOperations.currentNodeTime().toString())
|
||||||
|
|
||||||
|
connection.notifyServerAndClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// END 1
|
@ -4,8 +4,8 @@
|
|||||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||||
<script type="text/javascript" src="_static/codesets.js"></script>
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||||
|
|
||||||
Shell
|
Node shell
|
||||||
=====
|
==========
|
||||||
|
|
||||||
.. contents::
|
.. contents::
|
||||||
|
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
.. highlight:: kotlin
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||||
|
|
||||||
.. _graphstream: http://graphstream-project.org/
|
.. _graphstream: http://graphstream-project.org/
|
||||||
|
|
||||||
Using the client RPC API
|
Using the client RPC API
|
||||||
|
@ -1,58 +1,70 @@
|
|||||||
Writing a CorDapp
|
CorDapp structure
|
||||||
=================
|
=================
|
||||||
|
|
||||||
.. contents::
|
.. contents::
|
||||||
|
|
||||||
Overview
|
|
||||||
--------
|
|
||||||
CorDapps can be written in either Java, Kotlin, or a combination of the two. Each CorDapp component takes the form
|
|
||||||
of a JVM class that subclasses or implements a Corda library type:
|
|
||||||
|
|
||||||
* Flows subclass ``FlowLogic``
|
|
||||||
* States implement ``ContractState``
|
|
||||||
* Contracts implement ``Contract``
|
|
||||||
* Services subclass ``SingletonSerializationToken``
|
|
||||||
* Serialisation whitelists implement ``SerializationWhitelist``
|
|
||||||
|
|
||||||
Web content and RPC clients
|
|
||||||
---------------------------
|
|
||||||
For testing purposes, CorDapps may also include:
|
|
||||||
|
|
||||||
* **APIs and static web content**: These are served by Corda's built-in webserver. This webserver is not
|
|
||||||
production-ready, and should be used for testing purposes only
|
|
||||||
|
|
||||||
* **RPC clients**: These are programs that automate the process of interacting with a node via RPC
|
|
||||||
|
|
||||||
In production, a production-ready webserver should be used, and these files should be moved into a different module or
|
|
||||||
project so that they do not bloat the CorDapp at build time.
|
|
||||||
|
|
||||||
.. _cordapp-structure:
|
.. _cordapp-structure:
|
||||||
|
|
||||||
Structure and dependencies
|
Modules
|
||||||
--------------------------
|
-------
|
||||||
You should base your project on the Java template (for CorDapps written in Java) or the Kotlin template (for CorDapps
|
The source code for a CorDapp is divided into one or more modules, each of which will be compiled into a separate JAR.
|
||||||
written in Kotlin):
|
Together, these JARs represent a single CorDapp. Typically, a Cordapp contains all the classes required for it to be
|
||||||
|
used standalone. However, some Cordapps are only libraries for other Cordapps and cannot be run standalone.
|
||||||
|
|
||||||
* `Java Template CorDapp <https://github.com/corda/cordapp-template-java>`_
|
A common pattern is to have:
|
||||||
* `Kotlin Template CorDapp <https://github.com/corda/cordapp-template-kotlin>`_
|
|
||||||
|
|
||||||
Please checkout the branch of the template that corresponds to the version of Corda you are using. For example, someone
|
* One module containing only the CorDapp's contracts and/or states, as well as any required dependencies
|
||||||
building a CorDapp on Corda 3 should use the ``release-V3`` branch of the template.
|
* A second module containing the remaining classes that depend on these contracts and/or states
|
||||||
|
|
||||||
The required dependencies are defined by the ``build.gradle`` file in the root directory of the template.
|
This is because each time a contract is used in a transaction, the entire JAR containing the contract's definition is
|
||||||
|
attached to the transaction. This is to ensure that the exact same contract and state definitions are used when
|
||||||
|
verifying this transaction at a later date. Because of this, you will want to keep this module, and therefore the
|
||||||
|
resulting JAR file, as small as possible to reduce the size of your transactions and keep your node performant.
|
||||||
|
|
||||||
The project should be split into two modules:
|
However, this two-module structure is not prescriptive:
|
||||||
|
|
||||||
* A ``cordapp-contracts-states`` module containing classes such as contracts and states that will be sent across the
|
* A library CorDapp containing only contracts and states would only need a single module
|
||||||
wire as part of a flow
|
|
||||||
* A ``cordapp`` module containing the remaining classes
|
|
||||||
|
|
||||||
Each module will be compiled into its own CorDapp. This minimises the size of the JAR that has to be sent across the
|
* In a CorDapp with multiple sets of contracts and states that **do not** depend on each other, each independent set of
|
||||||
wire when nodes are agreeing ledger updates.
|
contracts and states would go in a separate module to reduce transaction size
|
||||||
|
|
||||||
|
* In a CorDapp with multiple sets of contracts and states that **do** depend on each other, either keep them in the
|
||||||
|
same module or create separate modules that depend on each other
|
||||||
|
|
||||||
|
* The module containing the flows and other classes can be structured in any way because it is not attached to
|
||||||
|
transactions
|
||||||
|
|
||||||
|
Template CorDapps
|
||||||
|
-----------------
|
||||||
|
You should base your project on one of the following templates:
|
||||||
|
|
||||||
|
* `Java Template CorDapp <https://github.com/corda/cordapp-template-java>`_ (for CorDapps written in Java)
|
||||||
|
* `Kotlin Template CorDapp <https://github.com/corda/cordapp-template-kotlin>`_ (for CorDapps written in Kotlin)
|
||||||
|
|
||||||
|
Please use the branch of the template that corresponds to the major version of Corda you are using. For example,
|
||||||
|
someone building a CorDapp on Corda 3.2 should use the ``release-V3`` branch of the template.
|
||||||
|
|
||||||
|
Build system
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The templates are built using Gradle. A Gradle wrapper is provided in the ``wrapper`` folder, and the dependencies are
|
||||||
|
defined in the ``build.gradle`` files. See :doc:`cordapp-build-systems` for more information.
|
||||||
|
|
||||||
|
No templates are currently provided for Maven or other build systems.
|
||||||
|
|
||||||
|
Modules
|
||||||
|
^^^^^^^
|
||||||
|
The templates are split into two modules:
|
||||||
|
|
||||||
|
* A ``cordapp-contracts-states`` module containing the contracts and states
|
||||||
|
* A ``cordapp`` module containing the remaining classes that depends on the ``cordapp-contracts-states`` module
|
||||||
|
|
||||||
|
These modules will be compiled into two JARs - a ``cordapp-contracts-states`` JAR and a ``cordapp`` JAR - which
|
||||||
|
together represent the Template CorDapp.
|
||||||
|
|
||||||
Module one - cordapp-contracts-states
|
Module one - cordapp-contracts-states
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Here is the structure of the ``src`` directory for the ``cordapp-contracts-states`` module:
|
Here is the structure of the ``src`` directory for the ``cordapp-contracts-states`` module of the Java template:
|
||||||
|
|
||||||
.. parsed-literal::
|
.. parsed-literal::
|
||||||
|
|
||||||
@ -73,8 +85,8 @@ These are definitions for classes that we expect to have to send over the wire.
|
|||||||
CorDapp.
|
CorDapp.
|
||||||
|
|
||||||
Module two - cordapp
|
Module two - cordapp
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
Here is the structure of the ``src`` directory for the ``cordapp`` module:
|
Here is the structure of the ``src`` directory for the ``cordapp`` module of the Java template:
|
||||||
|
|
||||||
.. parsed-literal::
|
.. parsed-literal::
|
||||||
|
|
||||||
@ -116,37 +128,27 @@ The ``src`` directory is structured as follows:
|
|||||||
|
|
||||||
Within ``main``, we have the following directories:
|
Within ``main``, we have the following directories:
|
||||||
|
|
||||||
* ``resources/META-INF/services`` contains registries of the CorDapp's serialisation whitelists and web plugins
|
* ``java``, which contains the source-code for our CorDapp:
|
||||||
* ``resources/certificates`` contains dummy certificates for test purposes
|
|
||||||
* ``resources/templateWeb`` contains a dummy front-end
|
|
||||||
* ``java`` (or ``kotlin`` in the Kotlin template), which includes the source-code for our CorDapp
|
|
||||||
|
|
||||||
The source-code for our CorDapp breaks down as follows:
|
* ``TemplateFlow.java``, which contains a template ``FlowLogic`` subclass
|
||||||
|
* ``TemplateState.java``, which contains a template ``ContractState`` implementation
|
||||||
|
* ``TemplateContract.java``, which contains a template ``Contract`` implementation
|
||||||
|
* ``TemplateSerializationWhitelist.java``, which contains a template ``SerializationWhitelist`` implementation
|
||||||
|
* ``TemplateApi.java``, which contains a template API for the deprecated Corda webserver
|
||||||
|
* ``TemplateWebPlugin.java``, which registers the API and front-end for the deprecated Corda webserver
|
||||||
|
* ``TemplateClient.java``, which contains a template RPC client for interacting with our CorDapp
|
||||||
|
|
||||||
* ``TemplateFlow.java``, which contains a dummy ``FlowLogic`` subclass
|
* ``resources/META-INF/services``, which contains various registries:
|
||||||
* ``TemplateState.java``, which contains a dummy ``ContractState`` implementation
|
|
||||||
* ``TemplateContract.java``, which contains a dummy ``Contract`` implementation
|
|
||||||
* ``TemplateSerializationWhitelist.java``, which contains a dummy ``SerializationWhitelist`` implementation
|
|
||||||
|
|
||||||
In developing your CorDapp, you should start by modifying these classes to define the components of your CorDapp. A
|
* ``net.corda.core.serialization.SerializationWhitelist``, which registers the CorDapp's serialisation whitelists
|
||||||
single CorDapp can define multiple flows, states, and contracts.
|
* ``net.corda.webserver.services.WebServerPluginRegistry``, which registers the CorDapp's web plugins
|
||||||
|
|
||||||
The template also includes a web API and RPC client:
|
* ``resources/templateWeb``, which contains a template front-end
|
||||||
|
|
||||||
* ``TemplateApi.java``
|
In a production CorDapp:
|
||||||
* ``TemplateClient.java``
|
|
||||||
* ``TemplateWebPlugin.java``
|
|
||||||
|
|
||||||
These are for testing purposes and would be removed in a production CorDapp.
|
* We would remove the files related to the deprecated Corda webserver (``TemplateApi.java``,
|
||||||
|
``TemplateWebPlugin.java``, ``resources/templateWeb``, and ``net.corda.webserver.services.WebServerPluginRegistry``)
|
||||||
|
and replace them with a production-ready webserver
|
||||||
|
|
||||||
Resources
|
* We would also move ``TemplateClient.java`` into a separate module so that it is not included in the CorDapp
|
||||||
---------
|
|
||||||
In writing a CorDapp, these pages may be particularly helpful:
|
|
||||||
|
|
||||||
* :doc:`getting-set-up`, to set up your development environment.
|
|
||||||
* The :doc:`hello-world-introduction` tutorial to write your first CorDapp.
|
|
||||||
* :doc:`cordapp-build-systems` to build and run your CorDapp.
|
|
||||||
* The `API docs </api/javadoc/index.html>`_ to read about the API available in developing CorDapps.
|
|
||||||
* There is also a :doc:`cheat-sheet` recapping the key types.
|
|
||||||
* The :doc:`flow-cookbook` to see code examples of how to perform common flow tasks.
|
|
||||||
* `Sample CorDapps <https://www.corda.net/samples/>`_ showing various parts of Corda's functionality.
|
|
Loading…
Reference in New Issue
Block a user