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

So my invite:

template PoolInvite
    with
        poolKey: PoolKey
        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
                exerciseByKey @Pool poolKey InviteAccept with 
                    invite = self

(in the above snippet, assume newParticipant is not an observer/stakeholder of the Pool)

should already “technically” allow the newParticipant to exercise InviteAccept because the owner is the signatory of the Invite? BUT because of the visibility issue you describe, newParticipant is not able to “find” the contract and thus the exercise fails due to “cannot find contract”?

Building on this a bit further:

given the details in Roles in Daml - Introducing Multi-party submissions

The first of the blog speaks directly to the use case I was describing.

give that example, my understanding of the pattern is:

click here for full code
module ExpensePoolChain where

{-
Example of using authorization chains from Pool > PoolInvite > PoolMember > Expense.
Where the "org" signatory of the pool is passed into each ~sub-contract (from a business process perspective).
The org (or equivalent) would be a stable/unchanging party.
-}

import Daml.Script

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

template Pool 
    with
        org: Party
        poolMemberRole: Party
        owner: Party
        poolId: Text -- Never changes
        name: Text
    where
        signatory org
        observer owner, poolMemberRole

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

        nonconsuming choice InviteNewMember : ContractId PoolInvite
            with
                newMember: Party
            controller owner
            do
                create PoolInvite with
                    org
                    poolId
                    newMember

        nonconsuming choice AddExpense : ContractId Expense
            with
                member: Party
                expense: Int
            controller member
            do  
                membership <- fetchByKey @PoolMember (org, poolId, member)

                create Expense with
                    poolKey = key(this)
                    poolMemberRole = poolMemberRole
                    member
                    expense
            


-- Expected to be created through a choice in Pool
template Expense
    with
        poolKey: PoolKey
        poolMemberRole: Party
        member: Party
        expense: Int
    where
        signatory poolKey._1, member -- Org and Member

-- Expected to be created through a choice in Pool
template PoolInvite
    with
        org: Party
        poolId: Text
        newMember: Party
    where
        signatory org
        observer newMember

        choice AcceptInvite : ContractId PoolMember
            controller newMember
            do
                create PoolMember with
                    org
                    poolId
                    member = newMember
        
-- Expected to be created through a choice in PoolInvite (where pool invite was created through the pool)
template PoolMember
    with
        org: Party
        poolId: Text
        member: Party
    where
        signatory org, member
        
        key(org, poolId, member): MemberKey
        maintainer key._1



normalFlow = script do
    [alice, bob, org, pool_member] <- mapA allocateParty ["Alice", "Bob", "Org", "POOL_MEMBER"]

    let poolId = "pool-1234"

    -- Create a pool:
    poolCid <- submitMulti [org] [] do
        createCmd Pool with 
            org
            poolMemberRole = pool_member
            owner = alice
            poolId = poolId
            name = "Pool1"

    -- Invite Bob to the Pool:
    invite1Cid <- submitMulti [alice] [] do 
        exerciseCmd poolCid InviteNewMember with 
            newMember = bob

    -- Bob accepts invite:
    poolMemberCid <- submitMulti [bob] [] do 
        exerciseCmd invite1Cid AcceptInvite

    -- Test comparing signatories:
    Some (bobsMemCid, bobsMem) <- queryContractKey @PoolMember bob (org, poolId, bob)
    Some (bobsPoolCid, bobsPool) <- queryContractKey @Pool [org] (org, poolId)
    let sigsMatch: Bool = signatory(bobsMem) == signatory(bobsPool) -- A pool and membership have same signatories

    -- Querying for the pool as a "pool_member"
    Some (bobsPoolAsMemberCid, bobsPoolAsMember) <- queryContractKey @Pool [bob, pool_member] (org, poolId)

    -- Because the IAM would provide bob with the pool_member readAs pool_member permission.
    -- Permission would have been determined by bob having an active PoolMember contract signed by the Pool org.
    bobsExpense <- submitMulti [bob] [pool_member] do
        exerciseByKeyCmd @Pool (org, poolId) AddExpense with
            member = bob
            expense = 22

    return ()

If this example, the org is passed as a signatory into each “sub-contract” in the business process: Pool > Pool Invite > PoolMember > Expense.

The choices are allowing the re-use and passing of the signatories into the sub-contracts.

The IAM could/would check if the user/party has an active contract for PoolMember which was signed by Org and if yes then grant the readAs pool_member permission/party in the JWT.

Validation of each sub-contract would always be based on the org signatory which can only come from the parent contract choices (unless created directly by org as some sort of admin function).

Assuming this flow makes sense and is correct @cocreature ?

I understood the authorization patterns as described in the docs, but i feel this pattern / power/implications of how you can re-use signatories to sign sub contracts is something that is not well articulated in the docs and tutorials.

That flow looks sensible from a quick look.

@cocreature thanks for the followup!

@cocreature building on this a bit further:

I am looking at triggers as a way to inspect the PoolMember Contract and generate rights based on that contract: My understanding is:

  1. Have to inspect the PoolMember
  2. lookup the Pool using the Key, derived by the data in the PoolMember (the signatory of the member is the “pool”)
  3. BUT how would you restrict the ability of other actors to crreate a pool and memberships but reference the same role-party? if they generated an equiv tree of contracts, the result would be adding the same party-role. If you create pool-specific party-roles this would potentially lead to massive number of parties? (down side to this?)

Thanks

Please ask new questions in separate threads, that makes it easier for us to keep track of what has already been answered and easier for others to discover if there is already an answer to an issue they’re having.