How to add Reject and Revise Choices to Multiparty Agreement Pattern?

Hello community, I was going through the multiparty agreement pattern documentation and would like to add the Reject and Negotiate choice that was stated but I am having issues with their implementation. Would appreciate it if someone could help me!
Thanks!

Hi @Dion_Then,

Can you elaborate a little bit on exactly what you’ve tried and why it didn’t work? It’s a bit hard to tell how we can help you without more details.

Hi @Gary_Verhaegen

For the Reject, I used the Finalize choice as a reference point and created a new template for when a party would reject. However, this only works for the last party member and requires all other parties to sign first. I am aiming for anyone to be able to reject without requiring the others to sign.

Here is my code:

module Main where
import Daml.Script
import DA.List (sort, unique)

template ApprovedContract
  with
    signatories: [Party]
    status: Text
  where
    signatory signatories
    ensure
      unique signatories

template RejectedContract
  with
    signatories: [Party]
    rejector: Party
    rejectionNote : Text
  where
    signatory signatories
    

template Pending
  with
    finalContract: ApprovedContract
    alreadySigned: [Party]
    document: Text
    note : Text
  where
    signatory alreadySigned
    observer finalContract.signatories
    ensure
      -- Can't have duplicate signatories
      unique alreadySigned

    -- The parties who need to sign is the finalContract.signatories with alreadySigned filtered out
    let toSign = filter (`notElem` alreadySigned) finalContract.signatories

    -- Approve Contract
    choice Sign : ContractId Pending with 
        signer : Party
      controller signer
        do
          -- Check the controller is in the toSign list, and if they are, sign the Pending contract
          assert (signer `elem` toSign)
          create this with 
            alreadySigned = signer :: alreadySigned
            finalContract.status = "Pending full approval"
    -- Reject Contract
    choice Reject: ContractId RejectedContract with
        signer : Party
        feedback : Text    
      controller signer
        do
          create RejectedContract with 
            signatories = finalContract.signatories
            rejector = signer
            rejectionNote = feedback

    -- Finalize Contract
    choice Finalize : ContractId ApprovedContract with
        signer : Party
      controller signer
        do
          -- Check that all the required signatories have signed Pending
          assert (sort alreadySigned == sort finalContract.signatories)
          create finalContract with
            status = "Fully Approved!"
          
test : Script (ContractId ApprovedContract)
test = script do

  -- setting of parties
  person1 <- allocatePartyWithHint "alice" (PartyIdHint "alice")
  person2 <- allocatePartyWithHint "bob" (PartyIdHint "bob")
  person3 <- allocatePartyWithHint "charlie" (PartyIdHint "charlie")
  person4 <- allocatePartyWithHint "dion" (PartyIdHint "dion")
  person1Id <- validateUserId "alice"
  person2Id <- validateUserId "bob"
  person3Id <- validateUserId "charlie"
  person4Id <- validateUserId "dion"
  createUser (User person1Id (Some person1)) [CanActAs person1]
  createUser (User person2Id (Some person2)) [CanActAs person2]
  createUser (User person3Id (Some person3)) [CanActAs person3]
  createUser (User person4Id (Some person4)) [CanActAs person4]  

  let finalContract = ApprovedContract with signatories = [person1, person2, person3, person4]; status = "New"

  -- Parties cannot create a contract already signed by someone else
  initialFailTest <- person1 `submitMustFail` do 
    createCmd Pending with finalContract; alreadySigned = [person1, person2]; document = "Details on contract"; note = "Please sign if contract is all good"

  -- Any party can create a pending contract provided they list themeselves as the only signatory
  pending <- person1 `submit` do
    createCmd Pending with finalContract; alreadySigned = [person1]; document = "Details on contract"; note = "Please sign if contract is all good"

  -- Each signatory of the finalContract can Sign the Pending contract
  pending <- person2 `submit` do
    exerciseCmd pending Sign with signer = person2

  pending <- person3 `submit` do
    exerciseCmd pending Sign with signer = person3

  pending <- person4 `submit` do
    exerciseCmd pending Sign with signer = person4

  -- A party can't sign the Pending contract twice
  pendingFailTest <- person3 `submitMustFail` do
    exerciseCmd pending Sign with signer = person3

  -- A party can't sign on behalf of someone else
  pendingFailTest <- person3 `submitMustFail` do 
    exerciseCmd pending Sign with signer = person4

  person1 `submit` do 
    exerciseCmd pending Finalize with signer = person1


  -- Reject pending (Not working)
  pending <- person1 `submit` do
    createCmd Pending with finalContract; alreadySigned = [person1]; document = "Details on contract"; note = "Please sign if contract is all good"

  pending <- person2 `submit` do
    exerciseCmd pending Sign with signer = person2

  pending <- person3 `submit` do
    exerciseCmd pending Sign with signer = person3

  person4 `submit` do
    exerciseCmd pending Reject with signer = person4

In addition, when I tried testing it in my script, I get this error

Couldn't match type ‘RejectedContract’ with ‘ApprovedContract’
        arising from a functional dependency between:
          constraint ‘HasExercise
                        Pending Reject (ContractId ApprovedContract)’
            arising from a use of ‘exerciseCmd’
          instance ‘HasExercise Pending Reject (ContractId RejectedContract)’
            at <no location info>

@Dion_Then
The reason for the type mismatch error “Couldn't match type ‘RejectedContract’ with ‘ApprovedContract’” is that your script is defined as returning ContractId ApprovedContract

test : Script (ContractId ApprovedContract)

whereas the output of the script code (the result of the last command in the script, which is the exercise of the Reject choice on a Pending contract), is the ContractId RejectedContract.
To resolve this for your use case (where you don’t need to reuse the result of the script) I recommend amending the script declaration to return a “unit” and adding pure () as the last line in the script. This way, as you modify your script, you won’t need to worry about the return type of the last command matching the return type in the declaration.

