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 ofIAssetare represented on the Ledger API. This declaration means that the specialviewmethod, 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
setOwnerandtoTransferProposalas part of theIAssetinterface, and methodassetas part of theIAssetTransferProposalinterface. 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
ProposeIAssetTransferas part of theIAssetinterface, and the choicesAcceptIAssetTransferProposal,RejectIAssetTransferProposalandWithdrawIAssetTransferProposalas part of theIAssetTransferProposalinterface. These correspond one-to-one with the choices ofCash/CashTransferProposalandNFT/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
viewmethod. For example, the controller ofchoice ProposeIAssetTransferis(view this).owner, that is, it’s theownerfield of theviewfor 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 thetoTransferProposalmethod to the current contract and thenewOwnerfield 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''