Internal templates? (templates/contracts that can only be created by other template choices?)

I have not been able to find reference to this in the docs, so sorry if I missed something:

Is there a concept of something like internal templates? Where a template can only be used by another template (such as part of a choice)? If this does not exist, I am not saying it should exist, but i was considering the list of templates in the the navigator UI and in Daml Hub: where we list all templates, but not all are “public”: such as the concept of Public APIs / Interfaces vs the internal code.

Thanks

There is no such concept at the moment. It comes up occasionally and I do think something along those lines is sensible but so far it hasn’t been prioritized.

Great thanks.

@cocreature how do users restrict the usage of a template if there is no concept of “internal”? If you had a flow such as Invite > Participant: and your invite consumed into a Participant, how do you prevent the Participant from being created directly (and only created through a choice in an invite) ?

You cannot prevent contracts from being created directly. However, you can prevent them from being created with your signature (by not providing your authority for that). Generally, I recommend to switch from “all contracts of template T” to “all contracts of template T signed by parties X”. A Daml ledger is an open system. Parties can add contracts that only affect them as they want to.

1 Like

@cocreature so based on that design, the signatory becomes a rippling change in the ledge:

An Expense Pool + Invites to join the pool. The original invites are created/signatory by a choice in the pool by the owner of the pool (the signatory of the invite). Now the owner of the pool changes, but there are still 100 invites that are active/open for that pool. From the description you provided, my understanding is: A change of the owner would have to propagate updates of the 100 invite contracts to recreate new versions of the contract with the new owner/signatory. Is that correct? Is that the recommended design?

Yes, in that design (and that’s the one I recommend) ownership changes become a bit trickier because you need to update other contracts as well. The details of how to best handle that depend on your specific design. In some cases it may make sense that as part of the ownership transfer you don’t directly modify the other contracts but you delegate authority to the new owner to change the owners of the other contracts.

But in either case, it means all 100 contracts must be updated.
If it was done with delegation it would 100 delegation wrappers + 100 invite contract updates?

No, you could create one contract with a non-consuming choice for the delegation. And you don’t need to do the invite upgrades immediately. You can often find designs where you update them lazily as they are used.

Is there a working example of this delegation?

Would this not be simpler to have secondary parties that were like “super owners” that the IAM gives actAs permissions to the actual “Pool” owner? Such as a party that represented the “pool”

I don’t have an example repo but something like this (untested)

template Invite
  with
    owner : Party
    invitee : Party
  where
    signatory owner
template DelegateOwnershipChange
  with
    previousOwner : Party
    newOwner : Party
  where
    signatory previousOwner, newOwner
    nonconsuming choice ChangeOwner : ContractId Invite
      with
        cid : ContractId Invite
      controller newOwner
      do invite <- fetch cid
           invite.owner == oldOwner
           create invite with owner = newOwner

Would this not be simpler to have secondary parties that were like “super owners” that the IAM gives actAs permissions to the actual “Pool” owner? Such as a party that represented the “pool”

That’s another option. Keep in mind thought that the IAM is per participant so to make this work you need to potentially move a party across participants (there is functionality for that in Canton in early access).

You can also have a delegation design where you have one party as the owner and then you delegate via the Daml model the rights to act as that owner to another party.

I feel like i am missing a detail in your example:

Based on what you wrote, what i see is: If there were 100 invites, your delegatingOwnership would be used to generate 100 delegating invites, which then generate 100 new invites / replacing the old invites. Which would still be 200+ transactions for a ownership change, no?

You can also have a delegation design where you have one party as the owner and then you delegate via the Daml model the rights to act as that owner to another party.

Is there a working example of this? Where delegation does not mean placing the original contract in “limbo” / wrapped in the delegating contract?

I am looking at this: https://github.com/StephenOTT/daml-quick-start/blob/1206d7cd0a683c0a3f633cce318f018d7f54084a/ExpensePool/daml/ExpensePoolWithState.daml

There is nonconsuming invites, and the invite need would need to still be active after a change to the pool (such changing the name of the pool)

Even with delegation is seems to just generate more and more contract updates

Here is a detailed example with Expense Pool + Invites.

The only way i have thought of to restrict creation is using signatory (“works in the tests”/does not throw errors):

This checks if the invite being processed is from the same pool and if the invite was created by the same pool owner.

But this example would still require Invite contract updates with a new pool owner issuing new invites/replacement invites.