test : Script ()
test = script do

  -- setting of parties
  person1 <- allocatePartyWithHint "alice" (PartyIdHint "alice")
  person2 <- allocatePartyWithHint "bob" (PartyIdHint "bob")
...
  person4 `submit` do
    exerciseCmd pending Reject with signer = person4

  pure ()

As for the implementation of the capability to reject the agreement, the question you need to answer is what would you like to happen as a result of this rejection. In your current implementation you’re creating a RejectedContract, which must be signed by all the same parties as the ApprovedContract. What is the purpose of this RejectedContract? If you’d like any party from finalContract.signatories to be able to reject the Pending contract (in other words invalidate this contract) without further consequences, then the Reject choice just needs to archive the Pending contract without creating any additional or replacement contracts. Since a consuming choice archives the contract, the Reject choice would simply be

    choice Reject: () with
        signer : Party   
      controller signer
        do
          assert $ signer `elem` finalContract.signatories
          return ()
1 Like

Hi @a_putkov
Thanks for the answers!
Maybe to make it more clear, my idea is for any party to be able create a contract.

Afterwards, anyone in the contract can Reject the contract with a rejection note that is just a text containing the reason for rejection.
After being informed for the rejection, the party who created the contract would then be able to Revise the contract with updated contract details and also requesting all parties to sign once again.

Reject Choice:

choice Reject: ContractId Pending with
        signer : Party
        rejectionNote: Text   
      controller signer
        do
          assert $ signer `elem` finalContract.signatories
          create this with
            note = rejectionNote 

Revise Choice (not working):

    choice Revise: ContractId Pending with
        signer : Party
        updatedDocument: Text
        updateNote: Text   
      controller signer
        do
          assert $ signer `elem` finalContract.signatories
          create this with
            document = updatedDocument
            note = updateNote
            alreadySigned = []
            alreadySigned = signer :: alreadySigned

However, I am facing issues with Revise as I am unsure on how to reset the alreadySigned : [Party] and add the party who revised the contract in. Which after, the remaining parties in the contract will all Sign again

With that being said, is this possible and if so, how do I go about this?

Regarding the Revise choice, you’re almost there. Just setting alreadySigned = [signer] when creating the Pending contract inside the Revise choice should work.

    choice Revise: ContractId Pending with
        signer : Party
        updatedDocument: Text
        updateNote: Text   
      controller signer
        do
          assert $ signer `elem` finalContract.signatories
          create this with
            document = updatedDocument
            note = updateNote
            alreadySigned = [signer]

Two more things for you to consider:

  1. Do you want anyone from finalContract.signatories list to be able to create the revised Pending contract or only the party that originally created it? The above implements the former. If you’d like the latter, then I suggest adding a field for the contract originator party to the Pending template, and changing the controller of the Revise choice to the value of that field.
  2. Your latest implementation of the Reject choice just adds a note to the Pending contract. The contract can still continue collecting signatures and nothing prevents this contract from being finalized if the party that rejected the contract decides to sign it. Is this the intended behavior? If you’d like to force the rejected Pending contract to be revised before it can be finalized, then you need to implement additional logic. E.g. add a status field to the Pending template and disallow finalizing the contract if status == "rejected" or something like that.
1 Like

Great, thanks!

As for the points made,

  1. Yes, I would prefer the latter whereby only the party that created can revise it.
  2. Yes, I would like to force the rejected Pending to be revised before it can be finalized.

Could you elaborate more on these 2 approaches?

UPDATE: I figured it out already! Thanks once again @a_putkov :smiley:

Try the following

import DA.Set as Set

data ContractStatus 
  = PendingFullApproval
  | Rejected
  | FullyApproved
  deriving (Eq, Show)

template ApprovedContract
  with
    signatories: Set Party
    status: ContractStatus
  where
    signatory signatories

template Pending
  with
    originator : Party
    finalContract: ApprovedContract
    alreadySigned: Set Party
    document: Text
    note : Text
  where
    signatory alreadySigned
    observer finalContract.signatories

    ensure (Set.member originator alreadySigned && Set.isSubsetOf alreadySigned finalContract.signatories)

    -- The parties who need to sign is the finalContract.signatories with alreadySigned filtered out
    let toSign = Set.difference finalContract.signatories alreadySigned

    choice Pending_Sign : ContractId Pending 
      with 
        signer : Party
      controller signer
        do
          -- Check the controller is in the toSign set, and if they are, sign the Pending contract
          assert $ Set.member signer toSign
          create this with 
            alreadySigned = Set.insert signer alreadySigned
            finalContract.status = PendingFullApproval

    choice Pending_Reject: ContractId Pending 
      with
        signer : Party
        rejectionNote: Text   
      controller originator
        do
          assert $ Set.member signer toSign
          create this with
            note = rejectionNote

    choice Pending_Revise: ContractId Pending 
      with
        updatedDocument: Text
        updateNote: Text   
      controller originator
        do
          assert $ finalContract.status == Rejected
          create this with
            document = updatedDocument
            note = updateNote
            alreadySigned = Set.singleton originator

    choice Pending_Finalize : ContractId ApprovedContract 
      with
        signer : Party
      controller signer
        do
          assert $ finalContract.status /= Rejected
          -- Check that all the required signatories have signed Pending
          assert $ alreadySigned == finalContract.signatories
          create finalContract with
            status = FullyApproved

Note that in the above only a party that hasn’t yet signed the Pending contract can reject it. If you’d like any party from finalContract.signatories to be able to reject the contract, replace assert $ Set.member signer toSign in Pending_Reject choice with assert $ Set.member signer finalContract.signatories