@hyperledger/cactus-plugin-ledger-connector-corda
¶
Summary¶
The Corda connector is written in Kotlin and ships as a Spring Boot JVM application that accepts API requests and translates those into Corda RPC calls.
Deploying the Corda connector therefore involves also deploying the mentioned JVM application in addition to deploying the Cactus API server with the desired plugins configured.
Concepts¶
Contract Invocation JSON DSL¶
One of our core design principles for Hyperledger Cactus is to have low impact deployments meaning that changes to the ledgers themselves should be kept to a minimum or preferably have no need for any at all. With this in mind, we had to solve the challenge of providing users with the ability to invoke Corda flows as dynamically as possible within the confines of the strongly typed JVM contrasted with the weakly typed Javascript language runtime of NodeJS.
Corda might release some convenience features to ease this in the future, but in the meantime we have the Contract Invocation JSON DSL which allows developers to specify truly arbitrary JVM types as part of their contract invocation arguments even if otherwise these types would not be possible to serialize or deserialize with traditional tooling such as the excellent Jackson JSON Java library or similar ones.
Expressing Primitive vs Reference Types with the DLS¶
The features of the DSL include expressing whether a contract invocation parameter is a reference or a primitive JVM data types. This is a language feature that Javascript has as well to some extent, but for those in need of a refresher, here’s a writeup from a well known Q/A website that I found on the internet: What’s the difference between primitive and reference types?
To keep it simple, the following types are primitive data types in the Java Virtual Machine (JVM) and everything else not included in the list below can be safely considered a reference type:
-
boolean
-
byte
-
short
-
char
-
int
-
long
-
float
-
double
If you’d like to further clarify how this works and feel like an exciting adventure then we recommend that you dive into the source code of the deserializer implementation of the JSON DSL and take a look at the following points of interest in the code located there:
-
val exoticTypes: Map<String, Class<*>>
-
fun instantiate(jvmObject: JvmObject)
Flow Invocation Types¶
Can be dynamic or tracked dynamic and the corresponding enum values are defined as:
/** * Determines which flow starting method will be used on the back-end when invoking the flow. Based on the value here the plugin back-end might invoke the rpc.startFlowDynamic() method or the rpc.startTrackedFlowDynamic() method. Streamed responses are aggregated and returned in a single response to HTTP callers who are not equipped to handle streams like WebSocket/gRPC/etc. do. * @export * @enum {string} */ export enum FlowInvocationType { TRACKEDFLOWDYNAMIC = 'TRACKED_FLOW_DYNAMIC', FLOWDYNAMIC = 'FLOW_DYNAMIC' }
Official Corda Java Docs - startFlowDynamic()
Official Corda Java Docs - startTrackedFlowDynamic()
Usage¶
Take a look at how the API client can be used to run transactions on a Corda ledger: packages/cactus-plugin-ledger-connector-corda/src/test/typescript/integration/jvm-kotlin-spring-server.test.ts
Invoke Contract (flow) with no parameters¶
Below, we’ll demonstrate invoking a simple contract with no parameters.
The contract source:
package com.example.organization.samples.application.flows;
class SomeCoolFlow { // constructor with no arguments public SomeCoolFlow() { this.doSomething(); }
public doSomething(): void { throw new RuntimeException("Method not implemented."); } }
Steps to build your request:
-
Find out the fully qualified class name of your contract (flow) and set this as the value for the request parameter
flowFullClassName
-
Decide on your flow invocation type which largely comes down to answering the question of: Does your invocation follow a request/response pattern or more like a channel subscription where multiple updates at different times are streamed to the client in response to the invocation request? In our example we assume the simpler request/response communication pattern and therefore will set the
flowInvocationType
toFlowInvocationType.FLOWDYNAMIC
-
Invoke the flow via the API client with the
params
argument being specified as an empty array[]
import { DefaultApi as CordaApi } from "hyperledger/cactus-plugin-ledger-connector-corda"; import { FlowInvocationType } from "hyperledger/cactus-plugin-ledger-connector-corda";
const apiUrl = "your-cactus-host.example.com"; // don't forget to specify the port if applicable const apiClient = new CordaApi({ basePath: apiUrl });
const res = await apiClient.invokeContractV1({ flowFullClassName: "com.example.organization.samples.application.flows.SomeCoolFlow", flowInvocationType: FlowInvocationType.FLOWDYNAMIC, params: [], timeoutMs: 60000, });
Invoke Contract (flow) with a single integer parameter¶
Below, we’ll demonstrate invoking a simple contract with a single numeric parameter.
The contract source:
package com.example.organization.samples.application.flows;
class SomeCoolFlow { // constructor with a primitive type long argument public SomeCoolFlow(long myParameterThatIsLong) { // do something with the parameter here } }
Steps to build your request:
-
Find out the fully qualified class name of your contract (flow) and set this as the value for the request parameter
flowFullClassName
-
Decide on your flow invocation type. More details at Invoke Contract (flow) with no parameters
-
Find out what is the fully qualified class name of the parameter you wish to pass in. You can do this be inspecting the sources of the contract itself. If you do not have access to those sources, then the documentation of the contract should have answers or the person who authored said contract. In our case here the fully qualified class name for the number parameter is simply
long
because it is a primitive data type and as such these can be referred to in their short form, but the fully qualified version also works such as:java.lang.Long
. When in doubt about these, you can always consult the official java.lang.Long Java Docs After having determined the above, you can construct your firstJvmObject
JSON object as follows in order to pass in the number42
as the first and only parameter for our flow invocation:params: [ { jvmTypeKind: JvmTypeKind.PRIMITIVE, jvmType: { fqClassName: "long", }, primitiveValue: 42, } ]
-
Invoke the flow via the API client with the
params
populated as explained above:import { DefaultApi as CordaApi } from "hyperledger/cactus-plugin-ledger-connector-corda"; import { FlowInvocationType } from "hyperledger/cactus-plugin-ledger-connector-corda";
// don't forget to specify the port if applicable const apiUrl = "your-cactus-host.example.com"; const apiClient = new CordaApi({ basePath: apiUrl });
const res = await apiClient.invokeContractV1({ flowFullClassName: "com.example.organization.samples.application.flows.SomeCoolFlow", flowInvocationType: FlowInvocationType.FLOWDYNAMIC, params: [ { jvmTypeKind: JvmTypeKind.PRIMITIVE, jvmType: { fqClassName: "long", }, primitiveValue: 42, } ], timeoutMs: 60000, });
Invoke Contract (flow) with a custom class parameter¶
Below, we’ll demonstrate invoking a contract with a single class instance parameter.
The contract sources:
package com.example.organization.samples.application.flows;
// contract with a class instance parameter class BuildSpaceshipFlow { public BuildSpaceshipFlow(SpaceshipInfo buildSpecs) { // build spaceship as per the specs } }
package com.example.organization.samples.application.flows;
// The type that the contract accepts as an input parameter class SpaceshipInfo { public SpaceshipInfo(String name, Integer seatsForHumans) { } }
Assembling and Sending your request:
Invoke the flow via the API client with the params
populated as shown below.
Key thing notice here is that we now have a class instance as a parameter for our contract (flow) invocation so we have to describe how this class instance itself will be instantiated by providing a nested array of parameters via the jvmCtorArgs
which stands for Java Virtual Machine Constructor Arguments meaning that elements of this array will be passed in dynamically (via Reflection) to the class constructor.
Java Equivalent
cordaRpcClient.startFlowDynamic( BuildSpaceshipFlow.class, new SpaceshipInfo( "The last spaceship you'll ever need.", 10000000 ) );
Cactus Invocation JSON DLS Equivalent to the Above Java Snippet
import { DefaultApi as CordaApi } from "hyperledger/cactus-plugin-ledger-connector-corda"; import { FlowInvocationType } from "hyperledger/cactus-plugin-ledger-connector-corda";
// don't forget to specify the port if applicable const apiUrl = "your-cactus-host.example.com"; const apiClient = new CordaApi({ basePath: apiUrl });
const res = await apiClient.invokeContractV1({ flowFullClassName: "com.example.organization.samples.application.flows.BuildSpaceshipFlow", flowInvocationType: FlowInvocationType.FLOWDYNAMIC, params: [ { jvmTypeKind: JvmTypeKind.REFERENCE, jvmType: { fqClassName: "com.example.organization.samples.application.flows.SpaceshipInfo", },
jvmCtorArgs: \[
{
jvmTypeKind: JvmTypeKind.PRIMITIVE,
jvmType: {
fqClassName: "java.lang.String",
},
primitiveValue: "The last spaceship you'll ever need.",
},
{
jvmTypeKind: JvmTypeKind.PRIMITIVE,
jvmType: {
fqClassName: "java.lang.Long",
},
primitiveValue: 10000000000,
},
\],
}
], timeoutMs: 60000, });
Transaction Monitoring¶
-
There are two interfaces to monitor changes of vault states - reactive
watchBlocksV1
method, and low-level HTTP API calls. -
Note: The monitoring APIs are implemented only on kotlin-server connector (
main-server
), not typescript connector! -
For usage examples review the functional test file:
packages/cactus-plugin-ledger-connector-corda/src/test/typescript/integration/monitor-transactions-v4.8.test.ts
-
Because transactions read from corda are stored on the connector, they will be lost if connector is closed/killed before transaction were read by the clients.
-
Each client has own set of state monitors that are managed independently. After starting the monitoring, each new transaction is queued on the connector until read and explicitly cleared by
watchBlocksV1
or direct HTTP API call. -
Client monitors can be periodically removed by the connector, if there was no action from the client for specified amount of time.
-
Client expiration delay can be configured with
cactus.sessionExpireMinutes
option. It default to 30 minutes. -
Each transaction has own index assigned by the corda connector. Index is unique for each client monitoring session. For instance:
-
Stopping monitoring for given state will reset the transaction index counter for given client. After restart, it will report first transaction with index 0.
-
Each client can see tha same transaction with different index.
-
Index can be used to determine the transaction order for given client session.
-
watchBlocksV1¶
-
watchBlocksV1(options: watchBlocksV1Options): Observable<CordaBlock>
-
Reactive (RxJS) interface to observe state changes.
-
Internally, it uses polling of low-level HTTP APIs.
-
Watching block should return each block at least once, no blocks should be missed after startMonitor has started. The only case when transaction is lost is when connector we were connected to died.
-
Transactions can be duplicated in case internal
ClearMonitorTransactionsV1
call was not successful (for instance, because of connection problems). -
Options:
-
stateFullClassName: string
: state to monitor. -
pollRate?: number
: how often poll the kotlin server for changes (default 5 seconds).
-
Low-level HTTP API¶
-
These should not be used when watchBlocks API is sufficient.
-
Consists of the following methods:
-
startMonitorV1
: Start monitoring for specified state changes. All changes after calling this function will be stored in internal kotlin-server buffer, ready to be read by calls toGetMonitorTransactionsV1
. Transactions occuring before the call to startMonitorV1 will not be reported. -
GetMonitorTransactionsV1
: Read all transactions for given state name still remaining in internal buffer. -
ClearMonitorTransactionsV1
: Remove transaction for given state name with specified index number from internal buffer. Should be used to acknowledge receiving specified transactions in user code, so that transactions are not reported multiple times. -
stopMonitorV1
: Don’t watch for transactions changes anymore, remove any transactions that were not read until now.
-
Custom Configuration via Env Variables¶
{ "cactus": { "threadCount": 3, "sessionExpireMinutes": 10, "corda": { "node": { "host": "localhost" }, "rpc": { "port": 10006, "username": "user1", "password": "test" } } } }
SPRING_APPLICATION_JSON='{"cactus":{"corda":{"node": {"host": "localhost"}, "rpc":{"port": 10006, "username":"user1", "password": "test"}}}}' gradle test
{ "flowFullClassName" : "net.corda.samples.example.flows.ExampleFlow\({"\)"}Initiator", "flowInvocationType" : "FLOW_DYNAMIC", "params" : [ { "jvmTypeKind" : "PRIMITIVE", "jvmType" : { "fqClassName" : "java.lang.Integer" }, "primitiveValue" : 42, "jvmCtorArgs" : null }, { "jvmTypeKind" : "REFERENCE", "jvmType" : { "fqClassName" : "net.corda.core.identity.Party" }, "primitiveValue" : null, "jvmCtorArgs" : [ { "jvmTypeKind" : "REFERENCE", "jvmType" : { "fqClassName" : "net.corda.core.identity.CordaX500Name" }, "primitiveValue" : null, "jvmCtorArgs" : [ { "jvmTypeKind" : "PRIMITIVE", "jvmType" : { "fqClassName" : "java.lang.String" }, "primitiveValue" : "PartyB", "jvmCtorArgs" : null }, { "jvmTypeKind" : "PRIMITIVE", "jvmType" : { "fqClassName" : "java.lang.String" }, "primitiveValue" : "New York", "jvmCtorArgs" : null }, { "jvmTypeKind" : "PRIMITIVE", "jvmType" : { "fqClassName" : "java.lang.String" }, "primitiveValue" : "US", "jvmCtorArgs" : null } ] }, { "jvmTypeKind" : "REFERENCE", "jvmType" : { "fqClassName" : "org.hyperledger.cactus.plugin.ledger.connector.corda.server.impl.PublicKeyImpl" }, "primitiveValue" : null, "jvmCtorArgs" : [ { "jvmTypeKind" : "PRIMITIVE", "jvmType" : { "fqClassName" : "java.lang.String" }, "primitiveValue" : "EdDSA", "jvmCtorArgs" : null }, { "jvmTypeKind" : "PRIMITIVE", "jvmType" : { "fqClassName" : "java.lang.String" }, "primitiveValue" : "X.509", "jvmCtorArgs" : null }, { "jvmTypeKind" : "PRIMITIVE", "jvmType" : { "fqClassName" : "java.lang.String" }, "primitiveValue" : "MCowBQYDK2VwAyEAoOv19eiCDJ7HzR9UrfwbFig7qcD1jkewKkkS4WF9kPA=", "jvmCtorArgs" : null } ] } ] } ], "timeoutMs" : null }
I 16:51:01 1 Client.main - nodeDiagnosticInfo= { "version" : "4.6", "revision" : "85e387e", "platformVersion" : 8, "vendor" : "Corda Open Source", "cordapps" : [ { "type" : "Workflow CorDapp", "name" : "workflows-1.0", "shortName" : "Example-Cordapp Flows", "minimumPlatformVersion" : 8, "targetPlatformVersion" : 8, "version" : "1", "vendor" : "Corda Open Source", "licence" : "Apache License, Version 2.0", "jarHash" : { "offset" : 0, "size" : 32, "bytes" : "V7ssTw0etgg3nSGk1amArB+fBH8fQUyBwIFs0DhID+0=" } }, { "type" : "Contract CorDapp", "name" : "contracts-1.0", "shortName" : "Example-Cordapp Contracts", "minimumPlatformVersion" : 8, "targetPlatformVersion" : 8, "version" : "1", "vendor" : "Corda Open Source", "licence" : "Apache License, Version 2.0", "jarHash" : { "offset" : 0, "size" : 32, "bytes" : "Xe0eoh4+T6fsq4u0QKqkVsVDMYSWhuspHqE0wlOlyqU=" } } ] }
Building Docker Image Locally¶
The cccs
tag used in the below example commands is a shorthand for the full name of the container image otherwise referred to as cactus-corda-connector-server
.
From the project root:
DOCKER_BUILDKIT=1 docker build ./packages/cactus-plugin-ledger-connector-corda/src/main-server/ -t cccs
Example NodeDiagnosticInfo JSON Response¶
{ "version": "4.6", "revision": "85e387e", "platformVersion": 8, "vendor": "Corda Open Source", "cordapps": [ { "type": "Workflow CorDapp", "name": "workflows-1.0", "shortName": "Obligation Flows", "minimumPlatformVersion": 8, "targetPlatformVersion": 8, "version": "1", "vendor": "Corda Open Source", "licence": "Apache License, Version 2.0", "jarHash": { "bytes": "Vf9MllnrC7vrWxrlDE94OzPMZW7At1HhTETL/XjiAmc=", "offset": 0, "size": 32 } }, { "type": "CorDapp", "name": "corda-confidential-identities-4.6", "shortName": "corda-confidential-identities-4.6", "minimumPlatformVersion": 1, "targetPlatformVersion": 1, "version": "Unknown", "vendor": "Unknown", "licence": "Unknown", "jarHash": { "bytes": "nqBwqHJMbLW80hmRbKEYk0eAknFiX8N40LKuGsD0bPo=", "offset": 0, "size": 32 } }, { "type": "Contract CorDapp", "name": "corda-finance-contracts-4.6", "shortName": "Corda Finance Demo", "minimumPlatformVersion": 1, "targetPlatformVersion": 8, "version": "1", "vendor": "R3", "licence": "Open Source (Apache 2)", "jarHash": { "bytes": "a43Q/GJG6JKTZzq3U80P8L1DWWcB/D+Pl5uitEtAeQQ=", "offset": 0, "size": 32 } }, { "type": "Workflow CorDapp", "name": "corda-finance-workflows-4.6", "shortName": "Corda Finance Demo", "minimumPlatformVersion": 1, "targetPlatformVersion": 8, "version": "1", "vendor": "R3", "licence": "Open Source (Apache 2)", "jarHash": { "bytes": "wXdD4Iy50RaWzPp7n9s1xwf4K4MB8eA1nmhPquTMvxg=", "offset": 0, "size": 32 } }, { "type": "Contract CorDapp", "name": "contracts-1.0", "shortName": "Obligation Contracts", "minimumPlatformVersion": 8, "targetPlatformVersion": 8, "version": "1", "vendor": "Corda Open Source", "licence": "Apache License, Version 2.0", "jarHash": { "bytes": "grTZzN71Cpxw6rZe/U5SB6/ehl99B6VQ1+ZJEx1rixs=", "offset": 0, "size": 32 } } ] }
Monitoring¶
Usage Prometheus¶
The prometheus exporter object is initialized in the PluginLedgerConnectorCorda
class constructor itself, so instantiating the object of the PluginLedgerConnectorCorda
class, gives access to the exporter object. You can also initialize the prometheus exporter object seperately and then pass it to the IPluginLedgerConnectorCordaOptions
interface for PluginLedgerConnectoCorda
constructor.
getPrometheusExporterMetricsEndpointV1
function returns the prometheus exporter metrics, currently displaying the total transaction count, which currently increments everytime the transact()
method of the PluginLedgerConnectorCorda
class is called.
Prometheus Integration¶
To use Prometheus with this exporter make sure to install Prometheus main component. Once Prometheus is setup, the corresponding scrape_config needs to be added to the prometheus.yml
- job_name: 'corda_ledger_connector_exporter' metrics_path: api/v1/plugins/hyperledger/cactus-plugin-ledger-connector-corda/get-prometheus-exporter-metrics scrape_interval: 5s static_configs: - targets: ['{host}:{port}']
Here the host:port
is where the prometheus exporter metrics are exposed. An example URL for metric would be something like this:
http://localhost:42379/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-corda/get-prometheus-exporter-metrics
Once edited, you can start the prometheus service by referencing the above edited prometheus.yml file. On the prometheus graphical interface (defaulted to http://localhost:9090), choose Graph from the menu bar, then select the Console tab. From the Insert metric at cursor drop down, select cactus_corda_total_tx_count and click execute
Helper code¶
response.type.ts¶
This file contains the various responses of the metrics.
data-fetcher.ts¶
This file contains functions encasing the logic to process the data points
metrics.ts¶
This file lists all the prometheus metrics and what they are used for.