in either case, it seems like unless there is another functionality: the design pattern becomes: live with the potential of mass contract updates or use the IAM and “role-parties”/“group-parties” (or whatever you want to call them) which would act as a super owner that real parties could actAs for admin-like functions on a ~“master-contract”.

module ExpensePoolWithState where

import Daml.Script

import DA.Set (Set)
import DA.Set qualified as Set

type PoolKey = (Party, Text)
type InviteKey = (PoolKey, Party)

template Pool 
    with
        poolId: Text
        owner: Party
        name: Text
        participants: Set Party
        invitedParticipants: Set Party
    where
        signatory owner
        observer participants, invitedParticipants

        ensure do
            -- Cannot have overlap between the two lists:
            Set.null(Set.intersection participants invitedParticipants)
            

        key(owner, poolId): PoolKey
        maintainer key._1

        choice InviteNewParticipant : (ContractId Pool, ContractId PoolInvite)
            with
                newParticipant: Party
            controller owner
            do
                newPool <- create this -- recreate the pool with the invited participant : This is required because of Observers being able to Accept the Invite.
                    with
                        owner
                        name
                        participants
                        invitedParticipants = Set.insert newParticipant invitedParticipants
                
                invite <- create PoolInvite with
                    poolKey = key(this)
                    newParticipant
                
                return (newPool, invite)

        choice InviteAccept : ContractId Pool
            with
                invite: ContractId PoolInvite
            controller invitedParticipants
            do
                -- SEE HERE:
                i <- fetch @PoolInvite invite
                
                assertMsg ("Creator of invite must be current pool owner") (signatory(i) == [owner])
                assertMsg "Pool Key does not match" (i.poolKey == key(this)) -- Assumes that if the owner changes than existing invites become invalid.  This line could be removed if you dont care about owner changes.
                assertMsg "New Participant is not part of invited participants." (Set.member i.newParticipant invitedParticipants)
                
                create this 
                    with
                    poolId
                    owner
                    name
                    participants = Set.insert i.newParticipant participants
                    invitedParticipants = Set.delete i.newParticipant invitedParticipants

        choice RemoveParticipant : ContractId Pool
            with
                participantToRemove: Party
            controller owner
            do
                assertMsg "Participant you want to remove is not in the list of participants for this pool" 
                    (Set.member participantToRemove participants)

                create this with
                    poolId
                    owner
                    name
                    participants = Set.delete participantToRemove participants
                    invitedParticipants 


        choice RemoveInvitedParticipant : ContractId Pool
            with
                participantToRemove: Party
            controller owner
            do
                assertMsg "Participant you want to remove is not in the list of participants for this pool" 
                    (Set.member participantToRemove invitedParticipants)
                
                -- Generates the invite Key + archives the invite: essentially revoking the invite
                let inviteKey = (key(this), participantToRemove):InviteKey
                invite <- fetchByKey @PoolInvite inviteKey
                exerciseByKey @PoolInvite inviteKey Archive

                -- recreates the Pool with the invitee removed
                create this with
                    poolId
                    owner
                    name
                    participants
                    invitedParticipants = Set.delete participantToRemove invitedParticipants



template PoolInvite
    with
        poolKey: PoolKey -- Use the pool key to find the pool in the future (such as if changes to the pool occur)
        newParticipant: Party
    where
        signatory poolKey._1 -- The pool owner is the creator of the invite
        observer newParticipant

        key(poolKey, newParticipant): InviteKey
        maintainer key._1._1 -- Pool owner

        postconsuming choice AcceptInvite : ContractId Pool
            controller newParticipant
            do
                -- This works because the invitee is part of the invitedParticiants Set in the Pool, which is an observer of the pool.
                exerciseByKey @Pool poolKey InviteAccept with 
                    invite = self



