Extend the Model¶
Extend the single domain quick start, beginning with the model. Daml choices determine how the backend API endpoints are written, which in turn guide the development of the frontend UI elements and event listeners.
Begin with an open terminal in the project’s root directory.
If you use Visual Studio, enter daml studio
to open the text editor.
daml-app-template ~ % daml studio
Note
daml studio
should automatically install the Daml Studio extension in Visual Studio. The extension assists Daml app development. You can double-check that the extension is installed by opening the extension menu and searching for “Daml Studio.”
Note
Intellij also supports Daml development.
The Daml Template¶
Daml templates model business workflows. They enforce rules regarding the creation and management of contracts. Templates define a contract type, which can be instantiated and managed on the Daml ledger.
Templates specify:
- fields and data types that define the contract payload information upon creation
- signatory, whose authorization is required for the creation of the contract
- choices, which are actions that a controller can take on a contract
Create a RejectedTransferOffer Template
Find ‘Model.daml’ in
/app/daml/src/main/daml/Com/Daml/App/Template/
.Find the
template TransferOffer
in Model.daml.Write a new
RejectedTransferOffer
template directly above the currently existingtemplate TransferOffer
. The new template is a composite of theTransferOffer
template that extends the ability for providers, senders, and receivers to reject a transfer offer with a reason.167 template RejectedTransferOffer 168 with 169 transferOffer : TransferOffer 170 rejectionReason : Text 171 where 172 signatory transferOffer.provider, transferOffer.sender, transferOffer.receiver 173 174 ensure rejectionReason /= "" 175 176 key (transferOffer.provider, transferOffer.sender, transferOffer.receiver, transferOffer.trackingId) : (Party, Party, Party, Text) 177 maintainer key._1
The template takes a transfer offer and a rejection reason, a non-empty string, requires a transfer offer with a
provider
,sender
, andreceiver
, and creates an identifier key for each rejection.signatory transferOffer.provider…
uses dot notation to maintain a direct link to the original offer. This format ensures that the Parties are identified consistently across related contracts.maintainer key._1
indicates that the provider is responsible for maintaining the contract.Create a new
RejectWithReason
choice within theTransferOffer
template:218 choice RejectWithReason : (ContractId RejectedTransferOffer, ContractId Transferable.I) 219 with 220 reason : Text 221 controller receiver 222 do 223 now <- getTime 224 assertMsg "The transfer offer has expired and cannot be rejected" (now < expiresAt) 225 rejectedOfferCid <- create RejectedTransferOffer with 226 transferOffer = this 227 rejectionReason = reason 228 unlockedCid <- unlockAndRemoveObservers (S.fromList [provider, sender]) receiver lockedTransferableCid 229 return (rejectedOfferCid, unlockedCid)
This choice takes a reason for the rejection and ensures that the receiver controls the choice and that the transfer offer contract has not expired. When the choice is exercised a new “RejectedTransferOffer” contract is created that then unlocks the assets that were to be transferred and removes the provider and sender as observers. This choice contains a handful of Daml-specific items:
controller receiver
specifies that only the receiver of the transfer offer can exercise this choice.now <- getTime
fetches the current time on the ledger.Note
In a dev environment without an active ledger, the getTime function returns the epoch time, January 1, 1970. This happens because the ledger is not active and cannot fetch the system time. In a Daml application deployed to an active ledger,
getTime
fetches the current ledger time, which reflects the time that the ledger itself is using.assertMsg
asserts the current time is less than the time of theexpiresAt
value. The assertion fails and the transaction is aborted if the value ofnow
is later than or equal to the time value ofexpiresAt
.unlockAndRemoveObservers
removes the receiver. When the receiver rejects the offer they are no longer a stakeholder in the transaction and have no need to observe the contract. The function call also unlocks the credits reserved for the transaction.
At this point, the extension features a new template, RejectedTransferOffer
, that allows for a rejection reason. It also implements a new choice, RejectWithReason
within the TransferOffer
template, which takes a reason and creates the RejectedTransferOffer
contract.
Canton needs to be stopped and the caches cleared to ensure the model rebuilds properly.
Quit and restart the terminals to terminate the processes.
Navigate to the project’s root directory, then run the stop-canton script to clean the Docker containers:
./scripts/stop-canton.sh
Clear Gradle’s cache:
./gradlew clean
Clear the frontend servers:
./gradlew :app:frontend:clean
Rebuild the DAR files:
./gradlew :app:daml:assemble
If the choices have been added correctly, the DARs should update with the new choices in the TransferOffer template.
You have implemented the new RejectedTransferOffer
template and RejectWithReason
choice in Model.daml
. This extension allows users greater control over their transfer offers. The next step integrates the new choices into the backend API to interact with the Daml ledger.