Fetch contract by key for use in choice or ensure

Given comment from: `Update` in `ensure` clause - #2 by cocreature

Is it possible in a choice or ensure, to get a contract by key so you can use the data from the contract in the execution of the choice?

In short: when can we fetch a contracts data by key within another contract ?

Fetching a contract by key always has to happen within an Update. So there is no way to do that in an ensure.

For completeness, since the question was about both ensure and choice: choices are implicitly always Updates, so you can fetch from a choice body.

Is there an alternative to fetch that would allow access to. I react data within an ensure?

No the ensure only gets access to the template payload, it cannot fetch anything external. If you need additional information, you need to store it in there.

@cocreature given ensure cannot be used to fetch a contract: is there a pattern for managing control of who can create a specific contract type based on a loose join of a key?

Something like this?

import DA.List

type PoolKey = (Party, Text)

template Pool 
    with
        owner: Party
        name: Text
        participants: [Party]
    where
        signatory owner
        observer participants

        ensure 
            unique participants
        
        key(owner, name): PoolKey
        maintainer key._1

        nonconsuming choice InviteNewParticipant : ContractId PoolInvite
            with
                newParticipant: Party
            controller owner -- Only the owner can invite new people.
            do
                create PoolInvite with
                    poolKey = key(this) -- We use the key for reference: If the owner changes, then previous invites would need to be recreated
                    newParticipant = newParticipant

        choice InviteAccept : ContractId Pool -- Internal action used 
            with
                invite: PoolInvite
            controller invite.newParticipant
            do
                assertMsg "Invite must have come from current Pool owner" (invite.poolKey == key(this))
                create this 
                    with owner, name, participants = invite.newParticipant :: this.participants



template PoolInvite
    with
        poolKey: PoolKey
        newParticipant: Party
    where
        signatory poolKey._1
        observer newParticipant

        choice AcceptInvite : ContractId Pool
            controller newParticipant
            do
                exerciseByKey @Pool poolKey InviteAccept with -- <--- This cannot run because newParticipant does not "yet" observe the pool.
                    invite = this

Here is a “working” version that uses a InvitedParticipants list within the pool.

This seems to allow parallel invites to be created. BUT it feels weird. Is there a way to exercise an a choice without being an observer? If not, then it would seem you have to always store “state” within the master contract.

module ExpensePoolV3 where

import DA.List as L

type PoolKey = (Party, Text)

template Pool 
    with
        owner: Party
        name: Text
        participants: [Party]
        invitedParticipants: [Party]
    where
        signatory owner
        observer participants, invitedParticipants

        ensure do
            L.unique(participants ++ invitedParticipants) -- merge two lists together: cannot have duplicates between particpants and invited

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

        choice InviteNewParticipant : ContractId Pool
            with
                newParticipant: Party
            controller owner -- Only the owner can invite new people.
            do
                create PoolInvite with
                    poolKey = key(this) -- We use the key for reference: If the owner changes, then previous invites would need to be recreated
                    newParticipant = newParticipant
                
                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 = newParticipant :: invitedParticipants

        choice InviteAccept : ContractId Pool
            with
                invite: PoolInvite
            controller invite.newParticipant
            do
                assertMsg "Invite must have come from current Pool owner" (invite.poolKey == key(this))
                create this 
                    with 
                        owner = owner
                        name = name
                        participants = invite.newParticipant :: participants
                        invitedParticipants = delete invite.newParticipant invitedParticipants



template PoolInvite
    with
        poolKey: PoolKey
        newParticipant: Party
    where
        signatory poolKey._1
        observer newParticipant

        choice AcceptInvite : ContractId Pool
            controller newParticipant
            do
                exerciseByKey @Pool poolKey InviteAccept with -- <--- This cannot run without using the "Invited Participants" prop in the Pool because newParticipant would not "yet" observe the pool.
                    invite = this

It seems without the ability to exercise a choice on an un-observed contract, state ~has to be stored as a field in the contract?

here is an updated working example that uses the master contract Pool to store everything.

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: PoolInvite
            controller invite.newParticipant
            do
                assertMsg "New Participant is not part of invited participants." (Set.member invite.newParticipant invitedParticipants)
                assertMsg "Invite is not from current pool owner." (invite.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.
                create this 
                    with
                    poolId
                    owner
                    name
                    participants = Set.insert invite.newParticipant participants
                    invitedParticipants = Set.delete invite.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
        newParticipant: Party
    where
        signatory poolKey._1 -- The pool owner
        observer newParticipant

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

        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 = this



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)

