Hi @David_Martins, welcome to the forum!
TL;DR: You could make it work in some cases but you can’t catch all errors making this a pretty confusing feature. In addition to that contract key uniqueness is very tricky to implement in distributed settings which is why Canton does not provide uniqueness. So overall, I don’t think it makes sense to implement this.
Now to some code:
As a first attempt, we can try the following
exception DuplicateKey
where
message "DuplicateKey"
createWithKey : forall t k. (Template t, TemplateKey t k) => t -> Update (ContractId t)
createWithKey c = do
r <- lookupByKey @t (key c)
case r of
None -> create c
Some _ -> throw DuplicateKey
Looks easy enough right?
Now let’s use this in a somewhat silly example where we try to find the first key that does not yet exist and create a contract with that key:
template WithKey
with
p : Party
v : Int
where
signatory p
key this : WithKey
maintainer key.p
template Helper
with
p : Party
where
signatory p
choice FirstFree : ContractId WithKey
controller p
do firstFree p
firstFree p = go p 0
go p i =
try createWithKey (WithKey p i)
catch
DuplicateKey -> go p (i + 1)
This works great as we can see in a simple example
test = do
p <- allocateParty "p"
submit p $ createAndExerciseCmd (Helper p) FirstFree
submit p $ createAndExerciseCmd (Helper p) FirstFree
This will create contracts with key (p, 0)
and (p, 1)
exactly as we expect.
Now to make this example even sillier, let’s delegate the creation to another party:
with
p : Party
o : Party
where
signatory p
observer o
nonconsuming choice FirstFreeDelegate : ContractId WithKey
controller o
do firstFree p
Seems to work great in a simple test script
testDelegate = do
p <- allocateParty "p"
o <- allocateParty "o"
cid <- submit p $ createCmd (Delegate p o)
submit o $ exerciseCmd cid FirstFreeDelegate
However, now let’s add another call to FirstFreeDelegate
at the end to create a second contracts. Suddenly, things don’t look so good anymore
Scenario execution failed on commit at Main:67:3:
Attempt to fetch, lookup or exercise a key associated with a contract not visible to the committer.
Oh no what happened?!
This is a subtlety when it comes to contract keys in Daml: The submitting party (o
in this example), must be a stakeholder on the contract for lookupByKey
to succeed. However, this is not the case here: p
is the only stakeholder.
Now why do we have this seemingly arbitrary restriction?
The property we want to provide for contract keys is that assuming the key lookup is “allowed” (meaning well authorized and the submitter) is a stakeholder it can always succeed.
What happens if we drop the stakeholder restriction? Remember that privacy is a fundamental feature in Daml. If we do not have the stakeholder restriction then we get into a situation where it might be the case that a contract with the given key exists, however the participant you submit to does not (yet) know about this contract so it has no choice but to return a negative lookup. That leads to pretty confusing semantics around contract keys so we instead went for the additional restriction where we can always correctly resolve a key if you are a stakeholder and fail the submission otherwise.
Now to summarize, as seen above, you can implement something that handles duplicate keys in some form but it has some issues and doesn’t work as you might expect it to work. Adding to that the fact that we don’t support contract key uniqueness (and therefore duplicate key errors) in Canton, there are currently no plans to add an exception for this.