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:
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 ofIAsset
are represented on the Ledger API. This declaration means that the specialview
method, when applied to a value of this interface, will return the specified type (in this caseVAsset
). This is the definition ofVAsset
:data VAsset = VAsset with issuer : Party owner : Party description : Text deriving (Eq, Ord, Show)
Hint
See Serializable Types for more information on serializability requirements.
You have defined the methods
setOwner
andtoTransferProposal
as part of theIAsset
interface, and methodasset
as part of theIAssetTransferProposal
interface. Later, when you provide instances of these interfaces, you will see that it is mandatory to implement each of these methods.You have defined the choice
ProposeIAssetTransfer
as part of theIAsset
interface, and the choicesAcceptIAssetTransferProposal
,RejectIAssetTransferProposal
andWithdrawIAssetTransferProposal
as part of theIAssetTransferProposal
interface. These correspond one-to-one with the choices ofCash
/CashTransferProposal
andNFT
/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 ofchoice ProposeIAssetTransfer
is(view this).owner
, that is, it’s theowner
field of theview
for the implicit current contractthis
, in other words, the owner of the current contract. The body of this choice iscreate (toTransferProposal this newOwner)
, so it creates a new contract whose contents are the result of applying thetoTransferProposal
method to the current contract and thenewOwner
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 Asset
s, 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 Script
s you
might have written before: it uses do
notation as usual, including
submit
blocks constructed from Command
s 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''