Daml Interfaces

After defining a few templates in Daml, you’ve probably found yourself repeating some behaviors between them. For instance, many templates have a notion of ownership where a party is designated as the “owner” of the contract, and this party has the power to transfer ownership of the contract to a different party (subject to that party agreeing to the transfer!). Daml Interfaces provide a way to abstract those behaviors into a Daml type.

Hint

Remember that you can load all the code for this section into a folder called intro13 by running daml new intro13 --template daml-intro-13

Context

First, define some templates:

template Cash
  with
    issuer : Party
    owner : Party
    currency : Text
    amount : Decimal
  where
    signatory issuer, owner
    ensure amount > 0.0

    choice ProposeCashTransfer : ContractId CashTransferProposal
      with newOwner : Party
      controller owner
      do
        create CashTransferProposal with
          cash = this
          newOwner = newOwner
template CashTransferProposal
  with
    cash : Cash
    newOwner : Party
  where
    signatory (signatory cash)
    observer newOwner

    choice AcceptCashTransferProposal : ContractId Cash
      controller newOwner
      do
        create cash with
          owner = newOwner

    -- Note that RejectCashTransferProposal and WithdrawCashTransferProposal are
    -- almost identical except for the controller - the "recipient" (the new
    -- owner) can reject the proposal, while the "sender" (the old owner) can
    -- withdraw the proposal if the recipient hasn't accepted it already. The
    -- effect in either case is the same: the CashTransferProposal contract is
    -- archived and a new Cash contract is created with the same contents as the
    -- original, but with a new ContractId on the ledger.
    choice RejectCashTransferProposal : ContractId Cash
      controller newOwner
      do
        create cash

    choice WithdrawCashTransferProposal : ContractId Cash
      controller cash.owner
      do
        create cash

These declarations from intro13/daml/Cash.daml define Cash as a simple template with an issuer, an owner, a currency, and an amount. A Cash contract grants its owner the choice ProposeCashTransfer, which allows the owner to propose another party, the newOwner, to take over ownership of the asset.

This is mediated by the CashTransferProposal template, which grants two choices to the new owner: AcceptCashTransferProposal and RejectCashTransferProposal, each of which archives the CashTransferProposal and creates a new Cash contract; in the former case the owner of the new Cash will be newOwner, in the latter, it will be the existing owner. Finally, the existing owner also has the choice WithdrawCashTransferProposal, which archives the proposal and creates a new Cash contract with identical contents to the original one.

Overall, the effect is that a Cash contract can be transferred to another party, if they agree, in two steps.

The declarations from intro13/daml/NFT.daml declare the templates NFT and NFTTransferProposal following the same pattern, with names changed where appropriate, with the main difference being that an NFT has a url : Text field whereas Cash has currency : Text and amount : Decimal.

Interface Definition

To abstract this behavior, you will next introduce two interfaces: IAsset and IAssetTransferProposal.

Hint

It is not mandatory to prefix interface names with the letter I, but it can be convenient to tell at a glance whether or not a type is an interface.

interface IAsset
  where
    viewtype VAsset

    setOwner : Party -> IAsset
    toTransferProposal : Party -> IAssetTransferProposal

    choice ProposeIAssetTransfer : ContractId IAssetTransferProposal
      with newOwner : Party
      controller (view this).owner
      do
        create (toTransferProposal this newOwner)
interface IAssetTransferProposal
  where
    viewtype VAssetTransferProposal

    asset : IAsset

    choice AcceptIAssetTransferProposal : ContractId IAsset
      controller (view this).newOwner
      do
        create $ setOwner (asset this) (view this).newOwner

    choice RejectIAssetTransferProposal : ContractId IAsset
      controller (view this).newOwner
      do
        create (asset this)

    choice WithdrawIAssetTransferProposal : ContractId IAsset
      controller (view (asset this)).owner
      do
        create (asset this)

