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.”

Visual Studio Daml Studio extension

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

  1. Find ‘Model.daml’ in /app/daml/src/main/daml/Com/Daml/App/Template/.

    Daml model file tree
  2. Find the template TransferOffer in Model.daml.

  3. Write a new RejectedTransferOffer template directly above the currently existing template TransferOffer. The new template is a composite of the TransferOffer 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, and receiver, 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.

  4. Create a new RejectWithReason choice within the TransferOffer 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 the expiresAt value. The assertion fails and the transaction is aborted if the value of now is later than or equal to the time value of expiresAt.

    • 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.

  1. Quit and restart the terminals to terminate the processes.

  2. Navigate to the project’s root directory, then run the stop-canton script to clean the Docker containers:

    ./scripts/stop-canton.sh
    
    Stop canton script terminal output
  3. Clear Gradle’s cache:

    ./gradlew clean
    
    Gradle clean build success
  4. Clear the frontend servers:

    ./gradlew :app:frontend:clean
    
    Gradle frontend clean success
  5. 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.

    Daml model assemble success

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.