Interfaces
An interface is a shared contract type. It declares a view and a set of choices, and any template can implement it. Code written against the interface works with every template that implements it, including templates written later or by another team.
Interfaces are how applications interoperate on Canton Network. The Token Standard
(CIP-0056) defines interfaces such as Holding and TransferInstruction. A token that
implements them works with any wallet that uses them.
The examples on this page are in main/daml/Holding.vy and
test/daml/HoldingTest.daml, and the test passes.
Declaring an interface
Section titled “Declaring an interface”from datetime import date
from forge.builtins.daml_lang import ( create, exercise, fetch, from_interface_contract_id, to_interface_contract_id,)from forge.models import ContractId, Decimal, Party, choice, template, view
struct HoldingView: owner: Party amount: Decimal description: str
interface Holding(view=HoldingView): @choice(controller=view.owner, consuming=True) def Holding_Transfer(self, new_owner: Party) -> ContractId[Holding]: passview=HoldingViewis the interface’s view type: a struct that every implementation must be able to produce. Clients read contracts through it without knowing which template they are.- Interface choices are declared with
@choiceand apassbody. The implementing templates provide the bodies. controller=view.ownerrefers to the view of the contract the choice is exercised on. A controller can also be a choice argument.
interface Holding where viewtype HoldingView
holding_TransferImpl : ContractId Holding -> Party -> Update (ContractId Holding)
choice Holding_Transfer : ContractId Holding with new_owner : Party controller (view this).owner do holding_TransferImpl this self new_ownerThe choice is declared once, on the interface, and calls a method that each template implements.
Implementing an interface
Section titled “Implementing an interface”A template implements an interface by listing it as a base class, the same way a Python class subclasses an abstract base class:
@templateclass Cash(Holding): bank: Party owner: Party amount: Decimal
signatory: Party = bank observer: Party = owner
@view def holding_view(self) -> HoldingView: return HoldingView(owner=self.owner, amount=self.amount, description="cash")
@choice(controller=view.owner, consuming=True) def Holding_Transfer(self, new_owner: Party) -> ContractId[Holding]: moved = create(Cash(bank=self.bank, owner=new_owner, amount=self.amount)) return to_interface_contract_id(moved)@viewmarks the method that builds the view. It’s matched to the interface by its return type, so a template that implements two interfaces has two@viewmethods.- A method with the same name as an interface choice is that choice’s implementation. Every other choice stays a normal template choice.
to_interface_contract_idturns the newContractId[Cash]into aContractId[Holding], the type the interface choice returns.
The template must implement every interface choice and have exactly one matching
@view (DF1228), and the base must be an interface (DF1227).
A second template implements the same interface with different fields:
@templateclass Bond(Holding): issuer: Party owner: Party face_value: Decimal maturity: date
signatory: Party = issuer observer: Party = owner
@view def holding_view(self) -> HoldingView: return HoldingView(owner=self.owner, amount=self.face_value, description="bond")Its Holding_Transfer creates a Bond the same way Cash creates a Cash.
Using an interface
Section titled “Using an interface”Code that takes ContractId[Holding] works with cash, bonds, and any future
holding:
@templateclass Wallet: owner: Party
signatory: Party = owner
@choice(controller=owner, consuming=False) def SendAll( self, holdings: list[ContractId[Holding]], receiver: Party ) -> list[ContractId[Holding]]: sent: list[ContractId[Holding]] = [] for holding in holdings: sent.append(exercise(holding, holding.Holding_Transfer(new_owner=receiver))) return sentexercise runs the interface choice, and Daml dispatches to the implementation
of whichever template the contract is.
To go back from an interface to a template, convert the contract ID with
from_interface_contract_id. Annotate the result so Daml knows which template
you expect:
@choice(controller=owner, consuming=False) def CashBank(self, holding: ContractId[Holding]) -> Party: cash_id: ContractId[Cash] = from_interface_contract_id(holding) cash = fetch(cash_id) return cash.bankfetch fails if the contract isn’t a Cash, so CashBank fails for a bond.
.vy |
Daml | Converts |
|---|---|---|
to_interface_contract_id(cid) |
toInterfaceContractId |
ContractId[Template] to ContractId[Interface] |
from_interface_contract_id(cid) |
fromInterfaceContractId |
ContractId[Interface] to ContractId[Template] |
to_interface(value) |
toInterface |
a template value to an interface value |
All three come from forge.builtins.daml_lang.
Testing interfaces
Section titled “Testing interfaces”Daml Script converts IDs with the same functions, and reads a contract’s view with
queryInterfaceContractId:
wallet <- submit alice do createCmd Wallet with owner = alice [cash_holding, bond_holding] <- submit alice do exerciseCmd wallet SendAll with holdings = [toInterfaceContractId cash, toInterfaceContractId bond] receiver = bob
Some cash_view <- queryInterfaceContractId bob cash_holding assert (cash_view.owner == bob && cash_view.description == "cash") Some bond_view <- queryInterfaceContractId bob bond_holding assert (bond_view.amount == 1000.0)
submitMustFail alice do exerciseCmd cash_holding Holding_Transfer with new_owner = aliceThe last step fails because Alice no longer owns the cash: the controller is the view’s owner, now Bob.
daml/HoldingTest.daml:holding_test: ok, 4 active contracts, 8 transactions.Interfaces and templates in one package
Section titled “Interfaces and templates in one package”damlc warns when an interface and its implementations are in the same package,
because it limits how the package can be upgraded, and the warning fails the
build. For a single package like this one, add the flag to daml.yaml:
build-options: - -Wno-upgrade-interfacesFor production code, put the interfaces in their own package so implementations can be upgraded independently. Building and Packaging covers splitting a project into packages.
Next steps
Section titled “Next steps”Building and Packaging explains how DamlForge projects are built into DARs and how packages depend on each other.