The sic boilerplate generator

sic is a tool for generating the boilerplate code that makes it possible to seamlessly interact with a CSL contract from another language. Currently it supports two target languages: Kotlin and TypeScript. Because Kotlin is a JVM-based language sic indirectly supports other such languages like Java. Likewise, TypeScript is a language that is compiled to JavaScript and is designed to be interoperable with JavaScript code, so sic indirectly supports JavaScript as well.

Overview

When you have written a CSL contract and want to integrate it in a larger application you need some way of communicating with the system responsible for running the CSL contract. This can be done using the public API and one of the API clients, however, this means that you would have to take care of sending the right JSON-encoded data yourself, with no possibility of help from your language’s type checker.

sic generates mappings from the CSL data types to native data types in the target language, meaning that you can get help from the target language’s type checker and IDE support when building values of these types. Moreover, reports, events and entry points (templates) in the contract are mapped to suitable constructs in the target language. Hence, instead of communicating directly using the Deon API, you can instead use specialized functions that use the generated native data types as input and output types, and which takes care of (de)serializing to the JSON format expected by Deon’s API. This makes it easier to work with the contract as you get the support that you would otherwise get when working in the target language. On top of that, it makes it dramatically less time-consuming to make changes to the CSL contract, as you will get the updated mappings for free by re-running sic, and any inconsistencies in the way you use the generated functions will be immediately caught by the target language’s typechecker.

Given a CSL contract, sic will generate the following components in the target language:

Data type definitions
Each data type in the contract will be mapped to a data type in the target language.
Report functions
For each report in the contract a corresponding function for invoking that report is created.
Event application functions
For each Event type in the contract a function for applying that event to a contract is created.
Contract instantiation functions
For each top-level template in the contract a function for instantiating a contract from that template is created.

Usage

sic is a command-line tool that works on Windows, MacOS, and Linux:

$ sic --help
sic <VERSION>

Usage: sic [-V|--version] [-n|--namespace NAMESPACE] [-t|--target ARG]
           [--stdlib PATH] [-w|--write] [-d|--destination PATH] FILES

Available options:
  -V,--version             Print version information
  -h,--help                Show this help text
  -n,--namespace NAMESPACE Namespace to put generated code in
  -t,--target ARG          Target language (default: Kotlin)
  --stdlib PATH            Use alternative CSL standard library
  -w,--write               Write generated source files to disk instead of just
                           printing them to stdout
  -d,--destination PATH    Root directory for generated source
                           files (default: "generated")

We shall use the contract “sic1.csl” for demonstration of how to use sic and for illustrating key points about the structure of the generated code:

type CustomerType
    | Regular Int
    | OneTime

type Address {
    street: String,
    number: Int,
    floor: Int
}

type Customer {
    name: String,
    age: Int,
    address: Address,
    customerType: CustomerType
}

type AddCustomer : Event {
    id : Int,
    customer : Customer
}

//  sum : List Int -> Int
val sum = \ints -> foldl (\(x : Int) -> \y -> x + y) 0 ints

//  sumCustomerAge : List Customer -> Int
val sumCustomerAge =
  \(customers : List Customer) ->
    sum (List::map (\(c : Customer) -> c.age) customers)

template rec Shop(total) = <*> a: AddCustomer
  where a.id = total then Shop(total + 1)

Using Kotlin as the target language

The default target language of sic is Kotlin, and the default behaviour is to write the generated code to standard output. Thus, when we run the command

$ sic sic1.csl

it will print a bunch of Kotlin code to the terminal. If we pass the flag --write to sic it will write the code to disk:

$ sic --write contract.csl
Generating interface for sic1.csl
Wrote file generated/com/deondigital/api/contract/sic1/Sic1.kt
Wrote file generated/com/deondigital/api/contract/sic1/Sic1.csl.kt

We see here that one CSL contract is represented as two Kotlin source files in the package com.deondigital.api.contract.sic1. The generated code will be put into the root directory generated/. Both the package name and the root directory can be changed with the flags --namespace (-n) and --destination (-d), respectively.

The first file, Sic1.kt, contains all the interface and data definitions that enable us to interact with the CSL contract from Kotlin in a convenient manner. The second file, Sic1.csl.kt, is an embedding of the contract source in Kotlin.

Data types

