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.
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 ()
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:
- 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. - 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 ifstatus == "rejected"
or something like that.
Great, thanks!
As for the points made,
- Yes, I would prefer the latter whereby only the party that created can revise it.
- 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
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