Skip to main content

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}