7 Composing choices¶
It’s time to put everything you’ve learnt so far together into a complete and secure Daml model for asset issuance, management, transfer, and trading. This application will have capabilities similar to the one in IOU Quickstart Tutorial. In the process you will learn about a few more concepts:
- Daml projects, packages and modules
- Composition of transactions
- Observers and stakeholders
- Daml’s execution model
- Privacy
The model in this section is not a single Daml file, but a Daml project consisting of several files that depend on each other.
Hint
Remember that you can load all the code for this section into a folder called intro7
by running daml new intro7 --template daml-intro-7
Daml projects¶
Daml is organized in projects, packages and modules. A Daml project is specified using a single daml.yaml
file, and compiles into a package in Daml’s intermediate language, or bytecode equivalent, Daml-LF. Each Daml file within a project becomes a Daml module, which is a bit like a namespace. Each Daml project has a source root specified in the source
parameter in the project’s daml.yaml
file. The package will include all modules specified in *.daml
files beneath that source directory.
You can start a new project with a skeleton structure using daml new project-name
in the terminal. A minimal project would contain just a daml.yaml
file and an empty directory of source files.
Take a look at thedaml.yaml
for the chapter 7 project:
sdk-version: __VERSION__
name: __PROJECT_NAME__
source: daml
version: 1.0.0
dependencies:
- daml-prim
- daml-stdlib
- daml-script
sandbox-options:
- --wall-clock-time
You can generally set name
and version
freely to describe your project. dependencies
does what the name suggests: It includes dependencies. You should always include daml-prim
and daml-stdlib
. The former contains internals of compiler and Daml Runtime, the latter gives access to the Daml Standard Library. daml-script
contains the types and standard library for Daml Script.
You compile a Daml project by running daml build
from the project root directory. This creates a dar
file in .daml/dist/dist/${project_name}-${project_version}.dar
. A dar
file is Daml’s equivalent of a JAR
file in Java: it’s the artifact that gets deployed to a ledger to load the package and its dependencies. dar
files are fully self-contained in that they contain all dependencies of the main package. More on all of this in 9 Working with Dependencies.
Project structure¶
This project contains an asset holding model for transferable, fungible assets and a separate trade workflow. The templates are structured in three modules: Intro.Asset
, Intro.Asset.Role
, and Intro.Asset.Trade
.
In addition, there are tests in modules Test.Intro.Asset
, Test.Intro.Asset.Role
, and Test.Intro.Asset.Trade
.
All but the last .
-separated segment in module names correspond to paths relative to the project source directory, and the last one to a file name. The folder structure therefore looks like this:
.
├── daml
│ ├── Intro
│ │ ├── Asset
│ │ │ ├── Role.daml
│ │ │ └── Trade.daml
│ │ └── Asset.daml
│ └── Test
│ └── Intro
│ ├── Asset
│ │ ├── Role.daml
│ │ └── Trade.daml
│ └── Asset.daml
└── daml.yaml
Each file contains a module header. For example, daml/Intro/Asset/Role.daml
:
module Intro.Asset.Role where
You can import one module into another using the import
keyword. The LibraryModules
module imports all six modules:
import Intro.Asset
Imports always have to appear just below the module declaration. You can optionally add a list of names after the import to import only the selected names:
import DA.List (sortOn, groupOn)
If your module contains any Daml Scripts, you need to import the corresponding functionality:
import Daml.Script
Project overview¶
The project both changes and adds to the Iou
model presented in 6 Parties and authority:
Assets are fungible in the sense that they have
Merge
andSplit
choices that allow theowner
to manage their holdings.Transfer proposals now need the authorities of both
issuer
andnewOwner
to accept. This makesAsset
safer thanIou
from the issuer’s point of view.With the
Iou
model, anissuer
could end up owing cash to anyone as transfers were authorized by justowner
andnewOwner
. In this project, only parties having anAssetHolder
contract can end up owning assets. This allows theissuer
to determine which parties may own their assets.The
Trade
template adds a swap of two assets to the model.
Composed choices and scripts¶
This project showcases how you can put the Update
and Script
actions you learnt about in 6 Parties and authority to good use. For example, the Merge
and Split
choices each perform several actions in their consequences.
- Two create actions in case of
Split
- One create and one archive action in case of
Merge
choice Split
: SplitResult
with
splitQuantity : Decimal
controller owner
do
splitAsset <- create this with
quantity = splitQuantity
remainder <- create this with
quantity = quantity - splitQuantity
return SplitResult with
splitAsset
remainder
choice Merge
: ContractId Asset
with
otherCid : ContractId Asset
controller owner
do
other <- fetch otherCid
assertMsg
"Merge failed: issuer does not match"
(issuer == other.issuer)
assertMsg
"Merge failed: owner does not match"
(owner == other.owner)
assertMsg
"Merge failed: symbol does not match"
(symbol == other.symbol)
archive otherCid
create this with
quantity = quantity + other.quantity
The return
function used in Split
is available in any Action
context. The result of return x
is a no-op containing the value x
. It has an alias pure
, indicating that it’s a pure value, as opposed to a value with side-effects. The return
name makes sense when it’s used as the last statement in a do
block as its argument is indeed the “return”-value of the do
block in that case.
Taking transaction composition a step further, the Trade_Settle
choice on Trade
composes two exercise
actions:
choice Trade_Settle
: (ContractId Asset, ContractId Asset)
with
quoteAssetCid : ContractId Asset
baseApprovalCid : ContractId TransferApproval
controller quoteAsset.owner
do
fetchedBaseAsset <- fetch baseAssetCid
assertMsg
"Base asset mismatch"
(baseAsset == fetchedBaseAsset with
observers = baseAsset.observers)
fetchedQuoteAsset <- fetch quoteAssetCid
assertMsg
"Quote asset mismatch"
(quoteAsset == fetchedQuoteAsset with
observers = quoteAsset.observers)
transferredBaseCid <- exercise
baseApprovalCid TransferApproval_Transfer with
assetCid = baseAssetCid
transferredQuoteCid <- exercise
quoteApprovalCid TransferApproval_Transfer with
assetCid = quoteAssetCid
return (transferredBaseCid, transferredQuoteCid)
The resulting transaction, with its two nested levels of consequences, can be seen in the test_trade
script in Test.Intro.Asset.Trade
:
TX #15 1970-01-01T00:00:00Z (Test.Intro.Asset.Trade:77:23)
#15:0
│ known to (since): 'Alice' (#15), 'Bob' (#15)
└─> 'Bob' exercises Trade_Settle on #13:1 (Intro.Asset.Trade:Trade)
with
quoteAssetCid = #10:1; baseApprovalCid = #14:2
children:
#15:1
│ known to (since): 'Alice' (#15), 'Bob' (#15)
└─> fetch #11:1 (Intro.Asset:Asset)
#15:2
│ known to (since): 'Alice' (#15), 'Bob' (#15)
└─> fetch #10:1 (Intro.Asset:Asset)
#15:3
│ known to (since): 'USD_Bank' (#15), 'Bob' (#15), 'Alice' (#15)
└─> 'Alice',
'Bob' exercises TransferApproval_Transfer on #14:2 (Intro.Asset:TransferApproval)
with
assetCid = #11:1
children:
#15:4
│ known to (since): 'USD_Bank' (#15), 'Bob' (#15), 'Alice' (#15)
└─> fetch #11:1 (Intro.Asset:Asset)
#15:5
│ known to (since): 'Alice' (#15), 'USD_Bank' (#15), 'Bob' (#15)
└─> 'Alice', 'USD_Bank' exercises Archive on #11:1 (Intro.Asset:Asset)
#15:6
│ referenced by #17:0
│ known to (since): 'Bob' (#15), 'USD_Bank' (#15), 'Alice' (#15)
└─> create Intro.Asset:Asset
with
issuer = 'USD_Bank'; owner = 'Bob'; symbol = "USD"; quantity = 100.0; observers = []
#15:7
│ known to (since): 'EUR_Bank' (#15), 'Alice' (#15), 'Bob' (#15)
└─> 'Bob',
'Alice' exercises TransferApproval_Transfer on #12:1 (Intro.Asset:TransferApproval)
with
assetCid = #10:1
children:
#15:8
│ known to (since): 'EUR_Bank' (#15), 'Alice' (#15), 'Bob' (#15)
└─> fetch #10:1 (Intro.Asset:Asset)
#15:9
│ known to (since): 'Bob' (#15), 'EUR_Bank' (#15), 'Alice' (#15)
└─> 'Bob', 'EUR_Bank' exercises Archive on #10:1 (Intro.Asset:Asset)
#15:10
│ referenced by #16:0
│ known to (since): 'Alice' (#15), 'EUR_Bank' (#15), 'Bob' (#15)
└─> create Intro.Asset:Asset
with
issuer = 'EUR_Bank'; owner = 'Alice'; symbol = "EUR"; quantity = 90.0; observers = []
Similar to choices, you can see how the scripts in this project are built up from each other:
test_issuance = do
setupResult@(alice, bob, bank, aha, ahb) <- setupRoles
assetCid <- submit bank do
exerciseCmd aha Issue_Asset
with
symbol = "USD"
quantity = 100.0
Some asset <- queryContractId bank assetCid
assert (asset == Asset with
issuer = bank
owner = alice
symbol = "USD"
quantity = 100.0
observers = []
)
return (setupResult, assetCid)
In the above, the test_issuance
script in Test.Intro.Asset.Role
uses the output of the setupRoles
script in the same module.
The same line shows a new kind of pattern matching. Rather than writing setupResult <- setupRoles
and then accessing the components of setupResult
using _1
, _2
, etc., you can give them names. It’s equivalent to writing
setupResult <- setupRoles
case setupResult of
(alice, bob, bank, aha, ahb) -> ...
Just writing (alice, bob, bank, aha, ahb) <- setupRoles
would also be legal, but setupResult
is used in the return value of test_issuance
so it makes sense to give it a name, too. The notation with @
allows you to give both the whole value as well as its constituents names in one go.
Daml’s execution model¶
Daml’s execution model is fairly easy to understand, but has some important consequences. You can imagine the life of a transaction as follows:
- Command Submission
- A user submits a list of Commands via the Ledger API of a Participant Node, acting as a Party hosted on that Node. That party is called the requester.
- Interpretation
- Each Command corresponds to one or more Actions. During this step, the
Update
corresponding to each Action is evaluated in the context of the ledger to calculate all consequences, including transitive ones (consequences of consequences, etc.). The result of this is a complete Transaction. Together with its requestor, this is also known as a Commit. - Blinding
- On ledgers with strong privacy, projections (see Privacy) for all involved parties are created. This is also called projecting.
- Transaction Submission
- The Transaction/Commit is submitted to the network.
- Validation
- The Transaction/Commit is validated by the network. Who exactly validates can differ from implementation to implementation. Validation also involves scheduling and collision detection, ensuring that the transaction has a well-defined place in the (partial) ordering of Commits, and no double spends occur.
- Commitment
- The Commit is actually committed according to the commit or consensus protocol of the Ledger.
- Confirmation
- The network sends confirmations of the commitment back to all involved Participant Nodes.
- Completion
- The user gets back a confirmation through the Ledger API of the submitting Participant Node.
The first important consequence of the above is that all transactions are committed atomically. Either a transaction is committed as a whole and for all participants, or it fails.
That’s important in the context of the Trade_Settle
choice shown above. The choice transfers a baseAsset
one way and a quoteAsset
the other way. Thanks to transaction atomicity, there is no chance that either party is left out of pocket.
The second consequence is that the requester of a transaction knows all consequences of their submitted transaction – there are no surprises in Daml. However, it also means that the requester must have all the information to interpret the transaction. We also refer to this as Principle 2 a bit later on this page.
That’s also important in the context of Trade
. In order to allow Bob to interpret a transaction that transfers Alice’s cash to Bob, Bob needs to know both about Alice’s Asset
contract, as well as about some way for Alice
to accept a transfer – remember, accepting a transfer needs the authority of issuer
in this example.
Observers¶
Observers are Daml’s mechanism to disclose contracts to other parties. They are declared just like signatories, but using the observer
keyword, as shown in the Asset
template:
template Asset
with
issuer : Party
owner : Party
symbol : Text
quantity : Decimal
observers : [Party]
where
signatory issuer, owner
ensure quantity > 0.0
observer observers
The Asset
template also gives the owner
a choice to set the observers, and you can see how Alice uses it to show her Asset
to Bob just before proposing the trade. You can try out what happens if she didn’t do that by removing that transaction.
usdCid <- submit alice do
exerciseCmd usdCid SetObservers with
newObservers = [bob]
Observers have guarantees in Daml. In particular, they are guaranteed to see actions that create and archive the contract on which they are an observer.
Since observers are calculated from the arguments of the contract, they always know about each other. That’s why, rather than adding Bob as an observer on Alice’s AssetHolder
contract, and using that to authorize the transfer in Trade_Settle
, Alice creates a one-time authorization in the form of a TransferAuthorization
. If Alice had lots of counterparties, she would otherwise end up leaking them to each other.
Controllers declared in the choice
syntax are not automatically made observers, as they can only be calculated at the point in time when the choice arguments are known. On the contrary, controllers declared via the controller cs can
syntax are automatically made observers, but this syntax is deprecated and will be removed in a future version of Daml.
Privacy¶
Daml’s privacy model is based on two principles:
Principle 1. Parties see those actions that they have a stake in. Principle 2. Every party that sees an action sees its (transitive) consequences.
Principle 2 is necessary to ensure that every party can independently verify the validity of every transaction they see.
A party has a stake in an action if
- they are a required authorizer of it
- they are a signatory of the contract on which the action is performed
- they are an observer on the contract, and the action creates or archives it
What does that mean for the exercise tradeCid Trade_Settle
action from test_trade
?
Alice is the signatory of tradeCid
and Bob a required authorizer of the Trade_Settled
action, so both of them see it. According to rule 2. above, that means they get to see everything in the transaction.
The consequences contain, next to some fetch
actions, two exercise
actions of the choice TransferApproval_Transfer
.
Each of the two involved TransferApproval
contracts is signed by a different issuer
, which see the action on “their” contract. So the EUR_Bank sees the TransferApproval_Transfer
action for the EUR Asset
and the USD_Bank sees the TransferApproval_Transfer
action for the USD Asset
.
Some Daml ledgers, like the script runner and the Sandbox, work on the principle of “data minimization”, meaning nothing more than the above information is distributed. That is, the “projection” of the overall transaction that gets distributed to EUR_Bank in step 4 of Daml’s execution model would consist only of the TransferApproval_Transfer
and its consequences.
Other implementations, in particular those on public blockchains, may have weaker privacy constraints.
Divulgence¶
Note that Principle 2 of the privacy model means that sometimes parties see contracts that they are not signatories or observers on. If you look at the final ledger state of the test_trade
script, for example, you may notice that both Alice and Bob now see both assets, as indicated by the Xs in their respective columns:
This is because the create
action of these contracts are in the transitive consequences of the Trade_Settle
action both of them have a stake in. This kind of disclosure is often called “divulgence” and needs to be considered when designing Daml models for privacy sensitive applications.
Next up¶
In 8 Exception Handling, we will learn about how errors in your model can be handled in Daml.