template Pool
with
owner: Party
name: Text
participants: [Party]
and you want to invite participants, the default example is to use a consuming choice which would then allow a single invite and then re-create the pool with the additional participant.
Is it possible to have nonconsuming invites that update the participants of the pool?
Nonconsuming would allow multiple invites to be created while having an active Pool contract. Parties would Accept their invite, and the pool would be updated. But you would be limited to single sequential participant updates and would need to use a key instead of the contract id.
From my tests it seems this pattern is “difficult” at best: where the PoolInvite template would have a Accept choice which would have to find the pool and exercise a participantUpdate choice (which seems difficult to have a proper control logic on the choice.
My design keeps coming back to: The Pool should not hold participants, and Participants should be another contract. This type of contract separation would be used whenever parallel activity is desired.
Here is an example how a club could allow its members to invite other members in a non-racy fashion (using a non-consuming choice). The example also shows how a member can prove its membership to third parties.
module Club where
import Daml.Script
template Club
with
owner: Party
name: Text
publicParty : Party
where
signatory owner
observer publicParty
key this : Club
maintainer key.owner
nonconsuming choice Invite : ContractId Member
with
member : Party
newMember : Party
controller member
do
isMember <- visibleByKey @Member (this, member)
assert $ isMember || member == owner
create Member with club = this; member = newMember
nonconsuming choice Prove : ContractId MembershipProof
with
member : Party
observers : [Party]
controller member
do
isMember <- visibleByKey @Member (this, member)
assert $ isMember || member == owner
create MembershipProof with club = this; member; observers
template Member
with
club : Club
member : Party
where
signatory club.owner
observer member
key (club, member) : (Club, Party)
maintainer key._1.owner
template MembershipProof
with
club : Club
member : Party
observers : [Party]
where
signatory club.owner, member
observer member, observers
agreement "Here by " <> club.name <> " assures that " <> show member <> " is a member of " <> club.name
test = script do
-- Setup parties
[owner, alice, bob, charlie, eve, publicParty] <- mapA allocateParty ["Owner", "Alice", "Bob", "Charlie", "Eve", "PublicParty"]
-- Setup club
clubCid <- submit owner do createCmd Club with owner; name = "Club ABC"; publicParty
-- Club invites Alice
aliceMembershiCid <- submit owner do exerciseCmd clubCid Invite with member = owner; newMember = alice
-- Alice invites Bob
bobMembershiCid <- submitMulti [alice] [publicParty] do exerciseCmd clubCid Invite with member = alice; newMember = bob
-- Bob proves membership (for Charlie)
submitMulti [bob] [publicParty] do exerciseCmd clubCid Prove with member = bob; observers = [charlie]
-- Eve can't prove membership for anyone
submitMultiMustFail [eve] [publicParty] do exerciseCmd clubCid Prove with member = eve; observers = []
return ()
@Johan_Sjodin you pass club into the membership and proof: if you had to update the original club (such as change of owner) this would break the relationships? Why pass the entire club contract instead of a club key?
So this basically implies that the pattern for scaling/parallel comes down to breaking everything up into contracts.
@StephenOTT you are right that in order to make the Club unique you would need a key there too. For simplicity, I added the following as key to the Club ^^
key this : Club
maintainer key.owner
(Regarding changing owner/name/publicParty, the club could do that as they are the sole party to Club and Member templates. However, the purpose of the above example was only to illustrate how you can avoid raise conditions adding new members.)
In order for the club to be able to change its name you could have a different key, say:
module Test.Club where
import Daml.Script
type ClubKey = (Party, Int)
template Club
with
owner : Party
id : Int
name: Text
publicParty : Party
where
signatory owner
observer publicParty
key (owner, id) : ClubKey
maintainer key._1
nonconsuming choice Invite : ContractId Member
with
member : Party
newMember : Party
controller member
do
isMember <- visibleByKey @Member ((owner, id), member)
assert $ isMember || member == owner
create Member with clubKey = key this; member = newMember
nonconsuming choice Prove : ContractId MembershipProof
with
member : Party
observers : [Party]
controller member
do
isMember <- visibleByKey @Member ((owner, id), member)
assert $ isMember || member == owner
create MembershipProof with clubKey = key this; member; observers
choice ChangeName : ContractId Club
with
newName : Text
controller owner
do
create this with name = newName
template Member
with
clubKey : ClubKey
member : Party
where
let owner = clubKey._1
signatory owner
observer member
key (clubKey, member) : (ClubKey, Party)
maintainer key._1._1
template MembershipProof
with
clubKey : ClubKey
member : Party
observers : [Party]
where
let owner = clubKey._1
signatory owner, member
observer member, observers
test = script do
-- Setup parties
[owner, alice, bob, charlie, eve, publicParty] <- mapA allocateParty ["Owner", "Alice", "Bob", "Charlie", "Eve", "PublicParty"]
-- Setup club
clubCid <- submit owner do createCmd Club with owner; id = 1; name = "Club ABC"; publicParty
-- Club invites Alice
aliceMembershiCid <- submit owner do exerciseCmd clubCid Invite with member = owner; newMember = alice
-- Alice invites Bob
bobMembershiCid <- submitMulti [alice] [publicParty] do exerciseCmd clubCid Invite with member = alice; newMember = bob
-- Bob proves membership (for Charlie)
submitMulti [bob] [publicParty] do exerciseCmd clubCid Prove with member = bob; observers = [charlie]
-- Eve can't prove membership for anyone
submitMultiMustFail [eve] [publicParty] do exerciseCmd clubCid Prove with member = eve; observers = []
-- Owner changes the name of the club
clubCid <- submitMulti [owner] [publicParty] do exerciseCmd clubCid ChangeName with newName = "Club 123"
-- Alice can prove membership (for Charlie)
submitMulti [alice] [publicParty] do exerciseCmd clubCid Prove with member = alice; observers = [charlie]
return ()
@Johan_Sjodin if you changed the owner of the club, would this not break your membership contracts? I see it as all part of the same pattern: yes the example shows a limited feature, but is suggests to pass the pool contract into other contracts such as membership: Which would seem to break if the original pool was ever updated (such as change owner). So the pattern would be to pass the key, and always do lookups?
In your example of the Club, if the Club changed owners, you are suggesting that all membership and proof contracts would be re-created with the new club contract?
Alternate pattern: pass the Club Key into Membership and Proof, and those contacts would need to do a lookup/fetch on the key to get the current active club contract ? This would seem to mean you do not need to re-create membership and proof contracts whenever some of the club data changes (such as owner).
As the owner of a club is the sole signatory of the Club and Membership (in the above example), the owner can archive the whole club and all associated memberships. Note that any party can also create its own club, and add arbitrary members. But no member would need to prove a membership of such a club. I haven’t thought more about changing the owner of a club (and it was not the purpose of the “minimal” example).
@Johan_Sjodin thanks for followup. I agree the owner of the club can archive, as my ExpensePool example demonstrates the same concept. And yes i agree the club is a minimal example, but in that example it passes the club into the membership and proof, essentially making the club immutable unless you are willing to re-create contracts for membership and proof whenever the club contract has changes.
This is the pattern I am identifying/questioning in my first comment of this thread. Many of the examples are based on designs of immutable parent contracts that if changed would require re-creation of all child contracts. Of course this story works well for the basic concepts, but I keep seeing the failure when trying to scale the workflow of the contracts to real-world structures/business workflows (as shown in comment).
has there been testing or experience on if it is “better” to re-create contracts vs create loose key based linkages between contracts? Example: If the Club is immutable, then a change to the club would require complete archiving of the club and child contracts followed by the recreation of the club with the new data + re-creation of all child contracts using the new version of the club. At ~scale is this a lot of/burdensome changes against the ledger??
vs if the club is mutable similar to my ExpensePool example above, then the linkages between contracts are based on Key and lookups during choices instead of using a static already passed club within the child templates/contracts. A ~caveat problem is that ensure cannot seem to use fetchByKey, so potential limitations there (as per comment in update-in-ensure-clause thread).
The second example ^^ (i.e., where the ClubKey = (Parties, Int)) actually allows the owner of the Club to change its name without having to update any of the other contracts referring to it by key (as the key remains the same). Any field which is not part of the key can obviously be updated in this fashion without breaking a reference by key.
The purpose of the first example ^^ was only to answer your initial question
with a “small” example how members could be added to a club in a non-racy manner (and there I simply put club : Club as key for simplicity reasons). But having such a key (as you point out) does not allow you to modify any fields of the Club (as they all belong to the key, and would hence break the key reference made), so it is better to put the fields you want to be able to modify outside of the key (as in the second example ^^).
Regarding:
I would say that depends very much on your use case what is better. If you know that a contract instance, say of Foo, is not going to be archived and recreated (very often), and is only referenced by a few other contract instances, you might even want to consider referencing by a ContractId Foo (especially if such referring instances also need to be updated in case the ContractId Foo is no longer active). But key references can indeed save you from recreating a lot of contracts.