2.0.0 • Published 1 month ago

@hyperledger/cactus-plugin-ledger-connector-corda v2.0.0

Weekly downloads
-
License
Apache-2.0
Repository
github
Last release
1 month ago

@hyperledger/cactus-plugin-ledger-connector-corda

Table of Contents

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:

  1. Find out the fully qualified class name of your contract (flow) and set this as the value for the request parameter flowFullClassName
  2. 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 to FlowInvocationType.FLOWDYNAMIC
  3. 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:

  1. Find out the fully qualified class name of your contract (flow) and set this as the value for the request parameter flowFullClassName
  2. Decide on your flow invocation type. More details at Invoke Contract (flow) with no parameters
  3. 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 first JvmObject JSON object as follows in order to pass in the number 42 as the first and only parameter for our flow invocation:
    ```json
    params: [
      {
        jvmTypeKind: JvmTypeKind.PRIMITIVE,
        jvmType: {
          fqClassName: "long",
        },
        primitiveValue: 42,
      }
    ]
    ```
  4. 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 to GetMonitorTransactionsV1. 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" : "85e387ea730d9be7d6dc2b23caba1ee18305af74",
  "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": "85e387ea730d9be7d6dc2b23caba1ee18305af74",
  "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. The test cases (For example, packages/cactus-plugin-ledger-connector-corda/src/test/typescript/integration/deploy-cordapp-jars-to-nodes.test.ts) exposes it over 0.0.0.0 and a random port(). The random port can be found in the running logs of the test case and looks like (42379 in the below mentioned URL) Metrics URL: http://0.0.0.0: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.

2.0.0

1 month ago

2.0.0-rc.7

2 months ago

2.0.0-rc.2

5 months ago

2.0.0-rc.1

5 months ago

2.0.0-main.339

5 months ago

2.0.0-main.214

9 months ago

2.0.0-dev.196

10 months ago

2.0.0-dev.197

10 months ago

2.0.0-dev.195

10 months ago

2.0.0-main.159

11 months ago

2.0.0-dev.94

1 year ago

2.0.0-dev.93

1 year ago

2.0.0-alpha.1

1 year ago

2.0.0-alpha.2

1 year ago

2.0.0-main.91

1 year ago

1.1.1

2 years ago

1.1.0

2 years ago

1.1.3

2 years ago

1.1.2

2 years ago

1.0.0

3 years ago

1.0.0-rc.3

3 years ago

1.0.0-rc.2

3 years ago

1.0.0-rc.1

3 years ago

0.10.0

3 years ago

0.9.0

3 years ago

0.8.0

3 years ago

0.7.0

3 years ago

0.6.0

3 years ago

0.5.0-alpha.0

4 years ago

0.5.0

4 years ago

0.4.1

4 years ago

0.4.0

4 years ago