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 /// The event's identity is checked before its payload, so decoding an
61 /// event of another template fails with a clear error instead of a
62 /// field-shape mismatch, or worse, a clean decode of the wrong contract:
63 /// its module and entity, and its package **name**. The package **id** is
64 /// deliberately not compared: under Smart Contract Upgrade the ledger may
65 /// report any vetted version of the package. A party's stream carries events
66 /// from every application it takes part in, and another package can define
67 /// the same `Module:Entity` with a payload of the same shape.
68 ///
69 /// # Errors
70 /// Returns [`ValueError`] if the event carries no template id or no package
71 /// name, is a different template or comes from a different package,
72 /// carries no payload, or its payload does not decode as `Self`.
73 fn from_created_event(
74 event: &canton_proto::com::daml::ledger::api::v2::CreatedEvent,
75 ) -> Result<Self, ValueError> {
76 let id = event.template_id.as_ref().ok_or_else(|| {
77 ValueError::new(
78 "created event carries no template id, so it cannot be checked to be this template",
79 )
80 })?;
81 check_identity::<Self>(
82 &id.module_name,
83 &id.entity_name,
84 &event.package_name,
85 &id.package_id,
86 )?;
87 let record = event
88 .create_arguments
89 .as_ref()
90 .ok_or_else(|| ValueError::new("created event carries no create_arguments payload"))?;
91 crate::value::from_record(record)
92 }
93
94 /// [`Template::from_created_event`] for the **JSON** Ledger API: decode a
95 /// created event from a JSON transaction, ACS entry, or `submit-and-wait`
96 /// response into this template's typed payload.
97 ///
98 /// ```ignore
99 /// for event in &response.transaction.events {
100 /// if let Ok(install) = AppInstall::from_json_created_event(event) {
101 /// println!("{}", install.provider);
102 /// }
103 /// }
104 /// ```
105 ///
106 /// Both transports carry the same contract for the same bindings, and until
107 /// this existed only one of them could read one back as a type. On the JSON
108 /// lane a caller had to reach into `event["CreatedEvent"]["createArgument"]`
109 /// themselves — and, having done so, had nothing checking that the event was
110 /// this template at all. A party's stream carries events for every template
111 /// it sees; where two payloads happen to share a field shape, decoding one
112 /// as the other succeeds and is wrong.
113 ///
114 /// Accepts the created event either bare or wrapped, because the API hands
115 /// it out several ways: a transaction's `events` are
116 /// `{"CreatedEvent": {…}}`, an ACS entry nests `createdEvent`, and the
117 /// `events/events-by-contract-id` response nests it under
118 /// `{"created": {"createdEvent": {…}}}`. There is no ambiguity — a created
119 /// event does not itself have those keys.
120 ///
121 /// The module and entity of `templateId` and the `packageName` are checked,
122 /// and the package id is deliberately not, for the reasons given on
123 /// [`Template::from_created_event`].
124 ///
125 /// # Errors
126 /// Returns [`ValueError`] if the value is not a created event, carries no
127 /// `templateId` or no package name, is a different template or comes from
128 /// a different package, carries no `createArgument`, or its payload does
129 /// not deserialize as `Self`.
130 fn from_json_created_event(event: &serde_json::Value) -> Result<Self, ValueError>
131 where
132 Self: serde::de::DeserializeOwned,
133 {
134 // Unwrap one or two layers: `events-by-contract-id` wraps the created
135 // event as `{"created": {"createdEvent": …}}`, so peel `created` first
136 // and then the created-event key inside it.
137 let inner = event.get("created").unwrap_or(event);
138 let created = ["CreatedEvent", "createdEvent"]
139 .iter()
140 .find_map(|key| inner.get(key))
141 .unwrap_or(inner);
142
143 let id = created
144 .get("templateId")
145 .and_then(serde_json::Value::as_str)
146 .ok_or_else(|| {
147 ValueError::new(
148 "created event carries no templateId, so it cannot be checked to be this \
149 template",
150 )
151 })?;
152 // `<package>:<Module>:<Entity>`: module names are dotted and entity
153 // names carry no colon, so the package part is what precedes the last
154 // two. It is whatever version the ledger vetted, and is compared only
155 // when it names the package (`#<package-name>`).
156 let mut parts = id.rsplitn(3, ':');
157 let entity = parts.next().unwrap_or_default();
158 let module = parts.next().unwrap_or_default();
159 let package = parts.next().unwrap_or_default();
160 let package_name = created
161 .get("packageName")
162 .and_then(serde_json::Value::as_str)
163 .unwrap_or_default();
164 check_identity::<Self>(module, entity, package_name, package)?;
165
166 let payload = created.get("createArgument").ok_or_else(|| {
167 // Singular: the gRPC field is `create_arguments` and this one is
168 // not, which is the sort of thing only a real response settles.
169 ValueError::new("created event carries no createArgument payload")
170 })?;
171 serde_json::from_value(payload.clone())
172 // The serde error names the field it failed on but not the value,
173 // which is the same bargain the gRPC codec strikes.
174 .map_err(|error| ValueError::new(format!("payload did not decode: {error}")))
175 }
176}
177
178/// Check that a created event is `T`: the same module and entity, from a
179/// package of the same **name**.
180///
181/// The package name comes from the event's own `package_name` field, or, when
182/// that is empty, from a `#<package-name>` package reference. A package id
183/// alone does not say which package it is, so an event that carries no name is
184/// refused rather than decoded on trust. A template generated without a package
185/// name (`T::PACKAGE_NAME` empty) is checked on module and entity only.
186fn check_identity<T: Contract + ?Sized>(
187 module: &str,
188 entity: &str,
189 package_name: &str,
190 package_ref: &str,
191) -> Result<(), ValueError> {
192 if entity != T::ENTITY_NAME || module != T::MODULE_NAME {
193 return Err(ValueError::new(format!(
194 "event is {module}:{entity}, expected {}:{}",
195 T::MODULE_NAME,
196 T::ENTITY_NAME,
197 )));
198 }
199 if T::PACKAGE_NAME.is_empty() {
200 return Ok(());
201 }
202 let name = if package_name.is_empty() {
203 package_ref.strip_prefix('#').ok_or_else(|| {
204 ValueError::new(format!(
205 "event of {module}:{entity} carries no package name, so it cannot be checked to \
206 come from `{}`",
207 T::PACKAGE_NAME
208 ))
209 })?
210 } else {
211 package_name
212 };
213 if name != T::PACKAGE_NAME {
214 return Err(ValueError::new(format!(
215 "event of {module}:{entity} is from package `{name}`, expected `{}`",
216 T::PACKAGE_NAME
217 )));
218 }
219 Ok(())
220}
221
222/// A Daml interface: a [`Contract`] identified by a marker type (held via
223/// `ContractId`), with a view type. Choices are exercised through the interface
224/// id, so a `ContractId<Interface>` can be exercised without knowing the
225/// concrete template.
226pub trait Interface: Contract {
227 /// The interface's view type — the record returned by an interface view.
228 type View: FromValue;
229}
230
231/// A template that declares a **contract key**. Generated code implements this
232/// on the payload struct of any keyed template, exposing the key's type so a
233/// contract can be looked up and choices exercised *by key* (see
234/// [`crate::exercise_by_key_command`]).
235pub trait WithKey: Template {
236 /// The contract key's type (a serializable Daml type).
237 type Key: ToValue;
238}
239
240#[cfg(test)]
241#[allow(clippy::expect_used, clippy::unwrap_used)]
242mod json_envelope_tests {
243 use super::*;
244 use canton_proto::com::daml::ledger::api::v2 as pb;
245 use serde::Deserialize;
246
247 // A minimal Template whose JSON payload is one field, enough to drive
248 // `from_json_created_event` through each envelope shape the API uses.
249 #[derive(Debug, PartialEq, Deserialize)]
250 struct Widget {
251 owner: String,
252 }
253 impl Contract for Widget {
254 const PACKAGE_ID: &'static str = "pkg";
255 const PACKAGE_NAME: &'static str = "app";
256 const MODULE_NAME: &'static str = "M";
257 const ENTITY_NAME: &'static str = "Widget";
258 }
259 impl ToValue for Widget {
260 fn to_value(&self) -> pb::Value {
261 crate::Unit.to_value()
262 }
263 }
264 impl FromValue for Widget {
265 fn from_value(_: &pb::Value) -> Result<Self, ValueError> {
266 Err(ValueError::new("unused"))
267 }
268 }
269 impl Template for Widget {
270 fn to_record(&self) -> pb::Record {
271 pb::Record::default()
272 }
273 }
274
275 fn created() -> serde_json::Value {
276 serde_json::json!({
277 "templateId": "whatever-package:M:Widget",
278 "packageName": "app",
279 "createArgument": { "owner": "alice" }
280 })
281 }
282
283 fn with(patch: &[(&str, serde_json::Value)]) -> serde_json::Value {
284 let mut event = created();
285 for (key, value) in patch {
286 if value.is_null() {
287 event.as_object_mut().expect("object").remove(*key);
288 } else {
289 event[*key] = value.clone();
290 }
291 }
292 event
293 }
294
295 fn grpc(package_id: &str, package_name: &str) -> pb::CreatedEvent {
296 pb::CreatedEvent {
297 template_id: Some(pb::Identifier {
298 package_id: package_id.to_string(),
299 module_name: "M".to_string(),
300 entity_name: "Widget".to_string(),
301 }),
302 package_name: package_name.to_string(),
303 ..Default::default()
304 }
305 }
306
307 /// Another package that defines `M:Widget` with the same payload shape is
308 /// a different template, and decoding its event as this one is refused;
309 /// another *version* of the same package (a different id under the same
310 /// name) is the same template, and decodes.
311 #[test]
312 fn the_package_name_is_checked_and_the_package_id_is_not() {
313 use serde_json::json;
314 assert!(
315 Widget::from_json_created_event(&with(&[(
316 "templateId",
317 json!("another-version:M:Widget")
318 )]))
319 .is_ok()
320 );
321 let foreign =
322 Widget::from_json_created_event(&with(&[("packageName", json!("someone-elses-app"))]))
323 .expect_err("a foreign package must not decode");
324 assert!(
325 foreign.to_string().contains("someone-elses-app"),
326 "{foreign}"
327 );
328
329 // A `#<package-name>` reference names the package too.
330 let by_name = with(&[
331 ("templateId", json!("#app:M:Widget")),
332 ("packageName", serde_json::Value::Null),
333 ]);
334 assert!(Widget::from_json_created_event(&by_name).is_ok());
335 let by_other_name = with(&[
336 ("templateId", json!("#other:M:Widget")),
337 ("packageName", serde_json::Value::Null),
338 ]);
339 assert!(Widget::from_json_created_event(&by_other_name).is_err());
340
341 // gRPC: the name rides in `package_name`; the id is whatever was vetted.
342 let error = Widget::from_created_event(&grpc("bbbb", "someone-elses-app"))
343 .expect_err("a foreign package must not decode");
344 assert!(error.to_string().contains("someone-elses-app"), "{error}");
345 }
346
347 /// An event the identity check cannot be run on is refused, not decoded on
348 /// trust: no template id, or a package id with no package name beside it.
349 #[test]
350 fn an_event_whose_identity_cannot_be_checked_is_refused() {
351 let no_template = with(&[("templateId", serde_json::Value::Null)]);
352 assert!(Widget::from_json_created_event(&no_template).is_err());
353 let no_name = with(&[("packageName", serde_json::Value::Null)]);
354 let error = Widget::from_json_created_event(&no_name).expect_err("no package name");
355 assert!(error.to_string().contains("no package name"), "{error}");
356
357 let mut event = grpc("bbbb", "app");
358 event.template_id = None;
359 let error = Widget::from_created_event(&event).expect_err("no template id");
360 assert!(error.to_string().contains("no template id"), "{error}");
361 let error = Widget::from_created_event(&grpc("bbbb", "")).expect_err("no package name");
362 assert!(error.to_string().contains("no package name"), "{error}");
363 }
364
365 #[test]
366 fn every_envelope_shape_the_json_api_uses_decodes() {
367 let want = Widget {
368 owner: "alice".to_string(),
369 };
370
371 // Bare, transaction-event `{"CreatedEvent": …}`, ACS-entry
372 // `{"createdEvent": …}`, and the `events-by-contract-id` double wrap
373 // `{"created": {"createdEvent": …}}` — the last one used to error with
374 // "carries no createArgument" because only the inner key was peeled.
375 let shapes = [
376 created(),
377 serde_json::json!({ "CreatedEvent": created() }),
378 serde_json::json!({ "createdEvent": created() }),
379 serde_json::json!({ "created": { "createdEvent": created() } }),
380 ];
381 for (i, shape) in shapes.iter().enumerate() {
382 assert_eq!(
383 Widget::from_json_created_event(shape).expect("shape decodes"),
384 want,
385 "envelope shape {i} did not decode"
386 );
387 }
388 }
389}