normalFlow = script do
    alice <- allocateParty "Alice"
    bob <- allocateParty "Bob"
    
    -- Create a pool:
    emptyPool1Cid <- submit alice do 
        createCmd Pool with 
            poolId = "pool-1234"
            owner = alice
            name = "Pool1"
            participants = Set.empty
            invitedParticipants = Set.empty


    -- Invite Bob to the Pool:
    (updatedPool1Cid, invite1Cid) <- submit alice do 
        exerciseCmd emptyPool1Cid InviteNewParticipant with 
            newParticipant = bob

    Some _updatedPool <- queryContractId alice updatedPool1Cid
    assert(Set.member bob _updatedPool.invitedParticipants)


    -- Revoke Bob's invite:
    emptyPool2Cid <- submit alice do 
        exerciseCmd updatedPool1Cid RemoveInvitedParticipant with
            participantToRemove = bob
    
    Some _updatedPool <- queryContractId alice emptyPool2Cid
    assert(Set.notMember bob _updatedPool.invitedParticipants)


    -- Re-Invite Bob to the Pool:
    (updatedPool2Cid, invite2Cid) <- submit alice do 
        exerciseCmd emptyPool2Cid InviteNewParticipant with 
            newParticipant = bob

    Some _updatedPool <- queryContractId alice updatedPool2Cid
    assert(Set.member bob _updatedPool.invitedParticipants)


    -- Bob accepts invite:
    updatedPool3Cid <- submit bob do 
        exerciseCmd invite2Cid AcceptInvite

    Some _updatedPool <- queryContractId alice updatedPool3Cid
    assert(Set.member bob _updatedPool.participants)
    assert(Set.notMember bob _updatedPool.invitedParticipants)


    -- Remove Bob as a participant:
    emptyPool3Cid <- submit alice do 
        exerciseCmd updatedPool3Cid RemoveParticipant with
            participantToRemove = bob

    Some _updatedPool <- queryContractId alice emptyPool3Cid
    assert(Set.notMember bob _updatedPool.participants)


    -- Re-Invite Bob to the Pool:
    (updatedPool4Cid, invite3Cid) <- submit alice do 
        exerciseCmd emptyPool3Cid InviteNewParticipant with 
            newParticipant = bob
    
    Some _updatedPool <- queryContractId alice updatedPool4Cid
    assert(Set.member bob _updatedPool.invitedParticipants)


    -- Bob accepts invite:
    updatedPool5Cid <- submit bob do 
        exerciseCmd invite3Cid AcceptInvite

    -- Final pool should have bob as a participant
    Some finalPool <- queryContractId bob updatedPool5Cid
    assert(Set.member bob finalPool.participants)
    assert(Set.notMember bob finalPool.invitedParticipants)
    assert(Set.null finalPool.invitedParticipants)

Let’s take a step back and look at the different options and the tradeoffs:

  1. Don’t make the owner a signatory. As you noticed yourself, this allows anyone to create invites. In some cases, that may be acceptable but usually this is not a workable model.
  2. Make the owner a signatory and solve ownership transfer by changing the owner party. There is no way around archiving & recreating the contracts in that case if you want to promptly change the owner in all contracts. However, at least in some cases you can do something more clever: In your example, one option would be that you keep the invites untouched but on InviteAccept you accept invites that still have the old owners.
  3. Make the owner a signatory and solve ownership transfer at the IAM level. This works if you have a relatively closed system where both parties are hosted on the same participant so they share the IAM. It becomes fairly difficult to manage if parties are hosted on different participants though.
  4. Make the owner a signatory and solve ownership by delegation. In that model, you have a super owner that never changes. That party delegates rights to the current owner to act on their behalf, e.g., to accept invites. There you don’t need to rewrite anything and it works regardless of how parties are distributed across participants.

A simple sketch for delegating would look something like this (only modelling one choice but the other ones follow the same pattern)

template OwnerDelegation
  with
    poolOwner : Party
    delegate : Party
  where
    signatory poolOwner, delegate
    nonconsuming choice InviteNewParticipant : (ContractId Pool, ContractId PoolInvite)
      with
        cid : ContractId Pool
        newParticipant : Party
      controller delegate
      do exercise cid (InviteNewParticipant newParticipant)

4 is the most flexible option, 2 and 3 may be a bit simpler in some cases if you’re happy to accept the limitations.

@cocreature thank you very much for the details! In you delegation draft, if the delegate is not a stakeholder of the cid, what is giving them the right to run ‘exercise cid (InviteNewParticipant newParticipant)’ ? Is it the double signatory?

(I am assuming the controller of the choice in the pool is ‘controller newParticupant’, but they would not have visibility of the contract so cannot exercise)

So there are two parts here:

Authorization & visibility.

The delegate has the authorization because within the choice body you have authorization from signatories & controllers.

Visibility is trickier. Ignoring divulgence, the reading parties (the union of actAs & readAs) must be a stakeholder on each contract used in the submision. One option would be that the owner provides readAs rights to the delegate. The other option ofc is that they are an observer although if you want to do that on each invitation as well you’re just back to having to modify all contracts. We are currently working on a new feature which would allow the owner to disclose the contracts to the delegate without requiring modification of the contracts to make this type of stuff easier.