1use canton_core::{Error, Result};
10use canton_daml::ContractId;
11
12#[allow(clippy::struct_field_names)]
18#[derive(Clone, Debug)]
19pub struct Contract<T> {
20 contract_id: ContractId<T>,
21 payload: T,
22 created_at_offset: i64,
23 archived_at_offset: Option<i64>,
24 created_effective_at: Option<time::OffsetDateTime>,
25 archived_effective_at: Option<time::OffsetDateTime>,
26 signatories: Vec<String>,
27 observers: Vec<String>,
28 witnesses: Vec<String>,
29 package_name: String,
30 package_version: String,
31 package_id: String,
32}
33
34impl<T> Contract<T> {
35 #[must_use]
37 pub fn contract_id(&self) -> &ContractId<T> {
38 &self.contract_id
39 }
40
41 #[must_use]
43 pub fn payload(&self) -> &T {
44 &self.payload
45 }
46
47 #[must_use]
49 pub fn into_payload(self) -> T {
50 self.payload
51 }
52
53 #[must_use]
55 pub fn created_at_offset(&self) -> i64 {
56 self.created_at_offset
57 }
58
59 #[must_use]
68 pub fn archived_at_offset(&self) -> Option<i64> {
69 self.archived_at_offset
70 }
71
72 #[must_use]
78 pub fn was_active_at(&self, offset: i64) -> bool {
79 self.created_at_offset <= offset
80 && self
81 .archived_at_offset
82 .is_none_or(|archived| archived > offset)
83 }
84
85 #[must_use]
87 pub fn created_effective_at(&self) -> Option<time::OffsetDateTime> {
88 self.created_effective_at
89 }
90
91 #[must_use]
93 pub fn archived_effective_at(&self) -> Option<time::OffsetDateTime> {
94 self.archived_effective_at
95 }
96
97 #[must_use]
99 pub fn signatories(&self) -> &[String] {
100 &self.signatories
101 }
102
103 #[must_use]
105 pub fn observers(&self) -> &[String] {
106 &self.observers
107 }
108
109 #[must_use]
112 pub fn witnesses(&self) -> &[String] {
113 &self.witnesses
114 }
115
116 #[must_use]
118 pub fn package_name(&self) -> &str {
119 &self.package_name
120 }
121
122 #[must_use]
124 pub fn package_version(&self) -> &str {
125 &self.package_version
126 }
127
128 #[must_use]
130 pub fn package_id(&self) -> &str {
131 &self.package_id
132 }
133}
134
135#[derive(Clone, Debug)]
137pub struct Exercise<C> {
138 contract_id: String,
139 choice: String,
140 consuming: bool,
141 argument: C,
142 result: serde_json::Value,
143 exercised_at_offset: i64,
144 exercised_effective_at: Option<time::OffsetDateTime>,
145 controllers: Vec<String>,
146}
147
148impl<C> Exercise<C> {
149 #[must_use]
151 pub fn contract_id(&self) -> &str {
152 &self.contract_id
153 }
154
155 #[must_use]
157 pub fn choice(&self) -> &str {
158 &self.choice
159 }
160
161 #[must_use]
163 pub fn consuming(&self) -> bool {
164 self.consuming
165 }
166
167 #[must_use]
169 pub fn argument(&self) -> &C {
170 &self.argument
171 }
172
173 #[must_use]
179 pub fn result(&self) -> &serde_json::Value {
180 &self.result
181 }
182
183 pub fn result_as<R: serde::de::DeserializeOwned>(&self) -> Result<R> {
188 serde_json::from_value(self.result.clone()).map_err(|e| Error::Payload(Box::new(e)))
189 }
190
191 #[must_use]
193 pub fn exercised_at_offset(&self) -> i64 {
194 self.exercised_at_offset
195 }
196
197 #[must_use]
199 pub fn exercised_effective_at(&self) -> Option<time::OffsetDateTime> {
200 self.exercised_effective_at
201 }
202
203 #[must_use]
205 pub fn controllers(&self) -> &[String] {
206 &self.controllers
207 }
208}
209
210fn column<'a, T: tokio_postgres::types::FromSql<'a>>(
215 row: &'a tokio_postgres::Row,
216 name: &str,
217) -> Result<T> {
218 row.try_get(name).map_err(|e| {
219 Error::UnexpectedResponse(format!("PQS returned no usable `{name}` column: {e}"))
220 })
221}
222
223impl<T: serde::de::DeserializeOwned> Contract<T> {
224 pub fn from_row(row: &tokio_postgres::Row) -> Result<Self> {
230 let payload: serde_json::Value = column(row, "payload")?;
231 let contract_id: String = column(row, "contract_id")?;
232 Ok(Self {
233 payload: serde_json::from_value(payload).map_err(|e| {
234 Error::Payload(Box::new(std::io::Error::other(format!(
235 "contract {contract_id}: {e}"
236 ))))
237 })?,
238 contract_id: ContractId::new(contract_id),
239 created_at_offset: column(row, "created_at_offset")?,
240 archived_at_offset: column(row, "archived_at_offset")?,
241 created_effective_at: column(row, "created_effective_at")?,
242 archived_effective_at: column(row, "archived_effective_at")?,
243 signatories: column(row, "signatories")?,
244 observers: column(row, "observers")?,
245 witnesses: column(row, "witnesses")?,
246 package_name: column(row, "package_name")?,
247 package_version: column(row, "package_version")?,
248 package_id: column(row, "package_id")?,
249 })
250 }
251}
252
253impl<C: serde::de::DeserializeOwned> Exercise<C> {
254 pub fn from_row(row: &tokio_postgres::Row) -> Result<Self> {
259 let argument: serde_json::Value = column(row, "argument")?;
260 let choice: String = column(row, "choice")?;
261 Ok(Self {
262 argument: serde_json::from_value(argument).map_err(|e| {
263 Error::Payload(Box::new(std::io::Error::other(format!(
264 "choice {choice}: {e}"
265 ))))
266 })?,
267 choice,
268 contract_id: column(row, "contract_id")?,
269 consuming: column(row, "consuming")?,
270 result: column::<Option<serde_json::Value>>(row, "result")?
271 .unwrap_or(serde_json::Value::Null),
272 exercised_at_offset: column(row, "exercised_at_offset")?,
273 exercised_effective_at: column(row, "exercised_effective_at")?,
274 controllers: column(row, "controllers")?,
275 })
276 }
277}
278
279#[cfg(test)]
280#[allow(clippy::unwrap_used, clippy::expect_used)]
281mod tests {
282 use super::*;
283
284 fn contract(created: i64, archived: Option<i64>) -> Contract<()> {
285 Contract {
286 contract_id: ContractId::new("00c"),
287 payload: (),
288 created_at_offset: created,
289 archived_at_offset: archived,
290 created_effective_at: Some(time::OffsetDateTime::UNIX_EPOCH),
291 archived_effective_at: archived.map(|_| time::OffsetDateTime::UNIX_EPOCH),
292 signatories: vec!["alice".to_string()],
293 observers: vec!["bob".to_string()],
294 witnesses: vec!["carol".to_string()],
295 package_name: "quickstart-licensing".to_string(),
296 package_version: "0.1.0".to_string(),
297 package_id: "abc123".to_string(),
298 }
299 }
300
301 #[test]
304 fn was_active_at_is_inclusive_of_creation_and_exclusive_of_archival() {
305 let live = contract(10, None);
306 assert!(!live.was_active_at(9));
307 assert!(live.was_active_at(10));
308 assert!(live.was_active_at(1_000));
309
310 let archived = contract(10, Some(20));
311 assert!(!archived.was_active_at(9));
312 assert!(archived.was_active_at(10));
313 assert!(archived.was_active_at(19));
314 assert!(
315 !archived.was_active_at(20),
316 "archived *at* 20 is not active at 20"
317 );
318 assert!(!archived.was_active_at(21));
319 }
320
321 #[test]
323 fn the_accessors_return_the_row() {
324 let c = contract(10, Some(20));
325 assert_eq!(c.contract_id().as_str(), "00c");
326 assert_eq!(c.created_at_offset(), 10);
327 assert_eq!(c.archived_at_offset(), Some(20));
328 assert!(c.created_effective_at().is_some());
329 assert!(c.archived_effective_at().is_some());
330 assert_eq!(c.signatories(), ["alice".to_string()]);
331 assert_eq!(c.observers(), ["bob".to_string()]);
332 assert_eq!(c.witnesses(), ["carol".to_string()]);
333 assert_eq!(c.package_name(), "quickstart-licensing");
334 assert_eq!(c.package_version(), "0.1.0");
335 assert_eq!(c.package_id(), "abc123");
336 assert_eq!(*c.payload(), ());
337 assert_eq!(contract(1, None).archived_effective_at(), None);
338
339 let e = Exercise {
340 contract_id: "00x".to_string(),
341 choice: "Accept".to_string(),
342 consuming: true,
343 argument: 7_i64,
344 result: serde_json::json!({ "ok": true }),
345 exercised_at_offset: 33,
346 exercised_effective_at: Some(time::OffsetDateTime::UNIX_EPOCH),
347 controllers: vec!["alice".to_string()],
348 };
349 assert_eq!(e.contract_id(), "00x");
350 assert_eq!(e.choice(), "Accept");
351 assert!(e.consuming());
352 assert_eq!(*e.argument(), 7);
353 assert_eq!(e.result(), &serde_json::json!({ "ok": true }));
354 assert_eq!(e.exercised_at_offset(), 33);
355 assert!(e.exercised_effective_at().is_some());
356 assert_eq!(e.controllers(), ["alice".to_string()]);
357
358 let non_consuming = Exercise {
359 consuming: false,
360 exercised_effective_at: None,
361 ..e
362 };
363 assert!(!non_consuming.consuming());
364 assert_eq!(non_consuming.exercised_effective_at(), None);
365 }
366}