There are a few things happening here:

  1. For each interface, you have defined a viewtype. This is mandatory for all interfaces. All viewtypes must be serializable records. The viewtype abstracts the read side by providing a uniform way in which implementations of IAsset are represented on the Ledger API. This declaration means that the special view method, when applied to a value of this interface, will return the specified type (in this case VAsset). This is the definition of VAsset:

    data VAsset = VAsset with
        issuer : Party
        owner : Party
        description : Text
      deriving (Eq, Ord, Show)
    

    Hint

    See Serializable Types for more information on serializability requirements.

  2. You have defined the methods setOwner and toTransferProposal as part of the IAsset interface, and method asset as part of the IAssetTransferProposal interface. Later, when you provide instances of these interfaces, you will see that it is mandatory to implement each of these methods.

  3. You have defined the choice ProposeIAssetTransfer as part of the IAsset interface, and the choices AcceptIAssetTransferProposal, RejectIAssetTransferProposal and WithdrawIAssetTransferProposal as part of the IAssetTransferProposal interface. These correspond one-to-one with the choices of Cash / CashTransferProposal and NFT / NFTTransferProposal.

    Notice that the choice controller and the choice body are defined in terms of the methods that you bundled with the interfaces, including the special view method. For example, the controller of choice ProposeIAssetTransfer is (view this).owner, that is, it’s the owner field of the view for the implicit current contract this, in other words, the owner of the current contract. The body of this choice is create (toTransferProposal this newOwner), so it creates a new contract whose contents are the result of applying the toTransferProposal method to the current contract and the newOwner field of the choice argument.

Hint

For a detailed explanation of the syntax used here, check out Reference: Interfaces

Interface Instances

On its own, an interface isn’t very useful, since all contracts on the ledger must belong to some template type. In order to make the link between an interface and a template, you must define an interface instance inside the body of either the template or the interface. In this example, add: interface instance IAsset for Cash and interface instance IAssetTransferProposal for CashTransferProposal:

    interface instance IAsset for Cash where
      view = VAsset with
        issuer
        owner
        description = show @Cash this

      setOwner newOwner =
        toInterface @IAsset $
          this with
            owner = newOwner

      toTransferProposal newOwner =
        toInterface @IAssetTransferProposal $
          CashTransferProposal with
            cash = this
            newOwner
    interface instance IAssetTransferProposal for CashTransferProposal where
      view = VAssetTransferProposal with
        assetView = view (toInterface @IAsset cash)
        newOwner

      asset = toInterface @IAsset cash

The corresponding interface instances for NFT and NFTTransferProposal are very similar so we omit them here.

Inside the interface instances, you must implement every method defined for the corresponding interface, including the special view method. Within each method implementation the variable this is in scope, corresponding to the implict current contract, which will have the type of the template (in this case Cash / CashTransferProposal), as well as each of the fields of the template type. For example, the view definition in interface instance IAsset for Cash mentions issuer and owner, which refer to the issuer and owner of the current Cash contract, as well as this, which refers to the entire Cash contract payload.

The implementations given for each method must match the types given in the interface definition. Notice that the view definition discussed above returns a VAsset, corresponding to IAsset’s viewtype. Similarly, setOwner returns an IAsset, and toTransferProposal returns an IAssetTransferProposal. In these last two, the function toInterface converts values from a template type into an interface type. In setOwner, toInterface is applied to a Cash value (this with owner = newOwner), producing an IAsset value; in toTransferProposal, it is applied to a CashTransferProposal value (CashTransferProposal with {...}), producing an IAssetTransferProposal value.

Using an Interface

Now that you have some interfaces and templates with instances for them, you can reduce duplication in the code for different templates by instead going through the common interface.

For instance, both Cash and NFT are Assets, which means that contracts of either template have an owner who can propose to transfer the contract to a third party. Thus, you can use Daml Script (see Test Templates Using Daml Script) to test that the same contract can be created by Alice and successively transferred to Bob and then Charlie, who then proposes to transfer to Dominic, who rejects the proposal, and finally to Emily before withdrawing the proposal, so in the end the contract remains in Charlie’s ownership. This procedure is tested on the Cash and NFT templates by the Daml Script tests cashTest and nftTest, respectively, both defined in intro13/daml/Main.daml.

But that’s a lot of duplication! cashTest and nftTest only differ in the line that creates the original asset and in the names of the choices used. With the new interfaces IAsset and IAssetTransferProposal, you can write the body of this test a single time, with the name mkAssetTest,

mkAssetTest assetTxt Parties {..} mkAsset = do