The file Sic1.kt will contain, amongst many other things, the definitions of the following data types:

sealed class CustomerType : Convertible {
  /* ... */
  data class Regular(val field0: Int) : CustomerType() {
    /* .. */
  }
  object OneTime : CustomerType() {
    /* .. */
  }
}
data class Address(
             val street: String,
             val number: Int,
             val floor: Int) : Convertible {
  /* ... */
}
data class Customer(
             val name: String,
             val age: Int,
             val address: Address,
             val customerType: CustomerType) : Convertible {
  /* ... */
}

We have left out a lot of details here, but the snippet demonstrates how a sum type in CSL is converted to a sealed class in CSL with a subclass for each constructor while a CSL record type is converted to a data class. The names of parameters of a data class match the names in the CSL record. Base types such as Int and String are represented by their native counterparts in Kotlin: kotlin.String and kotlin.Int.

Reports

The CSL reports are converted to functions in the target language with appropriate types. That is, the input and output types are mappings from the CSL type to the target language type as described in the above section.

In our generated Kotlin code, we can find the following interface:

interface ReportService {
  fun sum(ints: List<Int>) : Int
  fun sumCustomerAge(customers: List<Customer>) : Int
}

That is, the interface com.deondigital.api.sic1.ReportService declares two functions, one for each of the CSL reports in sic1.csl. The input and output types are mapped from the corresponding CSL types; note that the CSL List a type is mapped to Kotlin/Java’s List<T> type. To use the interface we can instantiate the class ReportServiceImpl which takes two parameters in its constructor:

  1. A reportCaller : (String, String, List<Value>) -> Value which is the function used to issue the report call to the Deon API. The parameters of this function match the ones taken by the DeonApiClient::postReport function, so in the usual case you can just supply the postReport method from a DeonApiClient that is set up with the right URL.
  2. A contractId – this is the contract on which the reports will be run.

The following snippet illustrates how one would typically run a report with this interface:

val apiClient = DeonAPIClient(API_URL) // Connect to the ledger
val contractId = "1234"
val r = ReportServiceImpl(apiClient::postReport, contractId)
val s = r.sumCustomerAge(listOf(
    Customer("bob", 42, Address("Main st.", 1, 5), CustomerType.OneTime),
    Customer("alice", 30, Address("Main st.", 10, 2), CustomerType.Regular(1)))
) // == 72

Contract instantiation

Every top-level template declaration in a CSL contract represents a possible instantiation point of a contract in the system.

The generated Kotlin code for sic1.csl contains the following interface:

interface Instantiate<T> {
  fun Shop(total: Int, peers : List<String> = listOf()) : T
}

Like the code for reports, instantiation is encapsulated in an interface with an accompanying implementation, InstantiateImpl, with a function for each top-level template in the CSL contract. In order to make it possible to use both the asynchronous and the synchronous interface from the DeonAPIClient, the interface is parametric in the return type of the instantiation functions. Moreover, all instantiation functions take the same parameters as the CSL templates (mapped to the Kotlin type), plus an additional parameter peers that can be used to specify on which peers a contract should be instantiated on, e.g., the Corda backend. This class takes three parameters in its constructor:

  1. A function for adding the contract declaration to the ledger: addDecl : (String, String) -> String. The parameters taken by this function match those taken by DeonAPIClient::addDeclaration.
  2. A function that sends the instantiation request of a given template in the declaration to the ledger: instantiate : (String, String, List<InstantiationArgument>, QualifiedName, List<String>) -> T. The parameters for this function match those of the synchronous DeonAPIClient::addContract and the asynchronous DeonAPIClient::submitContract. The return type T represents a contract id – a String or a Future<String>, respectively.
  3. An optional name to give the declaration – if left out the name of the CSL contract is used: declarationName : String = "sic1".

The first time a contract is instantiated, the CSL source is added to the ledger as a declaration using the supplied function, and all subsequent instantiation requests will be instantiations of the resulting declaration id.

val apiClient = DeonAPIClient(API_URL)
// Let's create an instantiator with the synchronous API calls:
val inst = InstantiateImpl(apiClient::addDeclaration, apiClient::addContract)
// Adds the CSL declaration to the ledger and instantiates 'Shop'
val contractId1 = inst.Shop(42)
// Reuses the declaration and instantiates a new contract from 'Shop'
val contractId2 = inst.Shop(11)

