use canton_core::{Error, Result};
use canton_daml::ContractId;
#[allow(clippy::struct_field_names)]
#[derive(Clone, Debug)]
pub struct Contract<T> {
contract_id: ContractId<T>,
payload: T,
created_at_offset: i64,
archived_at_offset: Option<i64>,
created_effective_at: Option<time::OffsetDateTime>,
archived_effective_at: Option<time::OffsetDateTime>,
signatories: Vec<String>,
observers: Vec<String>,
witnesses: Vec<String>,
package_name: String,
package_version: String,
package_id: String,
}
impl<T> Contract<T> {
#[must_use]
pub fn contract_id(&self) -> &ContractId<T> {
&self.contract_id
}
#[must_use]
pub fn payload(&self) -> &T {
&self.payload
}
#[must_use]
pub fn into_payload(self) -> T {
self.payload
}
#[must_use]
pub fn created_at_offset(&self) -> i64 {
self.created_at_offset
}
#[must_use]
pub fn archived_at_offset(&self) -> Option<i64> {
self.archived_at_offset
}
#[must_use]
pub fn was_active_at(&self, offset: i64) -> bool {
self.created_at_offset <= offset
&& self
.archived_at_offset
.is_none_or(|archived| archived > offset)
}
#[must_use]
pub fn created_effective_at(&self) -> Option<time::OffsetDateTime> {
self.created_effective_at
}
#[must_use]
pub fn archived_effective_at(&self) -> Option<time::OffsetDateTime> {
self.archived_effective_at
}
#[must_use]
pub fn signatories(&self) -> &[String] {
&self.signatories
}
#[must_use]
pub fn observers(&self) -> &[String] {
&self.observers
}
#[must_use]
pub fn witnesses(&self) -> &[String] {
&self.witnesses
}
#[must_use]
pub fn package_name(&self) -> &str {
&self.package_name
}
#[must_use]
pub fn package_version(&self) -> &str {
&self.package_version
}
#[must_use]
pub fn package_id(&self) -> &str {
&self.package_id
}
}
#[derive(Clone, Debug)]
pub struct Exercise<C> {
contract_id: String,
choice: String,
consuming: bool,
argument: C,
result: serde_json::Value,
exercised_at_offset: i64,
exercised_effective_at: Option<time::OffsetDateTime>,
controllers: Vec<String>,
}
impl<C> Exercise<C> {
#[must_use]
pub fn contract_id(&self) -> &str {
&self.contract_id
}
#[must_use]
pub fn choice(&self) -> &str {
&self.choice
}
#[must_use]
pub fn consuming(&self) -> bool {
self.consuming
}
#[must_use]
pub fn argument(&self) -> &C {
&self.argument
}
#[must_use]
pub fn result(&self) -> &serde_json::Value {
&self.result
}
pub fn result_as<R: serde::de::DeserializeOwned>(&self) -> Result<R> {
serde_json::from_value(self.result.clone()).map_err(|e| Error::Payload(Box::new(e)))
}
#[must_use]
pub fn exercised_at_offset(&self) -> i64 {
self.exercised_at_offset
}
#[must_use]
pub fn exercised_effective_at(&self) -> Option<time::OffsetDateTime> {
self.exercised_effective_at
}
#[must_use]
pub fn controllers(&self) -> &[String] {
&self.controllers
}
}
fn column<'a, T: tokio_postgres::types::FromSql<'a>>(
row: &'a tokio_postgres::Row,
name: &str,
) -> Result<T> {
row.try_get(name).map_err(|e| {
Error::UnexpectedResponse(format!("PQS returned no usable `{name}` column: {e}"))
})
}
impl<T: serde::de::DeserializeOwned> Contract<T> {
pub fn from_row(row: &tokio_postgres::Row) -> Result<Self> {
let payload: serde_json::Value = column(row, "payload")?;
let contract_id: String = column(row, "contract_id")?;
Ok(Self {
payload: serde_json::from_value(payload).map_err(|e| {
Error::Payload(Box::new(std::io::Error::other(format!(
"contract {contract_id}: {e}"
))))
})?,
contract_id: ContractId::new(contract_id),
created_at_offset: column(row, "created_at_offset")?,
archived_at_offset: column(row, "archived_at_offset")?,
created_effective_at: column(row, "created_effective_at")?,
archived_effective_at: column(row, "archived_effective_at")?,
signatories: column(row, "signatories")?,
observers: column(row, "observers")?,
witnesses: column(row, "witnesses")?,
package_name: column(row, "package_name")?,
package_version: column(row, "package_version")?,
package_id: column(row, "package_id")?,
})
}
}
impl<C: serde::de::DeserializeOwned> Exercise<C> {
pub fn from_row(row: &tokio_postgres::Row) -> Result<Self> {
let argument: serde_json::Value = column(row, "argument")?;
let choice: String = column(row, "choice")?;
Ok(Self {
argument: serde_json::from_value(argument).map_err(|e| {
Error::Payload(Box::new(std::io::Error::other(format!(
"choice {choice}: {e}"
))))
})?,
choice,
contract_id: column(row, "contract_id")?,
consuming: column(row, "consuming")?,
result: column::<Option<serde_json::Value>>(row, "result")?
.unwrap_or(serde_json::Value::Null),
exercised_at_offset: column(row, "exercised_at_offset")?,
exercised_effective_at: column(row, "exercised_effective_at")?,
controllers: column(row, "controllers")?,
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn contract(created: i64, archived: Option<i64>) -> Contract<()> {
Contract {
contract_id: ContractId::new("00c"),
payload: (),
created_at_offset: created,
archived_at_offset: archived,
created_effective_at: Some(time::OffsetDateTime::UNIX_EPOCH),
archived_effective_at: archived.map(|_| time::OffsetDateTime::UNIX_EPOCH),
signatories: vec!["alice".to_string()],
observers: vec!["bob".to_string()],
witnesses: vec!["carol".to_string()],
package_name: "quickstart-licensing".to_string(),
package_version: "0.1.0".to_string(),
package_id: "abc123".to_string(),
}
}
#[test]
fn was_active_at_is_inclusive_of_creation_and_exclusive_of_archival() {
let live = contract(10, None);
assert!(!live.was_active_at(9));
assert!(live.was_active_at(10));
assert!(live.was_active_at(1_000));
let archived = contract(10, Some(20));
assert!(!archived.was_active_at(9));
assert!(archived.was_active_at(10));
assert!(archived.was_active_at(19));
assert!(
!archived.was_active_at(20),
"archived *at* 20 is not active at 20"
);
assert!(!archived.was_active_at(21));
}
#[test]
fn the_accessors_return_the_row() {
let c = contract(10, Some(20));
assert_eq!(c.contract_id().as_str(), "00c");
assert_eq!(c.created_at_offset(), 10);
assert_eq!(c.archived_at_offset(), Some(20));
assert!(c.created_effective_at().is_some());
assert!(c.archived_effective_at().is_some());
assert_eq!(c.signatories(), ["alice".to_string()]);
assert_eq!(c.observers(), ["bob".to_string()]);
assert_eq!(c.witnesses(), ["carol".to_string()]);
assert_eq!(c.package_name(), "quickstart-licensing");
assert_eq!(c.package_version(), "0.1.0");
assert_eq!(c.package_id(), "abc123");
assert_eq!(*c.payload(), ());
assert_eq!(contract(1, None).archived_effective_at(), None);
let e = Exercise {
contract_id: "00x".to_string(),
choice: "Accept".to_string(),
consuming: true,
argument: 7_i64,
result: serde_json::json!({ "ok": true }),
exercised_at_offset: 33,
exercised_effective_at: Some(time::OffsetDateTime::UNIX_EPOCH),
controllers: vec!["alice".to_string()],
};
assert_eq!(e.contract_id(), "00x");
assert_eq!(e.choice(), "Accept");
assert!(e.consuming());
assert_eq!(*e.argument(), 7);
assert_eq!(e.result(), &serde_json::json!({ "ok": true }));
assert_eq!(e.exercised_at_offset(), 33);
assert!(e.exercised_effective_at().is_some());
assert_eq!(e.controllers(), ["alice".to_string()]);
let non_consuming = Exercise {
consuming: false,
exercised_effective_at: None,
..e
};
assert!(!non_consuming.consuming());
assert_eq!(non_consuming.exercised_effective_at(), None);
}
}