Hi @StephenOTT, yes there is. In case users have readAs rights of a stakeholder of the contract (say a publicParty) this is possible. Here is an example where I adjusted your code a little:

module ExpensePool where

import Daml.Script

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


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

template Pool
  with
    poolId : Text
    owner : Party
    name : Text
    participants : Parties
    publicParty : Parties
  where
    signatory owner
    observer participants, publicParty

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

    nonconsuming choice InviteNewParticipant : ContractId PoolInvite
      with
        newParticipant : Party
      controller owner
      do
        create PoolInvite with poolKey = key this; newParticipant

    choice RemoveParticipant : ContractId Pool
      with
        participantToRemove: Party
      controller owner
      do
        assertMsg "unknown participant" (Set.member participantToRemove participants)
        create this with participants = Set.delete participantToRemove participants

template PoolInvite
  with
    poolKey: PoolKey
    newParticipant: Party
  where
    signatory poolKey._1 -- The pool owner
    observer newParticipant

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

    choice AcceptInvite : ContractId Pool
      controller newParticipant
      do
        (poolCid, pool) <- fetchByKey @Pool poolKey
        archive poolCid
        create pool with participants = Set.insert newParticipant pool.participants

normalFlow = script do
  [alice, bob, publicParty] <- mapA allocateParty ["Alice", "Bob", "PublicParty"]

  -- Create a pool:
  poolCid <- submit alice do
    createCmd Pool with poolId = "pool-1234"; owner = alice; name = "Pool1"; participants = Set.empty; publicParty = Set.singleton publicParty

  -- Invite Bob to the Pool:
  invite1Cid <- submit alice do exerciseCmd poolCid InviteNewParticipant with newParticipant = bob

  -- Revoke Bob's invite:
  submit alice do archiveCmd invite1Cid

  -- Re-Invite Bob to the Pool:
  invite2Cid <- submit alice do exerciseCmd poolCid InviteNewParticipant with newParticipant = bob

  -- Bob accepts invite:
  poolCid <- submitMulti [bob] [publicParty] do exerciseCmd invite2Cid AcceptInvite

  Some _updatedPool <- queryContractId alice poolCid
  assert (Set.member bob _updatedPool.participants)

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

  Some _updatedPool <- queryContractId alice poolCid
  assert $ Set.notMember bob _updatedPool.participants

  -- Re-Invite Bob to the Pool:
  invite3Cid <- submit alice do exerciseCmd poolCid InviteNewParticipant with newParticipant = bob

  -- Bob accepts invite:
  poolCid <- submitMulti [bob] [publicParty] do exerciseCmd invite3Cid AcceptInvite

  -- Final pool should have bob as a participant
  Some finalPool <- queryContractId bob poolCid
  assert $ Set.member bob finalPool.participants

  return ()

No, there is in general no way to restrict who can create a contract of a given template. As a participant on a ledger you can choose not to sign contracts of a given type unless they have been created by a trusted set of parties, but you cannot prevent other people from having a different circle of trust than yours.

No, but as @Johan_Sjodin mentioned the notion of “being” an observer is a bit more flexible than it appears at first. More precisely, in order to exercise a choice:

  1. You need to have credentials that allow you to see the contract. The most direct approach here is to have your main party be either a signatory or an observer of the contract.
  2. The choice itself needs to have rules that allow you to exercise it, of course.

For 1, less direct approaches include:

a. The deprecated “controller can” syntax implicitly adds the controller to the observers of the contract, so this can give you visibility without explicitly appearing in either the signatory or observer entries.
b. The deprecated divulgence mechanism can give you visibility on a contract you’re not an observer (or signatory) on, and that visibility can be enough to exercise choices.
c. “You” means all the access rights granted by your token; if your token gives you rights as multiple parties, you could conceivably exercise a choice with an “actAs” party that doesn’t see the contract, based on visibility from one of your “readAs” parties, provided that you somehow still fit condition 2 with that “actAs” party.

@Gary_Verhaegen thank you for the detailed response. The pattern I keep coming back to is: using role-contracts and modifying readAs as per Internal templates? (templates/contracts that can only be created by other template choices?) - #22 by StephenOTT