Event application

Every subtype of Event in the CSL contract is mapped to a function in the target language that applies an event of that type to a running contract. Any fields that the event record might have is represented as a parameter to the event application function.

There is an interface that contains the necessary functions for applying events:

interface ContractService<T> {
  fun event() : T
  fun addCustomer(id: Int, customer: Customer) : T
}

It contains a function for applying a generic Event and a function for applying our custom AddCustomer event. Because the AddCustomer event record has two additional fields, id : Int and customer : Customer, the resulting Kotlin function accepts the corresponding parameters. The return type is parameterized to allow it to be used with both the synchronous and the asynchronous interface, like it was the case for contract instantiation from Kotlin.

To add an event to a running contract, create an instance of the ContractServiceImpl and supply the following parameters:

  1. The agent from which the event originates.
  2. A function for sending the actual event application request to the API, addEvent : (com.deondigital.api.Event) -> T. Unlike with Instantiate and ReportService, the type of this function does not directly match the functions DeonAPIClient::addEvent and DeonApiClient::submitEvent. These two functions both accept a contact id and the actual event, so you will need to supply a closure that binds the contract id.
  3. A “time provider”, timeProvider : () -> Instant. This is a function that, when called, returns a timestamp that is used as the timestamp for the event.

The example snippet below creates a ContractServiceImpl that uses the synchronous interface of DeonAPIClient to apply events, and which uses the current time as the event time stamp ({ Instant.now() }):

// 'agent', 'contractId', and 'apiClient' defined elsewhere
val c = ContractServiceImpl(agent,
                            { event -> apiClient.addEvent(contractId, event) },
                            { Instant.now() });
// Now we can apply two 'AddCustomer' events on the contract:
c.addCustomer(0, Customer("bob",
                          42,
                          Address("Main st.", 1, 5),
                          CustomerType.OneTime));
c.addCustomer(1, Customer("alice",
                          30,
                          Address("Main st.", 10, 2),
                          CustomerType.Regular(1)));

The sic-maven-plugin

There exists a Maven plugin which is intended to make it simple to integrate the boilerplate generation into a build process. Is is located in Deon Digital’s Nexus repository and you need to let Maven know that it can look after plugins there by adding the following to the <profile> section in settings.xml:

<pluginRepositories>
    <pluginRepository>
        <id>deon</id>
        <url>https://nexus.deondigital.com/repository/deon/</url>
    </pluginRepository>
</pluginRepositories>

Then, to use it in a Maven build you must add the following to the <plugins> section of your pom.xml:

