canton-daml 0.3.2

Runtime types and Value codecs for canton-codegen-generated Daml bindings.
Documentation
//! Traits the generated template and interface types implement.
//!
//! A [`Contract`] is anything with an on-ledger identity that choices can be
//! exercised on — a template *or* an interface. [`Template`] narrows that to a
//! concrete template (it also has a payload codec), [`Interface`] to an
//! interface (it has a view type), and [`WithKey`] to a keyed template.

use canton_proto::com::daml::ledger::api::v2::Identifier;

use crate::value::{FromValue, ToValue, ValueError};

/// Something with an on-ledger identity that choices can be exercised on: a
/// template or an interface. Its template id is `PACKAGE:MODULE:ENTITY`.
pub trait Contract {
    /// The id (hash) of the package that defines it. Pins the exact version;
    /// prefer [`Contract::template_id`], which uses the package **name**.
    const PACKAGE_ID: &'static str;
    /// The Daml package **name** (e.g. `splice-amulet`).
    const PACKAGE_NAME: &'static str;
    /// The Daml module it is defined in, dotted (e.g. `Splice.Amulet`).
    const MODULE_NAME: &'static str;
    /// The template/interface entity name (e.g. `AmuletRules`, `Holding`).
    const ENTITY_NAME: &'static str;

    /// The on-ledger id. Uses the upgrade-friendly package-**name** form
    /// (`#<package-name>`), so the participant resolves the version vetted under
    /// Smart Contract Upgrade rather than pinning one package id. Falls back to
    /// the package id when no name is known.
    #[must_use]
    fn template_id() -> Identifier {
        let package = if Self::PACKAGE_NAME.is_empty() {
            Self::PACKAGE_ID.to_string()
        } else {
            format!("#{}", Self::PACKAGE_NAME)
        };
        Identifier {
            package_id: package,
            module_name: Self::MODULE_NAME.to_string(),
            entity_name: Self::ENTITY_NAME.to_string(),
        }
    }
}

/// A Daml template: a [`Contract`] with a payload codec. Generated code
/// implements this on each template's payload struct.
pub trait Template: Contract + ToValue + FromValue {
    /// The payload as a Ledger API `Record` — the shape a create command
    /// carries. A template payload is always a record, so this is total;
    /// encoding that in the trait is what keeps `create_command` panic-free.
    fn to_record(&self) -> canton_proto::com::daml::ledger::api::v2::Record;

    /// Decode a `CreatedEvent` (from a transaction stream, an ACS snapshot, or
    /// a `submit-and-wait` response) into this template's typed payload — the
    /// typed **read** path:
    ///
    /// ```ignore
    /// let install: AppInstall = AppInstall::from_created_event(&event)?;
    /// ```
    ///
    /// The event's identity is checked before its payload, so decoding an
    /// event of another template fails with a clear error instead of a
    /// field-shape mismatch, or worse, a clean decode of the wrong contract:
    /// its module and entity, and its package **name**. The package **id** is
    /// deliberately not compared: under Smart Contract Upgrade the ledger may
    /// report any vetted version of the package. A party's stream carries events
    /// from every application it takes part in, and another package can define
    /// the same `Module:Entity` with a payload of the same shape.
    ///
    /// # Errors
    /// Returns [`ValueError`] if the event carries no template id or no package
    /// name, is a different template or comes from a different package,
    /// carries no payload, or its payload does not decode as `Self`.
    fn from_created_event(
        event: &canton_proto::com::daml::ledger::api::v2::CreatedEvent,
    ) -> Result<Self, ValueError> {
        let id = event.template_id.as_ref().ok_or_else(|| {
            ValueError::new(
                "created event carries no template id, so it cannot be checked to be this template",
            )
        })?;
        check_identity::<Self>(
            &id.module_name,
            &id.entity_name,
            &event.package_name,
            &id.package_id,
        )?;
        let record = event
            .create_arguments
            .as_ref()
            .ok_or_else(|| ValueError::new("created event carries no create_arguments payload"))?;
        crate::value::from_record(record)
    }

