canton_daml/template.rs
1//! Traits the generated template and interface types implement.
2//!
3//! A [`Contract`] is anything with an on-ledger identity that choices can be
4//! exercised on — a template *or* an interface. [`Template`] narrows that to a
5//! concrete template (it also has a payload codec), [`Interface`] to an
6//! interface (it has a view type), and [`WithKey`] to a keyed template.
7
8use canton_proto::com::daml::ledger::api::v2::Identifier;
9
10use crate::value::{FromValue, ToValue, ValueError};
11
12/// Something with an on-ledger identity that choices can be exercised on: a
13/// template or an interface. Its template id is `PACKAGE:MODULE:ENTITY`.
14pub trait Contract {
15 /// The id (hash) of the package that defines it. Pins the exact version;
16 /// prefer [`Contract::template_id`], which uses the package **name**.
17 const PACKAGE_ID: &'static str;
18 /// The Daml package **name** (e.g. `splice-amulet`).
19 const PACKAGE_NAME: &'static str;
20 /// The Daml module it is defined in, dotted (e.g. `Splice.Amulet`).
21 const MODULE_NAME: &'static str;
22 /// The template/interface entity name (e.g. `AmuletRules`, `Holding`).
23 const ENTITY_NAME: &'static str;
24
25 /// The on-ledger id. Uses the upgrade-friendly package-**name** form
26 /// (`#<package-name>`), so the participant resolves the version vetted under
27 /// Smart Contract Upgrade rather than pinning one package id. Falls back to
28 /// the package id when no name is known.
29 #[must_use]
30 fn template_id() -> Identifier {
31 let package = if Self::PACKAGE_NAME.is_empty() {
32 Self::PACKAGE_ID.to_string()
33 } else {
34 format!("#{}", Self::PACKAGE_NAME)
35 };
36 Identifier {
37 package_id: package,
38 module_name: Self::MODULE_NAME.to_string(),
39 entity_name: Self::ENTITY_NAME.to_string(),
40 }
41 }
42}
43
44/// A Daml template: a [`Contract`] with a payload codec. Generated code
45/// implements this on each template's payload struct.
46pub trait Template: Contract + ToValue + FromValue {
47 /// The payload as a Ledger API `Record` — the shape a create command
48 /// carries. A template payload is always a record, so this is total;
49 /// encoding that in the trait is what keeps `create_command` panic-free.
50 fn to_record(&self) -> canton_proto::com::daml::ledger::api::v2::Record;
51
52 /// Decode a `CreatedEvent` (from a transaction stream, an ACS snapshot, or
53 /// a `submit-and-wait` response) into this template's typed payload — the
54 /// typed **read** path:
55 ///
56 /// ```ignore
57 /// let install: AppInstall = AppInstall::from_created_event(&event)?;
58 /// ```
59 ///
60 /// When the event carries a template id, its module/entity are checked
61 /// first, so decoding an event of the wrong template fails with a clear
62 /// error instead of a field-shape mismatch. The package id is deliberately
63 /// **not** compared: under Smart Contract Upgrade the ledger may report any
64 /// vetted version of the package.
65 ///
66 /// # Errors
67 /// Returns [`ValueError`] if the event is a different template, carries no
68 /// payload, or its payload does not decode as `Self`.
69 fn from_created_event(
70 event: &canton_proto::com::daml::ledger::api::v2::CreatedEvent,
71 ) -> Result<Self, ValueError> {
72 if let Some(id) = &event.template_id
73 && (id.entity_name != Self::ENTITY_NAME || id.module_name != Self::MODULE_NAME)
74 {
75 return Err(ValueError::new(format!(
76 "event is {}:{}, expected {}:{}",
77 id.module_name,
78 id.entity_name,
79 Self::MODULE_NAME,
80 Self::ENTITY_NAME,
81 )));
82 }
83 let record = event
84 .create_arguments
85 .as_ref()
86 .ok_or_else(|| ValueError::new("created event carries no create_arguments payload"))?;
87 crate::value::from_record(record)
88 }
89
90 /// [`Template::from_created_event`] for the **JSON** Ledger API: decode a
91 /// created event from a JSON transaction, ACS entry, or `submit-and-wait`
92 /// response into this template's typed payload.
93 ///
94 /// ```ignore
95 /// for event in &response.transaction.events {
96 /// if let Ok(install) = AppInstall::from_json_created_event(event) {
97 /// println!("{}", install.provider);
98 /// }
99 /// }
100 /// ```
101 ///
102 /// Both transports carry the same contract for the same bindings, and until
103 /// this existed only one of them could read one back as a type. On the JSON
104 /// lane a caller had to reach into `event["CreatedEvent"]["createArgument"]`
105 /// themselves — and, having done so, had nothing checking that the event was
106 /// this template at all. A party's stream carries events for every template
107 /// it sees; where two payloads happen to share a field shape, decoding one
108 /// as the other succeeds and is wrong.
109 ///
110 /// Accepts the created event either bare or wrapped, because the API hands
111 /// it out several ways: a transaction's `events` are
112 /// `{"CreatedEvent": {…}}`, an ACS entry nests `createdEvent`, and the
113 /// `events/events-by-contract-id` response nests it under
114 /// `{"created": {"createdEvent": {…}}}`. There is no ambiguity — a created
115 /// event does not itself have those keys.
116 ///
117 /// The module and entity of `templateId` are checked, and the package id is
118 /// deliberately not, for the reason given on
119 /// [`Template::from_created_event`].
120 ///
121 /// # Errors
122 /// Returns [`ValueError`] if the value is not a created event, is a
123 /// different template, carries no `createArgument`, or its payload does not
124 /// deserialize as `Self`.
125 fn from_json_created_event(event: &serde_json::Value) -> Result<Self, ValueError>
126 where
127 Self: serde::de::DeserializeOwned,
128 {
129 // Unwrap one or two layers: `events-by-contract-id` wraps the created
130 // event as `{"created": {"createdEvent": …}}`, so peel `created` first
131 // and then the created-event key inside it.
132 let inner = event.get("created").unwrap_or(event);
133 let created = ["CreatedEvent", "createdEvent"]
134 .iter()
135 .find_map(|key| inner.get(key))
136 .unwrap_or(inner);
137
138 if let Some(id) = created
139 .get("templateId")
140 .and_then(serde_json::Value::as_str)
141 {
142 // `<package>:<Module>:<Entity>` — the package part is whatever
143 // version the ledger vetted, so only the last two are compared.
144 let mut parts = id.rsplit(':');
145 let entity = parts.next().unwrap_or_default();
146 let module = parts.next().unwrap_or_default();
147 if entity != Self::ENTITY_NAME || module != Self::MODULE_NAME {
148 return Err(ValueError::new(format!(
149 "event is {module}:{entity}, expected {}:{}",
150 Self::MODULE_NAME,
151 Self::ENTITY_NAME,
152 )));
153 }
154 }
155
156 let payload = created.get("createArgument").ok_or_else(|| {
157 // Singular: the gRPC field is `create_arguments` and this one is
158 // not, which is the sort of thing only a real response settles.
159 ValueError::new("created event carries no createArgument payload")
160 })?;
161 serde_json::from_value(payload.clone())
162 // The serde error names the field it failed on but not the value,
163 // which is the same bargain the gRPC codec strikes.
164 .map_err(|error| ValueError::new(format!("payload did not decode: {error}")))
165 }
166}
167
168/// A Daml interface: a [`Contract`] identified by a marker type (held via
169/// `ContractId`), with a view type. Choices are exercised through the interface
170/// id, so a `ContractId<Interface>` can be exercised without knowing the
171/// concrete template.
172pub trait Interface: Contract {
173 /// The interface's view type — the record returned by an interface view.
174 type View: FromValue;
175}
176
177/// A template that declares a **contract key**. Generated code implements this
178/// on the payload struct of any keyed template, exposing the key's type so a
179/// contract can be looked up and choices exercised *by key* (see
180/// [`crate::exercise_by_key_command`]).
181pub trait WithKey: Template {
182 /// The contract key's type (a serializable Daml type).
183 type Key: ToValue;
184}
185
186#[cfg(test)]
187#[allow(clippy::expect_used, clippy::unwrap_used)]
188mod json_envelope_tests {
189 use super::*;
190 use canton_proto::com::daml::ledger::api::v2 as pb;
191 use serde::Deserialize;
192
193 // A minimal Template whose JSON payload is one field, enough to drive
194 // `from_json_created_event` through each envelope shape the API uses.
195 #[derive(Debug, PartialEq, Deserialize)]
196 struct Widget {
197 owner: String,
198 }
199 impl Contract for Widget {
200 const PACKAGE_ID: &'static str = "pkg";
201 const PACKAGE_NAME: &'static str = "app";
202 const MODULE_NAME: &'static str = "M";
203 const ENTITY_NAME: &'static str = "Widget";
204 }
205 impl ToValue for Widget {
206 fn to_value(&self) -> pb::Value {
207 crate::Unit.to_value()
208 }
209 }
210 impl FromValue for Widget {
211 fn from_value(_: &pb::Value) -> Result<Self, ValueError> {
212 Err(ValueError::new("unused"))
213 }
214 }
215 impl Template for Widget {
216 fn to_record(&self) -> pb::Record {
217 pb::Record::default()
218 }
219 }
220
221 fn created() -> serde_json::Value {
222 serde_json::json!({
223 "templateId": "whatever-package:M:Widget",
224 "createArgument": { "owner": "alice" }
225 })
226 }
227
228 #[test]
229 fn every_envelope_shape_the_json_api_uses_decodes() {
230 let want = Widget {
231 owner: "alice".to_string(),
232 };
233
234 // Bare, transaction-event `{"CreatedEvent": …}`, ACS-entry
235 // `{"createdEvent": …}`, and the `events-by-contract-id` double wrap
236 // `{"created": {"createdEvent": …}}` — the last one used to error with
237 // "carries no createArgument" because only the inner key was peeled.
238 let shapes = [
239 created(),
240 serde_json::json!({ "CreatedEvent": created() }),
241 serde_json::json!({ "createdEvent": created() }),
242 serde_json::json!({ "created": { "createdEvent": created() } }),
243 ];
244 for (i, shape) in shapes.iter().enumerate() {
245 assert_eq!(
246 Widget::from_json_created_event(shape).expect("shape decodes"),
247 want,
248 "envelope shape {i} did not decode"
249 );
250 }
251 }
252}