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.
A declaration function
Before a contract instance can be instantiated on a backend, the contract declaration must be made available on it. sic generates a convenience function that does this.

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
Wrote file generated/com/deondigital/api/contract/sic1/preamble.kt
Wrote file generated/com/deondigital/api/contract/sic1/builtins.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 find the following class:

open class ReportService(
  private val reportCaller : (
      String,
      String,
      List<com.deondigital.api.Value>
  ) -> com.deondigital.api.Value,
  private val contractId : String
) : /* ... */ {

  fun sum(ints: List<Int>) : Int = /* implementation */
  fun sumCustomerAge(customers: List<Customer>) : Int = /* implementation */
}

The class com.deondigital.api.contract.sic1.ReportService declares two functions, one for each of the CSL reports in sic1.csl. Input and output types of the functions are mapped from the corresponding CSL types; note that the CSL List a type is mapped to Kotlin/Java’s List<T> type. To instantiate the class one must provide two arguments:

  1. A reportCaller which is the function used to issue the actual 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 id of 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 = ReportService(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

The astute reader will have noticed that the class ReportService is an open class and that it itself is a subclass of another class. When sic generates code for multi-file projects the report service classes inherit from one another, reflecting the project structure in the deon-project. For standalone CSL files the report service just inherits from an empty abstract base class.

Adding a contract declaration

In order to instantiate a contract one must first put the declaration onto the ledger. This is done with the function com.deondigital.api.contract.sic1.addDeclaration, which must be supplied with a function that does the actual communication with the ledger:

fun addDeclaration(add : (String, String) -> String) : String = /* ... */

The signature of the parameter add matches that of DeonApiClient::addDeclaration, so under normal circumstances one would just call addDeclaration(DeonApiClient::addDeclaration) to put the declaration on the ledger. The function returns the id that the declaration gets assigned on the ledger.

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 class:

open class Instantiate<T>(
    private val declarationId : String,
    private val instantiate : (
        String,
        String,
        List<com.deondigital.api.Value>,
        com.deondigital.api.QualifiedName,
        List<String>
    ) -> T
) : /* ... */ {
    fun Shop(total: Int, `metaArgs` : MetaArgs? = null) : T =
        /* implementation */
}

Like the code for reports, instantiation is encapsulated in an open class that contains 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 optional parameter with the type MetaArgs that can be used to supply additional information about the contract that will be instantiated, such as which peers should be used in the Corda backend and which name will be given to the contract instance. This class takes two parameters in its constructor:

  1. The id of the CSL declaration on the ledger, usually the output of addDeclaration.
  2. A function that sends the instantiation request of a given template in the declaration to the ledger: instantiate. The parameters for this function match those of DeonAPIClient::addContract.
val apiClient = DeonAPIClient(API_URL)
// Add the CSL declaration to the ledger
val declarationId = addDeclaration(apiClient::addDeclaration)
val inst = Instantiate(declarationId, apiClient::addContract)
// Instantiate the template 'Shop'
val contractId1 = inst.Shop(42)
// Instantiate a new contract from the template 'Shop'
val contractId2 = inst.Shop(11)
// Instantiate yet another contract from the 'Shop'
// template, but give the instance a custom name
val namedContract = inst.Shop(47, MetaArgs(name = "shopContract47"))

Event application

Every subtype of Event in the contract gets mapped to a function 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.

The generated Kotlin code will contain a class definition similar to this:

open class ContractService<T>(
    private val agent : com.deondigital.api.AgentValue,
    private val addEvent : (
        com.deondigital.api.Event,
        com.deondigital.api.Tag?
    ) -> T,
    private val timeProvider : () -> java.time.Instant =
        { java.time.Instant.now() }
) : /* ... */ {
  fun AddCustomer(id: Int, customer: Customer) : T = /* implementation */
}

The function AddCustomer() applies an AddCustomer event, and the basic Event can be applied using a similar function in the super class. Because the AddCustomer event record contains two fields in addition to the fields in Event, id : Int and customer : Customer, the Kotlin function accepts two parameters corresponding to the fields. The return type is parameterized like it was the case for contract instantiation from Kotlin.

To add an event to a running contract, instantiate ContractService 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. Usually you would supply a lambda that calls DeonAPIClient::addEvent with the desired contract id.
  3. An optional “time provider”, timeProvider. This is a function that is used to create the timestamp for the timestamp field on the created event. The default value just uses the current time.

The example snippet below creates a ContractService:

// 'agent', 'contractId', and 'apiClient' defined elsewhere
val c = ContractService(
    agent,
    { event, tag -> apiClient.addEvent(contractId, event, tag) }
);
// 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)));

Targeting R3 Corda

sic provides an additional Kotlin target named Corda. Using this target, sic will, in addition to creating the usual Kotlin files, also create a contractOps.kt file. This file contains the class ContractOps that provides an interface to contracts on the R3 Corda Distributed Ledger (https://www.corda.net/). To construct an instance of the class, a CordaRPCOps and a ContractHandler is needed. The former is obtained from creating a connection to a Corda Node; and the latter is available from the csl-cordapp package:

<dependency>
  <groupId>com.deondigital</groupId>
  <artifactId>csl-cordapp</artifactId>
  <version>v0.41.0</version> <!-- Insert appropriate CSL version here -->
</dependency>

The plugin csl-cordapp provides a default anonymized implementation of the ContractHandler interface, but it is possible to implement a custom implementation.

Using TypeScript as the target language

To instruct sic to generate TypeScript mappings for the CSL contract, run it with the flag --target 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
Wrote file generated/./builtins.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 builtins.ts and preamble.ts contains definitions that are not tied to the particular contract.

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 class 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

The generated TypeScript code contains the classes:

// In builtins.ts
export abstract class Reports {
  constructor(
      protected readonly client : p.ContractsApi,
      protected readonly contractId : string) {}
}
// In sic1.ts
export class Reports extends builtins.Reports {
    sum(ints : number[]) : Promise<number> {
        /* implementation */
    }
    sumCustomerAge(customers : Customer[]) : Promise<number> {
        /* implementation */
    }
}

Through its superclass builtins.Reports, Reports requires two parameters in its constructor:

  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([new Customer (
    "bob",
    42,
    new Address (
        "Main st.",
        1,
        5
      ),
    { discr: "OneTime" }
  ), new Customer (
    "alice",
    30,
    new Address (
      "Main st.",
      10,
      5
    ),
    { discr: "Regular", field0: 1 }
  )])

Adding a contract declaration

Before instantiation a contract we must add the declaration to the ledger backend with the addDeclaration function:

export function addDeclaration(client : p.DeclarationsApi) : Promise<string> {
    /* implementation */
}

Calling this function with an implementation of the DeclarationsAPI from @deondigital/api-client will put the sic1.csl on the ledger and return the corresponding declaration id.

Contract instantiation

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

export class Instantiate extends builtins.Instantiate {
    constructor(contractsClient : p.ContractsApi, declarationId : string) {
        /* implementation */
    }

    async Shop(
        total : number,
        $args? : { peers? : string[], name? : string }) : Promise<string> {
        /* implementation */
    }
}

This is similar to the Kotlin code: sic generates on member function per template and each function takes as parameters the CSL template expression parameters plus an additional $args parameter. The return type is a Promise containing the id of the newly instantiated contract. To instantiate this class we supply two parameters to the constructor:

  1. An instance of a ContractsAPI (from @deondigital/api-client) to do the instantiation call.
  2. A declaration id for a ledger declaration containing the appropriate CSL sources – the output of a call to addDeclaration.
// 'deonApiClient' and 'declarationId' defined elsewhere
// Create an instantiator
const instantiate = new Instantiate(deonApiClient.contracts, declarationId);
// Instantiate 'Shop'
const contractId1 = await instantiate.Shop(42);
// Instantiate a new contract from 'Shop'
const contractId2 = await instantiate.Shop(11);
// Instantiate yet another contract from the 'Shop'
// template, but give the instance a custom name
const namedContract = await instantiate.Shop(47, { name : "shopContract47" });

Event application

sic has generated the following TypeScript class:

// In builtins.ts
export class Commands {
    constructor(
        protected readonly client : p.ContractsApi,
        protected readonly contractId : string,
        protected readonly agent : p.Agent,
        protected readonly timeProvider : () => Date = () => new Date()
    ) { /* implementation */ }

    async Event($tag? : p.Tag) : Promise<p.Tag | void> {
        /* implementation */
    }
}
// In sic1.ts
export class Commands extends builtins.Commands {
    async AddCustomer(
        id : number,
        customer : Customer,
        $tag? : p.Tag) : Promise<p.Tag | void> {
        /* implementation */
    }
}

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. Note that the function for applying the basic event type Event is contained in the super class builtins.Commands.

Supply the following parameters to the constructor to instantiate Commands:

  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, new Customer (
   "bob",
   42,
   new Address ("Main st.", 1, 5),
   { discr: "OneTime" }
));
await c.AddCustomer(1, new Customer (
   "alice",
   30,
   new Address ("Main st.", 10, 5),
   { discr: "Regular", field0: 1 }
));

The @deondigital/sic NPM package

The sic tool is distributed in the NPM package @deondigital/sic. It provides a handy way to install sic:

$ npx @deondigital/sic

This will download the latest version of sic and run it. If you want sic code generation as part of your build process, add @deondigital/sic as a project dependency and add a "generate" entry to the "scripts" section of package.json:

/* ... */
"scripts": {
  /* ... */
  "generate": "sic --target TypeScript --write *.csl"
},
"dependencies": {
  /* ... */
  "@deondigital/sic": "0.37.0"
}

Now you can use the generate script in your project:

$ npm run generate

Note

The version number of the @deondigital/sic package follows that of the rest of Deon Digital CSL. Version 0.37.0 of @deondigital/sic will download a version of sic that is compatible with version 0.37.0 of the Deon Digital CSL runtime.

Emit JSON representation of core AST

It is possible to use sic to emit a structured core representation of a CSL project in JSON format by using the CoreAST target.

$ sic --write --target CoreAST sic1.csl
Generating interface for sic1.csl
No project file found
Wrote file generated/./declarations.json

The file declarations.json contains a JSON representation of the core AST of sic1.csl along with the standard library. Note that this core representation is the result of several steps of internal processing that among other things strips away type information.

Emit JSON representation of contract structure

It is possible to get a JSON-serialized version of the data structure that sic uses for the various emitters. This structure contains information about reports and entrypoints and their associated types, as well as the type definitions from the contract. Information about the project structure, if any, is also included.

By using this information one can, e.g., create a specialized “third-party emitter”. To use, simply run

$ sic --write --target Ontology sic1.csl
Generating interface for sic1.csl
No project file found
Wrote file generated/./sic1.json

This will write a list of contract descriptors to sic1.json with a descriptor for each CSL file in the project – in this case just one – as well as descriptors for the preamble and built-ins. The output is structured roughly as follows:

[
 /* Descriptors for preamble and built-in functions... */
 {
     "tag": "ContractDescriptor",
     /* Available reports with associated in/out types */
     "reports": [ /* ... */ ],

     /* Type declarations in sic1.csl */
     "types": [ /* ... */ ],

     /* The contents of the file sic1.csl */
     "cslSource": "/* ... */" ,

     /* Identifier of the contract descriptor.  This is the
        name referred to in the "dependencies" field. */
     "name": "sic1",

     /* Names of other contract descriptors that this one
        depends on, as specified in a deon-project file. For
        sic1.csl there is no deon-project so the list is empty. */
     "dependencies": [],

     /* List of entry points in the contract with associated parameter types */
     "entryPoints": [ /* ... */ ]
 }
 ]

Projects with multiple CSL files

CSL contracts can be grouped into projects by defining a file called deon-project in a folder. This file contains a newline-separated list of relative or absolute paths to CSL files, its presence folder foo means that the folder is a “project”, and that the CSL files should be loaded in the order specified in the deon-project file. For example, the following deon-project file specifies a project that contains the files sic1.csl, sic2.csl, and sic3.csl:

sic1.csl
subfolder/sic2.csl
/absolute/folder/sic3.csl

Contracts that are part of a deon-project are typechecked in the context of all contracts that come before them in the project specification. Thus, sic1.csl may only refer to names declared in the same file or in the standard library, whereas anything declared in sic1.csl is in scope in sic2.csl, and anything in sic1.csl and sic2.csl is in scope in sic3.csl.

Using projects in sic

Given the project file myproject/deon-project:

sic1.csl
sic2.csl

Running the command:

$ sic --write --target Kotlin --namespace org.foo myproject/*.csl

Will output the following:

Wrote file org/foo/project/deon-project.kt
Wrote file org/foo/project/builtins.kt
Wrote file org/foo/project/preamble.kt
Wrote file org/foo/project/sic1.kt
Wrote file org/foo/project/sic1.csl.kt
Wrote file org/foo/project/sic2.kt
Wrote file org/foo/project/sic2.csl.kt

When using one of the Kotlin targets sic will generate a deon-project.kt file with some convenience aliases, making it easier to refer to the Contract, Instantiation, and Report endpoints associated with the last of the project’s CSL files.