How to migrate contracts between complex Daml models, using generated Daml code

Motto: if you cannot avoid using boilerplate code, it’s better to generate it than writing it.

Inspired by @RyanMedlen, @Gary_Verhaegen and Rust macros used for implementing traits.

The challenge: I want to migrate Daml contracts from one Daml model into an upgraded Daml model. The double dot syntax for copying the unchanged fields only works if we don’t have custom types in the contract payload to be migrated, which is unlikely for a real-life business use case. We can reduce the amount of boilerplate by keeping the old model as a dependency, but this is suboptimal. See more details in this earlier forum question.

It turns out though that the boilerplate code we need is pretty boring - which is good because it can be generated.

Let’s see a little bit more fancy version of the Asset template from the skeleton project template:

data AssetCode = AssetCode with 
    name: Text
    version: Int
  deriving (Eq, Show)

data Redeemable
  = No
  | Yes with until:Date
  deriving(Eq, Show)

type AssetId = ContractId Asset

template Asset
  with
    issuer : Party
    owner  : Party
    assetCode   : AssetCode
    redeemable : Redeemable
  where
    ensure assetCode.name /= ""
    signatory issuer
    observer owner
    choice Give : AssetId
      with
        newOwner : Party
      controller owner
      do create this with
           owner = newOwner

In the upgraded Daml model I’ve added a dummy text field, and I want to write a migrate function which converts an old Asset contract into a new Asset contract.

I need the following type class and instance declarations. The majority of the code can be generated, I only need to add the extra dummy field manually:

module Migrate where
import Package1.Main qualified
import Package2.Main qualified

class HasMigrate old new where
  migrate : old -> new

instance HasMigrate Party Party where
  migrate = identity

instance HasMigrate Date Date where
  migrate = identity

instance HasMigrate Text Text where
  migrate = identity

instance HasMigrate Int Int where
  migrate = identity

instance HasMigrate Package1.Main.AssetCode Package2.Main.AssetCode where
  migrate (Package1.Main.AssetCode field0 field1) = Package2.Main.AssetCode (migrate field0) (migrate field1)

-- Extra field added manually
instance HasMigrate Package1.Main.Asset Package2.Main.Asset where
  migrate (Package1.Main.Asset field0 field1 field2 field3) = Package2.Main.Asset (migrate field0) (migrate field1) (migrate field2) (migrate field3) "Dummy field"

instance HasMigrate Package1.Main.Redeemable Package2.Main.Redeemable where
  migrate (Package1.Main.Yes field0) = Package2.Main.Yes (migrate field0)
  migrate (Package1.Main.No ) = Package2.Main.No 

A few lines of code makes the trick. I used for this Python but it can be actually any programming language:

#! python3
import os.path

module = "Migrate"

filename = f"upgrade/daml/{module}.daml"

old_module = "Package1.Main"
new_module = "Package2.Main"

standard_types = ["Party", "Date", "Text", "Int"]

# [(<Record name>, <# of fields>)]
record_type_shapes = [("AssetCode", 2), ("Asset", 4)]

# [(<Variant name), [<List of variant shapes, see record shape])]
variant_type_shapes = [("Redeemable", [("Yes", 1), ("No", 0)])]

header_lines = [f"module {module} where", f"import {old_module} qualified", f"import {new_module} qualified", "\n"]
header = "\n".join(header_lines)

class_declaration = "class HasMigrate old new where" + "\n  migrate : old -> new" + "\n\n"

def standard_instance(type_):
    return f"instance HasMigrate {type_} {type_} where" + "\n  " + "migrate = identity" + "\n\n"

def instance_header(type_):
    return f"instance HasMigrate {old_module}.{type_} {new_module}.{type_} where"

def migrate_product_type(type_, fields):
    return f"migrate ({old_module}.{type_} {' '.join([f'field{idx}' for idx in range(fields)])}) = {new_module}.{type_} {' '.join([f'(migrate field{idx})' for idx in range(fields)])}"

def record_instance(type_, fields):
    return instance_header(type_) + '\n  ' + migrate_product_type(type_, fields) + '\n\n'

def variant_instance(type_, option_shapes):
    options = [f"{migrate_product_type(type_, fields)}" for type_,fields in option_shapes]
    lines = [instance_header(type_)] + options
    return '\n  '.join(lines) + '\n\n'

def codegen():
    with open(filename, 'w') as f:
        f.write(header)
        f.write(class_declaration)
        for type_ in standard_types:
            f.write(standard_instance(type_))
        for type_,fields in record_type_shapes:
            f.write(record_instance(type_, fields))
        for type_,option_shapes in variant_type_shapes:
            f.write(variant_instance(type_, option_shapes))
    print(f"{filename} created")

def remove_if_exists(path):
    if os.path.exists(path):
        os.remove(path)
        print(f"{path} removed")

if __name__ == "__main__":
    remove_if_exists(filename)
    codegen()

Having all this in place, we only need an upgrade contract for authorization, on which a choice will use the migrate function to convert an old Asset instance into a new asset creation input:

module Main where

import DA.Date
import Daml.Script

import Package1.Main qualified
import Package2.Main qualified

import Migrate

template Upgrade
  with 
    issuer : Party
    owner : Party
  where 
    signatory issuer
    observer owner
    choice Perform : ContractId Package2.Main.Asset
      with 
        cid : ContractId Package1.Main.Asset
      controller owner
      do
        oldAsset <- fetch cid
        let newAsset = migrate oldAsset
        archive cid
        create newAsset

test: Script (ContractId Package2.Main.Asset)
test = do 
  alice <- allocatePartyWithHint "Alice" (PartyIdHint "Alice")
  bob <- allocatePartyWithHint "Bob" (PartyIdHint "Bob")

  assetCid <- submit alice do
    createCmd Package1.Main.Asset with
      issuer = alice
      owner = bob
      assetCode = Package1.Main.AssetCode with
        name = "TV"
        version = 1
      redeemable = Package1.Main.Yes (date 2023 Dec 31)

  upgradeCid <- submit alice do
    createCmd Upgrade with 
      issuer = alice
      owner = bob

  submit bob do 
    exerciseCmd upgradeCid Perform with cid = assetCid

For contracts with multiple signatories, we need a multistep process for the migration, in order to get authorization from all signatories.

Needless to say that for such a small Daml model the Daml codegen doesn’t make a big difference, but for a really complex model it can spare us a lot of typing.

After having tackled the codegen challenge, we need a way to collect all the types from the Daml model, for which we need to declare a HasMigrate instance. I’m contemplating several approaches for this, and I haven’t found a fully automated method yet.

2 Likes

Update: I ended up writing a small extension to the DAML LF API Type Signature library extracting those type metadata which are needed for codegen, e.g. for variants:

{
    "path" : "<path/to/the/dalf.dalf>",
    "kind" : "variant",
    "typeConstructor" : "<type constructor of the variant>",
    "options" : [
      {
        "valueConstructor" : "<value constructor of option #1>",
        "fields" : 0
      },
      {
        "valueConstructor" : "<value constructor of option #2>",
        "fields" : 1
      }
    ],
    "typeParams" : 0
  },