    /// [`Template::from_created_event`] for the **JSON** Ledger API: decode a
    /// created event from a JSON transaction, ACS entry, or `submit-and-wait`
    /// response into this template's typed payload.
    ///
    /// ```ignore
    /// for event in &response.transaction.events {
    ///     if let Ok(install) = AppInstall::from_json_created_event(event) {
    ///         println!("{}", install.provider);
    ///     }
    /// }
    /// ```
    ///
    /// Both transports carry the same contract for the same bindings, and until
    /// this existed only one of them could read one back as a type. On the JSON
    /// lane a caller had to reach into `event["CreatedEvent"]["createArgument"]`
    /// themselves — and, having done so, had nothing checking that the event was
    /// this template at all. A party's stream carries events for every template
    /// it sees; where two payloads happen to share a field shape, decoding one
    /// as the other succeeds and is wrong.
    ///
    /// Accepts the created event either bare or wrapped, because the API hands
    /// it out several ways: a transaction's `events` are
    /// `{"CreatedEvent": {…}}`, an ACS entry nests `createdEvent`, and the
    /// `events/events-by-contract-id` response nests it under
    /// `{"created": {"createdEvent": {…}}}`. There is no ambiguity — a created
    /// event does not itself have those keys.
    ///
    /// The module and entity of `templateId` and the `packageName` are checked,
    /// and the package id is deliberately not, for the reasons given on
    /// [`Template::from_created_event`].
    ///
    /// # Errors
    /// Returns [`ValueError`] if the value is not a created event, carries no
    /// `templateId` or no package name, is a different template or comes from
    /// a different package, carries no `createArgument`, or its payload does
    /// not deserialize as `Self`.
    fn from_json_created_event(event: &serde_json::Value) -> Result<Self, ValueError>
    where
        Self: serde::de::DeserializeOwned,
    {
        // Unwrap one or two layers: `events-by-contract-id` wraps the created
        // event as `{"created": {"createdEvent": …}}`, so peel `created` first
        // and then the created-event key inside it.
        let inner = event.get("created").unwrap_or(event);
        let created = ["CreatedEvent", "createdEvent"]
            .iter()
            .find_map(|key| inner.get(key))
            .unwrap_or(inner);

        let id = created
            .get("templateId")
            .and_then(serde_json::Value::as_str)
            .ok_or_else(|| {
                ValueError::new(
                    "created event carries no templateId, so it cannot be checked to be this \
                     template",
                )
            })?;
        // `<package>:<Module>:<Entity>`: module names are dotted and entity
        // names carry no colon, so the package part is what precedes the last
        // two. It is whatever version the ledger vetted, and is compared only
        // when it names the package (`#<package-name>`).
        let mut parts = id.rsplitn(3, ':');
        let entity = parts.next().unwrap_or_default();
        let module = parts.next().unwrap_or_default();
        let package = parts.next().unwrap_or_default();
        let package_name = created
            .get("packageName")
            .and_then(serde_json::Value::as_str)
            .unwrap_or_default();
        check_identity::<Self>(module, entity, package_name, package)?;

        let payload = created.get("createArgument").ok_or_else(|| {
            // Singular: the gRPC field is `create_arguments` and this one is
            // not, which is the sort of thing only a real response settles.
            ValueError::new("created event carries no createArgument payload")
        })?;
        serde_json::from_value(payload.clone())
            // The serde error names the field it failed on but not the value,
            // which is the same bargain the gRPC codec strikes.
            .map_err(|error| ValueError::new(format!("payload did not decode: {error}")))
    }
}

/// Check that a created event is `T`: the same module and entity, from a
/// package of the same **name**.
///
/// The package name comes from the event's own `package_name` field, or, when
/// that is empty, from a `#<package-name>` package reference. A package id
/// alone does not say which package it is, so an event that carries no name is
/// refused rather than decoded on trust. A template generated without a package
/// name (`T::PACKAGE_NAME` empty) is checked on module and entity only.
fn check_identity<T: Contract + ?Sized>(
    module: &str,
    entity: &str,
    package_name: &str,
    package_ref: &str,
) -> Result<(), ValueError> {
    if entity != T::ENTITY_NAME || module != T::MODULE_NAME {
        return Err(ValueError::new(format!(
            "event is {module}:{entity}, expected {}:{}",
            T::MODULE_NAME,
            T::ENTITY_NAME,
        )));
    }
    if T::PACKAGE_NAME.is_empty() {
        return Ok(());
    }
    let name = if package_name.is_empty() {
        package_ref.strip_prefix('#').ok_or_else(|| {
            ValueError::new(format!(
                "event of {module}:{entity} carries no package name, so it cannot be checked to \
                 come from `{}`",
                T::PACKAGE_NAME
            ))
        })?
    } else {
        package_name
    };
    if name != T::PACKAGE_NAME {
        return Err(ValueError::new(format!(
            "event of {module}:{entity} is from package `{name}`, expected `{}`",
            T::PACKAGE_NAME
        )));
    }
    Ok(())
}

/// A Daml interface: a [`Contract`] identified by a marker type (held via
/// `ContractId`), with a view type. Choices are exercised through the interface
/// id, so a `ContractId<Interface>` can be exercised without knowing the
/// concrete template.
pub trait Interface: Contract {
    /// The interface's view type — the record returned by an interface view.
    type View: FromValue;
}

/// A template that declares a **contract key**. Generated code implements this
/// on the payload struct of any keyed template, exposing the key's type so a
/// contract can be looked up and choices exercised *by key* (see
/// [`crate::exercise_by_key_command`]).
pub trait WithKey: Template {
    /// The contract key's type (a serializable Daml type).
    type Key: ToValue;
}

