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    /// 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}