<plugin>
    <groupId>com.deondigital</groupId>
    <artifactId>sic-maven-plugin</artifactId>
    <!-- This is the version of the plugin itself,
         not necessarily of the 'sic' executable -->
    <version>v0.23.0-SNAPSHOT</version>
    <executions>
        <execution>
            <goals><goal>sic</goal></goals>
            <configuration>
                <!-- This is the arguments that'll get passed to 'sic' -->
                <argumentString>--write ../*.csl</argumentString>
                <!-- This is the version of 'sic' that will be
                     fetched from Deon Digital's file server. -->
                <version>v0.22.0</version>
                <!-- If you have a 'sic' installed on your system
                     and in the PATH and want to use that, set
                     this to true -->
                <!-- <useSystemSic>true</useSystemSic> -->
            </configuration>
        </execution>
    </executions>
</plugin>

The plugin will take care of downloading the appropriate version of sic for the platform you’re running on.

Using TypeScript as the target language

Had we run sic with the flag --target TypeScript instead we would have gotten the equivalent data definitions in TypeScript:

$ sic --write --target TypeScript sic1.csl
Generating interface for sic1.csl
Wrote file generated/./Sic1.ts
Wrote file generated/./sic1.csl.ts
Wrote file generated/./preamble.ts

We get three files: Sic1.ts contains the data definitions and interfaces for our CSL contract, sic1.csl.ts contains the CSL source, and preamble.ts contains some shared internal functionality.

Data types

Amongst the definitions in the file Sic1.ts are the mappings of the CSL types:

export type CustomerType
  = { discr : "Regular", field0 : number }
  | { discr : "OneTime" }
export type Address = {
  street : string
  _number : number
  floor : number
}
export type Customer = {
  name : string
  age : number
  address : Address
  customerType : CustomerType
}

These definitions allow us to work in TypeScript directly with, e.g., a Customer object with the field name as a native string. Note that, because “number” is a reserved word in TypeScript, the field number in the CSL contract has been renamed to _number in the TypeScript embedding.

Reports

If we inspect the generated TypeScript code, we will find the interface:

export interface Reports {
  sum(ints : number[]) : Promise<number>
  sumCustomerAge(customers : Customer[]) : Promise<number>
}

Like in Kotlin, this is an interface with a function for each CSL report. Also like in Kotlin, there is an implementation of this interface that takes two parameters:

  1. An implementation of the ContractsAPI (from @deondigital/api-client), used for issuing the actual call to the API.
  2. A contract id.

We can call a report with native TypeScript types analogously to the way we did it for Kotlin:

// 'deonApiClient' and 'contractId' defined elsewhere
const r = new Reports(deonApiClient.contracts, contractId)
const s = await r.sumCustomerAge([{
    name: "bob",
    age: 42,
    address: {
        street: "Main st.",
        _number: 1,
        floor: 5
      },
    customerType: { discr: "OneTime" }
  }, {
    name: "alice",
    age: 30,
    address: {
      street: "Main st.",
      _number: 10,
      floor: 5
    },
    customerType: { discr: "Regular", field0: 1 }
  }])

Contract instantiation

The generated TypeScript code for sic1.csl contains the following interface for contract instantiation:

export interface Instantiate {
  Shop(total : number, peers? : string[]) :
  Promise<{contractId: string,
           commands: (agent: p.Agent) => Commands, reports: Reports}>
}

Like for calling reports, the structure in the TypeScript is similar to the structure in the Kotlin code: there is an interface with one function per template and each function takes as parameters the CSL template expression parameters plus an additional peers parameter. The return type is an asynchronous Promise with a structure containing the id of the newly instantiated contract, and pre-instantiated objects for applying events and calling reports on it. The implementation of this interface takes three parameters in its constructor:

  1. An instance of a ContractsAPI (from @deondigital/api-client) to do the instantiation call.
  2. An instance of a DeclarationsAPI (from @deondigital/api-client) to put the CSL declaration on the ledger.
  3. An optional name to give the declaration. If it is left unspecified, the declaration will be named after the CSL file – "sic1" in our example.

As in the Kotlin implementation the declaration will be put onto the ledger the first time a contract is instantiated.

// 'deonApiClient' defined elsewhere
// Create an instantiator
const instantiate = new Instantiate(deonApiClient.contracts,
                                    deonApiClient.declarations);
// Adds the CSL declaration to the ledger and instantiates 'Shop'
const c1 = await instantiate.Shop(42);
// c1.contractId contains the new contract's id
// Reuses the declaration and instantiates a new contract from 'Shop'
const c2 = await instantiate.Shop(11);

Event application

sic has generated the following TypeScript interface for us:

export interface Commands {
  event() : Promise<p.Tag | void>
  addCustomer(id : number, customer : Customer) : Promise<p.Tag | void>
}

Again, the pattern is quite like it was for Kotlin: one function per event type and the functions take as parameters the fields of the event.

To instantiate Commands, supply the following parameters to the constructor:

  1. A ContractsApi instance from @deondigital/api-client to communicate with the API.
  2. The id of the contract on which to apply the events.
  3. The originating agent for the event.
  4. An optional “time provider” that returns the timestamp to be used in the event. It can be left out, in which case the current time is used.

In this snippet we create a Commands instance and apply two AddCustomer events on a contract:

// 'deonApiClient', 'contractId', and 'agent' defined elsewhere
const c = new Commands(deonApiClient.contracts,
                       contractId,
                       agent);
// Now we can apply two 'AddCustomer' events on the contract:
await c.addCustomer(0, {
   name: "bob",
   age: 42,
   address: {
       street: "Main st.",
       _number: 1,
       floor: 5
     },
   customerType: { discr: "OneTime" }
});
await c.addCustomer(1, {
   name: "alice",
   age: 30,
   address: {
     street: "Main st.",
     _number: 10,
     floor: 5
   },
   customerType: { discr: "Regular", field0: 1 }
});