#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod json_envelope_tests {
    use super::*;
    use canton_proto::com::daml::ledger::api::v2 as pb;
    use serde::Deserialize;

    // A minimal Template whose JSON payload is one field, enough to drive
    // `from_json_created_event` through each envelope shape the API uses.
    #[derive(Debug, PartialEq, Deserialize)]
    struct Widget {
        owner: String,
    }
    impl Contract for Widget {
        const PACKAGE_ID: &'static str = "pkg";
        const PACKAGE_NAME: &'static str = "app";
        const MODULE_NAME: &'static str = "M";
        const ENTITY_NAME: &'static str = "Widget";
    }
    impl ToValue for Widget {
        fn to_value(&self) -> pb::Value {
            crate::Unit.to_value()
        }
    }
    impl FromValue for Widget {
        fn from_value(_: &pb::Value) -> Result<Self, ValueError> {
            Err(ValueError::new("unused"))
        }
    }
    impl Template for Widget {
        fn to_record(&self) -> pb::Record {
            pb::Record::default()
        }
    }

    fn created() -> serde_json::Value {
        serde_json::json!({
            "templateId": "whatever-package:M:Widget",
            "packageName": "app",
            "createArgument": { "owner": "alice" }
        })
    }

    fn with(patch: &[(&str, serde_json::Value)]) -> serde_json::Value {
        let mut event = created();
        for (key, value) in patch {
            if value.is_null() {
                event.as_object_mut().expect("object").remove(*key);
            } else {
                event[*key] = value.clone();
            }
        }
        event
    }

    fn grpc(package_id: &str, package_name: &str) -> pb::CreatedEvent {
        pb::CreatedEvent {
            template_id: Some(pb::Identifier {
                package_id: package_id.to_string(),
                module_name: "M".to_string(),
                entity_name: "Widget".to_string(),
            }),
            package_name: package_name.to_string(),
            ..Default::default()
        }
    }

    /// Another package that defines `M:Widget` with the same payload shape is
    /// a different template, and decoding its event as this one is refused;
    /// another *version* of the same package (a different id under the same
    /// name) is the same template, and decodes.
    #[test]
    fn the_package_name_is_checked_and_the_package_id_is_not() {
        use serde_json::json;
        assert!(
            Widget::from_json_created_event(&with(&[(
                "templateId",
                json!("another-version:M:Widget")
            )]))
            .is_ok()
        );
        let foreign =
            Widget::from_json_created_event(&with(&[("packageName", json!("someone-elses-app"))]))
                .expect_err("a foreign package must not decode");
        assert!(
            foreign.to_string().contains("someone-elses-app"),
            "{foreign}"
        );

        // A `#<package-name>` reference names the package too.
        let by_name = with(&[
            ("templateId", json!("#app:M:Widget")),
            ("packageName", serde_json::Value::Null),
        ]);
        assert!(Widget::from_json_created_event(&by_name).is_ok());
        let by_other_name = with(&[
            ("templateId", json!("#other:M:Widget")),
            ("packageName", serde_json::Value::Null),
        ]);
        assert!(Widget::from_json_created_event(&by_other_name).is_err());

        // gRPC: the name rides in `package_name`; the id is whatever was vetted.
        let error = Widget::from_created_event(&grpc("bbbb", "someone-elses-app"))
            .expect_err("a foreign package must not decode");
        assert!(error.to_string().contains("someone-elses-app"), "{error}");
    }

    /// An event the identity check cannot be run on is refused, not decoded on
    /// trust: no template id, or a package id with no package name beside it.
    #[test]
    fn an_event_whose_identity_cannot_be_checked_is_refused() {
        let no_template = with(&[("templateId", serde_json::Value::Null)]);
        assert!(Widget::from_json_created_event(&no_template).is_err());
        let no_name = with(&[("packageName", serde_json::Value::Null)]);
        let error = Widget::from_json_created_event(&no_name).expect_err("no package name");
        assert!(error.to_string().contains("no package name"), "{error}");

        let mut event = grpc("bbbb", "app");
        event.template_id = None;
        let error = Widget::from_created_event(&event).expect_err("no template id");
        assert!(error.to_string().contains("no template id"), "{error}");
        let error = Widget::from_created_event(&grpc("bbbb", "")).expect_err("no package name");
        assert!(error.to_string().contains("no package name"), "{error}");
    }

    #[test]
    fn every_envelope_shape_the_json_api_uses_decodes() {
        let want = Widget {
            owner: "alice".to_string(),
        };

        // Bare, transaction-event `{"CreatedEvent": …}`, ACS-entry
        // `{"createdEvent": …}`, and the `events-by-contract-id` double wrap
        // `{"created": {"createdEvent": …}}` — the last one used to error with
        // "carries no createArgument" because only the inner key was peeled.
        let shapes = [
            created(),
            serde_json::json!({ "CreatedEvent": created() }),
            serde_json::json!({ "createdEvent": created() }),
            serde_json::json!({ "created": { "createdEvent": created() } }),
        ];
        for (i, shape) in shapes.iter().enumerate() {
            assert_eq!(
                Widget::from_json_created_event(shape).expect("shape decodes"),
                want,
                "envelope shape {i} did not decode"
            );
        }
    }
}