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 Update
s, 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:
- 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.
- 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.