Introduction
Andreas Lochbihler of Digital Asset conducted a guest lecture at Rapperswil last May, and in the lecture he demonstrated a real life example, showing how a bike shop offers a repair service for a bike owner in return of the cash the bike owner is holding. Here you can find his guest lecture:
This is a good guided tutorial for those who are interested to learn Daml and build a more complex contract step by step. This article serves as a supplementary work to his demonstration, providing the code and some explanation on the test result. The code is directly adopted from his demonstration, and rewritten with Daml SDK v1.10.0.
Overview of Bike Shop Example
In the Bike Shop example, three parties are involved.
- SwissBank: issuing cash as contracts
- Martin: the bike owner, who owns cash issued by the Bank
- BikeShop: the bike shop, who provides service to bike owners in exchange of cash
Here is the flow of this demonstration.
- Create record type Cash, which is a named tuple.
- Create template Cash and let SwissBank issue the cash to Martin who owns this cash.
- Make this cash transferable by adding choice on the cash template
- Explore a bilateral service contract, which requires both parties, BikeShop and Martin to agree on a service at a price. Either party cannot apply this contract to the other party without their own consent.
- Build this bilateral contract through the propose-accept model.
- Add a choice to show Martin’s satisfaction on the service and complete payment.
- Add constraints to ensure the cash received meets the requirement of the service contract.
Demonstration Flow
Step 0: Setup environment
In your terminal, use this command to launch the project. (If you have not yet installed the Daml SDK, please refer to the installation instruction here.)
daml new bikeshopdemo --template empty-skeletoncd bikeshopdemodaml studio
Since we demonstrate the flow in Daml Studio directly, we are safe to just use an empty skeleton for our example.
Here is a quick introduction on how to use Daml Studio.
In each step, we provide both Daml Code and Test Script. Simply paste them into the editor panel. If Test Script works well, we will see the Script results shown on top of Test Script. Click the Script results, and we can see the result shown on the right side.
Over the rest of this example we will be editing or copying code for both Daml Code and Test Script. Daml Code and Test Script are provided throughout this example, as well as examples of the result screen. The code and test script in bold face is the newly added or updated part from the previous step.
Step 1: Create a record type Cash
In this step we make observations on a data type Record, which is a named tuple. We will compare this with templates in the next step.
Daml code
module BikeShop whereimport Daml.Scriptdata Currency = USD | EUR | GBP | CHF
deriving (Eq, Show)data Cash = Cash
with
issuer: Party
owner: Party
currency: Currency
amount: Decimal
Test script, to show what happens in the ledger when we assign values to the Cash data record type.
test = do
bank <- allocateParty "SwissBank"
martin <- allocateParty "Martin"let
cash = Cash
with
issuer = bank
owner = martin
currency = CHF
amount = 200.0pure()
Result of test script
We see no active contracts in the ledger. This is because the record Cash here just keeps some data, and not yet a contract created in the ledger. We see how this is used in the next step when we create a contract with this record type.
Step 2: Create a contract for Cash
In this step we start working on contracts in ledger, and see how a contract is created from a template. The template is coded in Daml, and the simulation of contract creation is done in the test script. This example is a Cash contract, an amount of money issued by a bank (a party) and owned by somebody (a party). Note how our Cash definition has changed from data to template so that we can instantiate it and store it on our ledger.
Daml code
module BikeShop whereimport Daml.Scriptdata Currency = USD | EUR | GBP | CHF
deriving (Eq, Show)template Cash
with
issuer: Party
owner: Party
currency: Currency
amount: Decimal
where
signatory issuer
Test script code to show how the signatory creates a Cash contract in the ledger (that is, SwissBank issues cash to Martin).
test = do
bank <- allocateParty "Bank"
martin <- allocateParty "Martin"let
cash = Cash
with
issuer = bank
owner = martin
currency = CHF
amount = 200.0submit bank do
createCmd cashpure()
Result of test script
Now we see a Cash contract is created in the ledger. In the Daml code we can see the template has a similar declaration as the data record (the with block). A template also requires a signatory (in the where block), the party (-ies) required when a contract is created from the template. In our test script, it is SwissBank which created this contract. Later we see choices are added in a template in the where block as well.
Step 3: Add a choice Transfer on Cash contract
We see the simple contract template in the previous step. Now we add an action item on this contract such that the owner can transfer this cash to another party. The action is done by choice(s) being added in the template. We also simulate a transfer of cash in the test script.
Daml code
module BikeShop whereimport Daml.Scriptdata Currency = USD | EUR | GBP | CHF
deriving (Eq, Show)template Cash
with
issuer: Party
owner: Party
currency: Currency
amount: Decimal
where
signatory issuer
controller owner can
Transfer : ContractId Cash
with
newOwner: Party
do
create this with owner=newOwner
Test script code to show that one party can transfer cash to another party (here Martin transfers the cash, issued by SwissBank, to BikeShop).
test = do
bank <- allocateParty "SwissBank"
martin <- allocateParty "Martin"
bikeshop <- allocateParty "BikeShop"let
cash = Cash
with
issuer = bank
owner = martin
currency = CHF
amount = 200.0cashCid <- submit bank do
createCmd cash
submit martin do
exerciseCmd cashCid Transfer with
newOwner = bikeshoppure()
Result of test script
We first observe active contract(s) in ledger (uncheck Show archived).
We see one active Cash contract, which is the cash issued by SwissBank and now owned by BikeShop. We can also see archived contracts in the ledger by checking Show archived.
We now have a more complete picture. The original Cash contract (issued by SwissBank to Martin) is archived, and the present Cash contract of the same content (CHF 200.0) is now owned by BikeShop, after Martin exercised the choice Transfer. Exercising a consuming (default) choice causes the current contract to be archived (#0:0), and a new contract is created according to the code in the exercised Transfer choice (#1:1).
Also note that adding a choice with controller owner sets owner an observer automatically. For example, in the current active contract (#1:1) BikeShop is the owner (designated as O), and therefore BikeShop is an observer as a result of being made a controller on a choice in the contract. (compared to Step 2).
Step 4: Bike Repair contract requiring two signatories
Now we can enrich the example with some real business contracts between two parties. In step 3 we see a simulation of Martin transferring his Cash contract to BikeShop. In reality it is the result after the BikeShop having completed a job such as repairing Martin’s bike. In this case we add this repair contract, and till the end of this example we will have a complete workflow.
Here we will see a bilateral template BikeRepair, which requires the consent or authorization of both parties before a contract is made. In Daml terms both the bike owner (Martin) and the bike shop (BikeShop) are the signatories on the BikeRepair contract that we instantiate below.
In this step, we first see how a template of this type of contract looks in the Daml code. Then we simulate situations where either side cannot create this contract without consent of the other side. In the next step we will see how to create this type of contract through propose-accept model.
Daml code
module BikeShop whereimport Daml.Script
import DA.Datedata Currency = USD | EUR | GBP | CHF
deriving (Eq, Show)template Cash
with
issuer: Party
owner: Party
currency: Currency
amount: Decimal
where
signatory issuer
controller owner can
Transfer : ContractId Cash
with
newOwner: Party
do
create this with owner=newOwnertemplate BikeRepair
with
bikeShop: Party
bikeOwner: Party
description: Text
price: Decimal
paymentDue: Date
where
signatory bikeShop, bikeOwner
Test script to show either side cannot create BikeRepair as it requires signatories from both bikeShop and bikeOwner.
BikeOwner (Martin) cannot create a repair contract by himself.
test = do
bank <- allocateParty "SwissBank"
martin <- allocateParty "Martin"
bikeshop <- allocateParty "BikeShop"let
cash = Cash
with
issuer = bank
owner = martin
currency = CHF
amount = 200.0cashCid <- submit bank do
createCmd cashlet
bikeRepair = BikeRepair with
bikeShop = bikeshop
bikeOwner = martin
description = "fix the bike"
price = 200.0
paymentDue = date 2021 Feb 17submit martin do
createCmd bikeRepairpure()
We see the result: missing an authorization from BikeShop.
BikeShop likewise cannot create a repair contract by themselves.
test = do
bank <- allocateParty "SwissBank"
martin <- allocateParty "Martin"
bikeshop <- allocateParty "BikeShop"let
cash = Cash
with
issuer = bank
owner = martin
currency = CHF
amount = 200.0cashCid <- submit bank do
createCmd cashlet
bikeRepair = BikeRepair with
bikeShop = bikeshop
bikeOwner = martin
description = "fix the bike"
price = 200.0
paymentDue = date 2021 Feb 17submit bikeshop do
createCmd bikeRepairpure()
Similarly, missing an authorization from Martin the bike’s owner.
Step 5: Create Bike Repair contract through propose-accept model
In the previous step we saw that a contract which required two signatories cannot be created directly by one single party. In real life the process of forming such a contract is through two steps: proposal and acceptance. This step shows how a new type of contract, called BikeRepairProposal helps to create this type of proposal. We simulate the flow in our test script and make observations on the results.
Daml code
module BikeShop whereimport Daml.Script
import DA.Datedata Currency = USD | EUR | GBP | CHF
deriving (Eq, Show)template Cash
with
issuer: Party
owner: Party
currency: Currency
amount: Decimal
where
signatory issuer
controller owner can
Transfer : ContractId Cash
with
newOwner: Party
do
create this with owner=newOwnertemplate BikeRepair
with
bikeShop: Party
bikeOwner: Party
description: Text
price: Decimal
paymentDue: Date
where
signatory bikeShop, bikeOwnertemplate BikeRepairProposal
with
proposer: Party
receiver: Party
proposal: BikeRepair
where
signatory proposer
controller receiver can
Accept : ContractId BikeRepair
do
create proposal
Test script code to show how propose-accept works to create a contract with two signatories. BikeShop initiates the proposal step by creating a BikeRepairProposal contract. Martin then accepts the proposal by exercising the choice Accept in this BikeRepairProposal contract.
test = do
bank <- allocateParty "SwissBank"
martin <- allocateParty "Martin"
bikeshop <- allocateParty "BikeShop"let
cash = Cash
with
issuer = bank
owner = martin
currency = CHF
amount = 200.0cashCid <- submit bank do
createCmd cashlet
bikeRepair = BikeRepair with
bikeShop = bikeshop
bikeOwner = martin
description = "fix the bike"
price = 200.0
paymentDue = date 2021 Feb 17proposalCid <- submit bikeshop do
createCmd BikeRepairProposal
with
proposer = bikeshop
receiver = martin
proposal = bikeRepairsubmit martin do
exerciseCmd proposalCid Acceptpure()
Result of test script (Cash contract is not shown)
If we just see the active contract, we now see the BikeRepair contract is successfully created, with both BikeShop and Martin as signatory.
By showing archived contracts, we see the BikeRepairProposal was created and now archived after Martin has accepted.
Step 6: Add a choice to pay once bike owner is satisfied with the work
Now we can add a choice to complete the example. In the BikeRepair contract we can add a choice that the bike owner accepts the job with satisfaction, which triggers a payment of his Cash to the BikeShop. Only when this choice is exercised is the cash of the BikeOwner transferred to the BikeShop. This choice is called Pay.
Daml code
module BikeShop whereimport Daml.Script
import DA.Datedata Currency = USD | EUR | GBP | CHF
deriving (Eq, Show)template Cash
with
issuer: Party
owner: Party
currency: Currency
amount: Decimal
where
signatory issuer
controller owner can
Transfer : ContractId Cash
with
newOwner: Party
do
create this with owner=newOwnertemplate BikeRepair
with
bikeShop: Party
bikeOwner: Party
description: Text
price: Decimal
paymentDue: Date
where
signatory bikeShop, bikeOwner
controller bikeOwner can
Pay : ContractId Cash
with
cashCid: ContractId Cash
do
exercise cashCid Transfer with newOwner=bikeShoptemplate BikeRepairProposal
with
proposer: Party
receiver: Party
proposal: BikeRepair
where
signatory proposer
controller receiver can
Accept : ContractId BikeRepair
do
create proposal
Test script to show Martin is paying for the service, assuming he is satisfied with the service.
test = do
bank <- allocateParty "SwissBank"
martin <- allocateParty "Martin"
bikeshop <- allocateParty "BikeShop"let
cash = Cash
with
issuer = bank
owner = martin
currency = CHF
amount = 200.0cashCid <- submit bank do
createCmd cashlet
bikeRepair = BikeRepair with
bikeShop = bikeshop
bikeOwner = martin
description = "fix the bike"
price = 200.0
paymentDue = date 2021 Feb 17proposalCid <- submit bikeshop do
createCmd BikeRepairProposal
with
proposer = bikeshop
receiver = martin
proposal = bikeRepairrepairCid <- submit martin do
exerciseCmd proposalCid Acceptsubmit martin do
exerciseCmd repairCid Pay with cashCidpure()
Result of test script
First, see the active contracts in the ledger. We see that only a Cash contract is there, with BikeShop as the owner.
By showing archived contracts, we can see the whole story. Those contracts like BikeRepairProposal and BikeRepair are all archived after the repair job is completed.
Worth mentioning is that in both BikeRepair and BikeRepairProposal, SwissBank is out of the scope. In fact it meets the real world requirement that the repair job is only a matter between BikeShop and Martin, and SwissBank should not have any information about this interaction. What SwissBank knows is that a transfer of a Cash contract, issued by SwissBank, is requested by Martin.
Step 7: Make sure the Cash contract meets the currency and price due to the BikeShop
In this final step we add some constraints when exercising a choice. In our example, we make sure that the amount specified in the Cash contract is identical to the price required in the BikeRepair contract. We simulate a case when it is not the case.
Daml code
module BikeShop whereimport Daml.Script
import DA.Datedata Currency = USD | EUR | GBP | CHF
deriving (Eq, Show)template Cash
with
issuer: Party
owner: Party
currency: Currency
amount: Decimal
where
signatory issuer
controller owner can
Transfer : ContractId Cash
with
newOwner: Party
do
create this with owner=newOwnertemplate BikeRepair
with
bikeShop: Party
bikeOwner: Party
description: Text
price: Decimal
paymentDue: Date
where
signatory bikeShop, bikeOwner
controller bikeOwner can
Pay : ContractId Cash
with
cashCid: ContractId Cash
do
cash <- fetch cashCid
assert (
cash.currency == CHF &&
cash.amount == price)
exercise cashCid Transfer with newOwner=bikeShoptemplate BikeRepairProposal
with
proposer: Party
receiver: Party
proposal: BikeRepair
where
signatory proposer
controller receiver can
Accept : ContractId BikeRepair
do
create proposal
Test script (to show assertion is working: that mismatch of Cash contract and the Repair price)
test = do
bank <- allocateParty "SwissBank"
martin <- allocateParty "Martin"
bikeshop <- allocateParty "BikeShop"let
cash = Cash
with
issuer = bank
owner = martin
currency = CHF
amount = 2000.0cashCid <- submit bank do
createCmd cashlet
bikeRepair = BikeRepair with
bikeShop = bikeshop
bikeOwner = martin
description = "fix the bike"
price = 200.0
paymentDue = date 2021 Feb 17proposalCid <- submit bikeshop do
createCmd BikeRepairProposal
with
proposer = bikeshop
receiver = martin
proposal = bikeRepairrepairCid <- submit martin do
exerciseCmd proposalCid Acceptsubmit martin do
exerciseCmd repairCid Pay with cashCidpure()
We can see assertion error.
This ends the demonstration of our Bike Shop example.
Andreas’ guest lecture provided very detailed explanation. I hope this guide help you follow this example with the codes and simulation. Wish to learn more about Daml? You can always start here.