Contract Keys
A contract ID changes every time a contract is archived and recreated, for example when a choice updates it. A contract key gives a contract a stable identity built from values it holds, like a bank and an account number, so a choice can find the current contract without tracking its ID.
The examples on this page are in main/daml/Account.vy and
test/daml/AccountTest.daml, and every test passes.
A key needs two clauses:
key: tuple[Party, str] = (bank, number) maintainer: Party = key[0]keyis computed from template fields. It needs a type annotation (DF1213).maintainernames the parties responsible for the key. It’s computed fromkeyonly, never from template fields (DF1236).- Each needs the other (
DF1210,DF1211).
Requirements
Section titled “Requirements”Contract keys compile only with Daml SDK 3.5 or later and the Daml-LF 2.3 target.
Add the target to the daml.yaml of every package that declares or uses keys:
sdk-version: 3.5.2build-options: - --target=2.3Without it, forge dar stops with DF1234 before calling damlc.
In Daml 3.5, keys aren’t unique: two active contracts can have the same key.
fetch_by_key and lookup_by_key return one of them, and lookup_all_by_key
returns all of them.
Declaring a key
Section titled “Declaring a key”@templateclass Account: bank: Party owner: Party number: str
signatory: Party = bank observer: Party = owner key: tuple[Party, str] = (bank, number) maintainer: Party = key[0]
@choice(controller=bank, consuming="nonconsuming") def GetOwner(self) -> Party: return self.ownertemplate Account with bank : Party owner : Party number : Text where signatory bank observer owner key (bank, number) : (Party, Text) maintainer key._1key[0] becomes key._1, Daml’s first tuple element. A key can also be a single
field (key: Party = owner) or a struct (key: AccountKey = AccountKey(bank=bank, number=number)).
Using a key
Section titled “Using a key”These builtins come from forge.builtins.daml_lang, like fetch:
.vy |
Daml | Returns |
|---|---|---|
lookup_by_key(key) |
lookupByKey |
Optional[ContractId[T]] |
fetch_by_key(key) |
fetchByKey |
tuple[ContractId[T], T] |
lookup_all_by_key(key) |
lookupAllByKey |
list[tuple[ContractId[T], T]] |
lookup_n_by_key(limit, key) |
lookupNByKey |
at most limit matches, same type as above |
exercise_by_key(T, key, T.Choice(...)) |
exerciseByKey @T |
the choice’s result |
visible_by_key(T, key) |
visibleByKey @T |
bool |
Daml finds the template from the type of the result. For the first four, give the
variable that receives the result a type annotation, as in the example below.
Without one, damlc can’t tell which template you mean. exercise_by_key and
visible_by_key return something that doesn’t mention the template, so they take
it as their first argument (DF1235). Their choice argument is written as
Template.Choice(...), because there is no contract ID to call it on.
lookup_all_by_key and lookup_n_by_key need DA.ContractKeys in Daml. The
compiler adds that import for you.
A second template in the same file uses every builtin:
@templateclass Registry: bank: Party
signatory: Party = bank
@choice(controller=bank, consuming="nonconsuming") def Find(self, number: str) -> Optional[Party]: found: Optional[ContractId[Account]] = lookup_by_key((self.bank, number)) if found is None: return None return exercise_by_key(Account, (self.bank, number), Account.GetOwner())
@choice(controller=bank, consuming="nonconsuming") def Owner(self, number: str) -> Party: pair: tuple[ContractId[Account], Account] = fetch_by_key((self.bank, number)) return pair[1].owner
@choice(controller=bank, consuming="nonconsuming") def Count(self, number: str) -> int: rows: list[tuple[ContractId[Account], Account]] = lookup_all_by_key((self.bank, number)) return len(rows)
@choice(controller=bank, consuming="nonconsuming") def CountFirst(self, number: str) -> int: rows: list[tuple[ContractId[Account], Account]] = lookup_n_by_key(1, (self.bank, number)) return len(rows)
@choice(controller=bank, consuming="nonconsuming") def Visible(self, number: str) -> bool: return visible_by_key(Account, (self.bank, number))The file starts with these imports:
from typing import Optional
from forge.builtins.daml_lang import ( exercise_by_key, fetch_by_key, lookup_all_by_key, lookup_by_key, lookup_n_by_key, visible_by_key,)from forge.models import ContractId, Party, choice, templateThe annotated assignments become typed bindings, and the template argument becomes a Daml type application:
(pair : (ContractId Account, Account)) <- fetchByKey (self.bank, number) visibleByKey @Account (self.bank, number)A Script test creates two accounts with the same key, which Daml 3.5 allows:
submit bank do createCmd Account with bank; owner = alice; number = "1" submit bank do createCmd Account with bank; owner = bob; number = "1"
owner <- submit bank do exerciseCmd registry Owner with number = "1" assert (owner == alice || owner == bob)
found <- submit bank do exerciseCmd registry Find with number = "1" assert (found == Some owner)
count <- submit bank do exerciseCmd registry Count with number = "1" assert (count == 2)
first <- submit bank do exerciseCmd registry CountFirst with number = "1" assert (first == 1)fetch_by_key on a key with no contract fails the transaction:
submitMustFail bank do exerciseCmd registry Owner with number = "2"Lookup order
Section titled “Lookup order”Because keys aren’t unique, a lookup can match several contracts. Daml returns them in this order:
- Contracts created earlier in the same transaction, most recent first.
- Contracts disclosed to the submitter, in the order the command lists them.
- Other contracts the submitting parties can see.
fetch_by_key, lookup_by_key, and exercise_by_key use the first match.
lookup_n_by_key and lookup_all_by_key return matches in the same order. The
lookups are fastest when a key matches at most one contract, so design keys that
your application keeps unique, such as account or invoice numbers.
Authorization
Section titled “Authorization”fetch_by_keyneeds the authority of one of the contract’s stakeholders, likefetch. It fails the transaction if no visible contract has the key.lookup_by_keyneeds the authority of every maintainer, and returnsNonewhen no contract matches.exercise_by_keyneeds the authority of the choice’s controllers, likeexercise.
Registry has the bank as its only signatory and the bank is the maintainer of
every Account key, so each of its choices has the authority it needs.
Keys in Daml Script
Section titled “Keys in Daml Script”Daml Script can query and exercise by key from outside a transaction:
script_key_test : Script ()script_key_test = do bank <- allocateParty "Bank" alice <- allocateParty "Alice" submit bank do createCmd Account with bank; owner = alice; number = "7" Some (_, account) <- queryByKey @Account bank (bank, "7") assert (account.owner == alice) owner <- submit bank do exerciseByKeyCmd @Account (bank, "7") GetOwner assert (owner == alice) rows <- queryNByKey @Account bank 10 (bank, "7") assert (length rows == 1)| Function | Returns |
|---|---|
queryByKey @T party key |
Optional (ContractId T, T), the first match visible to party |
queryNByKey @T party n key |
up to n matches |
exerciseByKeyCmd @T key choice |
a command for submit, like exerciseCmd |
daml/AccountTest.daml:script_key_test: ok, 1 active contracts, 2 transactions.daml/AccountTest.daml:keys_test: ok, 3 active contracts, 11 transactions.Next steps
Section titled “Next steps”Interfaces shows how different templates can share one set of choices, so code can work with any of them.