You now have not the test itself, but rather a recipe for making the test given some inputs - in this case, assetTxt (a label used for debugging), Parties {..} (a structure containing the Party values for Alice and friends) and finally mkAsset (a function that returns a contract value of type t when given two Party arguments - the constraint Implements t IAsset means that t must be some template with an interface instance for IAsset).

Before looking at the body of mkAssetTest, notice how you use it to define the new tests cashAssetTest and nftAssetTest; these are almost identical except for the label and function given in each case to mkAssetTest. In effect, you have abstracted those away, so you don’t need to include those details in the body of mkAssetTest:

cashAssetTest : Script (ContractId IAsset)
cashAssetTest = do
  parties <- allocateParties
  mkAssetTest "Cash" parties mkCash
mkCash : Party -> Party -> Cash
mkCash issuer owner = Cash with
  issuer
  owner
  currency = "USD"
  amount = 42.0
nftAssetTest : Script (ContractId IAsset)
nftAssetTest = do
  parties <- allocateParties
  mkAssetTest "NFT" parties mkNft
mkNft : Party -> Party -> NFT
mkNft issuer owner = NFT with
  issuer
  owner
  url = "https://nyan.feline/"

In turn, mkAssetTest isn’t very different from other Daml Scripts you might have written before: it uses do notation as usual, including submit blocks constructed from Commands that define the ordered transactions that take place in the test. The main difference is that when querying values of interface types you cannot use the functions query and queryContractId; instead you must use queryInterface (for obtaining the set of visible active contracts of a given interface type) and queryInterfaceContractId (for obtaining a single contract given its ContractId). Importantly, these functions return the view of the contract corresponding to the used interface, rather than the contract record itself. This is because the ledger might contain contracts of template types that you don’t know about but that do implement our interface, so the view is the only sensible thing that can be returned by the ledger.

Also note that immediately after creating the asset with createCmd, you convert the resulting ContractId t into a ContractId IAsset using toInterfaceContractId, which allows you to exercise IAsset choices on it.

mkAssetTest : forall t.
  (Template t, Implements t IAsset, HasAgreement t) =>
  Text -> Parties -> (Party -> Party -> t) -> Script (ContractId IAsset)
mkAssetTest assetTxt Parties {..} mkAsset = do
  aliceAsset <-
    alice `submit` do
      toInterfaceContractId @IAsset <$>
        createCmd (mkAsset alice alice)

  aliceAssetView <-
    queryInterfaceContractId @IAsset alice aliceAsset

  debugRaw $ unlines
    [ "Alice's Asset (" <> assetTxt <> "):"
    , "\tContractId: " <> show aliceAsset
    , "\tValue: " <> show aliceAssetView
    ]

  bobAssetTransferProposal <-
    alice `submit` do
      exerciseCmd aliceAsset ProposeIAssetTransfer with
        newOwner = bob

  bobAsset <-
    bob `submit` do
      exerciseCmd bobAssetTransferProposal AcceptIAssetTransferProposal

  charlieAssetTransferProposal <-
    bob `submit` do
      exerciseCmd bobAsset ProposeIAssetTransfer with
        newOwner = charlie

  charlieAsset <-
    charlie `submit` do
      exerciseCmd charlieAssetTransferProposal AcceptIAssetTransferProposal

  dominicAssetTransferProposal <-
    charlie `submit` do
      exerciseCmd charlieAsset ProposeIAssetTransfer with
        newOwner = dominic

  charlieAsset' <-
    dominic `submit` do
      exerciseCmd dominicAssetTransferProposal RejectIAssetTransferProposal

  emilyAssetTransferProposal <-
    charlie `submit` do
      exerciseCmd charlieAsset' ProposeIAssetTransfer with
        newOwner = emily

  charlieAsset'' <-
    charlie `submit` do
      exerciseCmd emilyAssetTransferProposal WithdrawIAssetTransferProposal

  charlieAssetView <-
    queryInterfaceContractId @IAsset charlie charlieAsset''

  debugRaw $ unlines
    [ "Charlie's Asset (" <> assetTxt <> "):"
    , "\tContractId: " <> show charlieAsset''
    , "\tView: " <> show charlieAssetView
    ]

  charlieAssetView ===
    Some (view (toInterface @IAsset (mkAsset alice charlie)))

  pure charlieAsset''