1use canton_core::{Error, Result};
39use canton_daml as rt;
40use canton_proto::com::daml::ledger::api::v2 as pb;
41use serde_json::Value;
42
43#[derive(Clone, Debug, PartialEq, Eq, Hash)]
45#[non_exhaustive]
46pub struct Identifier {
47 pub package_id: String,
49 pub module_name: String,
51 pub entity_name: String,
53}
54
55impl Identifier {
56 fn from_grpc(id: &pb::Identifier) -> Self {
57 Self {
58 package_id: id.package_id.clone(),
59 module_name: id.module_name.clone(),
60 entity_name: id.entity_name.clone(),
61 }
62 }
63
64 fn from_json(text: &str) -> Result<Self> {
68 let mut parts = text.rsplitn(3, ':');
69 match (parts.next(), parts.next(), parts.next()) {
70 (Some(entity), Some(module), Some(package))
71 if !entity.is_empty() && !module.is_empty() && !package.is_empty() =>
72 {
73 Ok(Self {
74 package_id: package.to_string(),
75 module_name: module.to_string(),
76 entity_name: entity.to_string(),
77 })
78 }
79 _ => Err(malformed(format!(
80 "`{text}` is not a package:Module:Entity id"
81 ))),
82 }
83 }
84}
85
86impl std::fmt::Display for Identifier {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 write!(
89 f,
90 "{}:{}:{}",
91 self.package_id, self.module_name, self.entity_name
92 )
93 }
94}
95
96#[derive(Clone, Debug)]
98#[non_exhaustive]
99pub enum Update {
100 Transaction(Transaction),
102 Reassignment(Reassignment),
104 TopologyTransaction(TopologyTransaction),
106 OffsetCheckpoint(OffsetCheckpoint),
109}
110
111impl Update {
112 #[must_use]
114 pub fn offset(&self) -> i64 {
115 match self {
116 Self::Transaction(t) => t.offset,
117 Self::Reassignment(r) => r.offset,
118 Self::TopologyTransaction(t) => t.offset,
119 Self::OffsetCheckpoint(c) => c.offset,
120 }
121 }
122
123 #[must_use]
125 pub fn update_id(&self) -> Option<&str> {
126 match self {
127 Self::Transaction(t) => Some(&t.update_id),
128 Self::Reassignment(r) => Some(&r.update_id),
129 Self::TopologyTransaction(t) => Some(&t.update_id),
130 Self::OffsetCheckpoint(_) => None,
131 }
132 }
133
134 #[must_use]
136 pub fn as_transaction(&self) -> Option<&Transaction> {
137 match self {
138 Self::Transaction(t) => Some(t),
139 _ => None,
140 }
141 }
142
143 pub fn from_grpc(update: pb::get_updates_response::Update) -> Result<Self> {
148 use pb::get_updates_response::Update as U;
149 Ok(match update {
150 U::Transaction(t) => Self::Transaction(Transaction::from_grpc(t)?),
151 U::Reassignment(r) => Self::Reassignment(Reassignment::from_grpc(r)),
152 U::TopologyTransaction(t) => {
153 Self::TopologyTransaction(TopologyTransaction::from_grpc(t))
154 }
155 U::OffsetCheckpoint(c) => Self::OffsetCheckpoint(OffsetCheckpoint::from_grpc(&c)),
156 })
157 }
158
159 pub fn from_grpc_point(update: pb::get_update_response::Update) -> Result<Self> {
166 use pb::get_update_response::Update as U;
167 Ok(match update {
168 U::Transaction(t) => Self::Transaction(Transaction::from_grpc(t)?),
169 U::Reassignment(r) => Self::Reassignment(Reassignment::from_grpc(r)),
170 U::TopologyTransaction(t) => {
171 Self::TopologyTransaction(TopologyTransaction::from_grpc(t))
172 }
173 })
174 }
175
176 pub fn from_json(element: &Value) -> Result<Self> {
185 let update = element.get("update").unwrap_or(element);
186 let (kind, body) = update
187 .as_object()
188 .and_then(|object| object.iter().next())
189 .ok_or_else(|| malformed("an update element with no update in it"))?;
190 let value = body.get("value").unwrap_or(body);
191 Ok(match kind.as_str() {
192 "Transaction" => Self::Transaction(Transaction::from_json(value)?),
193 "Reassignment" => Self::Reassignment(Reassignment::from_json(value)?),
194 "TopologyTransaction" => {
195 Self::TopologyTransaction(TopologyTransaction::from_json(value)?)
196 }
197 "OffsetCheckpoint" => Self::OffsetCheckpoint(OffsetCheckpoint::from_json(value)?),
198 other => return Err(malformed(format!("an update of unknown kind `{other}`"))),
199 })
200 }
201}
202
203#[derive(Clone, Debug)]
205#[non_exhaustive]
206pub struct Transaction {
207 pub update_id: String,
209 pub command_id: String,
211 pub workflow_id: String,
213 pub offset: i64,
215 pub synchronizer_id: String,
217 pub effective_at: Option<rt::Timestamp>,
219 pub record_time: Option<rt::Timestamp>,
221 pub events: Vec<Event>,
223}
224
225impl Transaction {
226 pub fn from_grpc(tx: pb::Transaction) -> Result<Self> {
232 let events = tx
233 .events
234 .into_iter()
235 .filter_map(|event| event.event)
236 .map(Event::from_grpc)
237 .collect::<Result<Vec<_>>>()?;
238 Ok(Self {
239 update_id: tx.update_id,
240 command_id: tx.command_id,
241 workflow_id: tx.workflow_id,
242 offset: tx.offset,
243 synchronizer_id: tx.synchronizer_id,
244 effective_at: tx.effective_at.as_ref().map(timestamp),
245 record_time: tx.record_time.as_ref().map(timestamp),
246 events,
247 })
248 }
249
250 pub fn from_json(tx: &Value) -> Result<Self> {
256 let events = tx
257 .get("events")
258 .and_then(Value::as_array)
259 .map(|events| {
260 events
261 .iter()
262 .map(Event::from_json)
263 .collect::<Result<Vec<_>>>()
264 })
265 .transpose()?
266 .unwrap_or_default();
267 Ok(Self {
268 update_id: text(tx, "updateId")?,
269 command_id: text_or_empty(tx, "commandId"),
270 workflow_id: text_or_empty(tx, "workflowId"),
271 offset: int(tx, "offset")?,
272 synchronizer_id: text_or_empty(tx, "synchronizerId"),
273 effective_at: json_time(tx, "effectiveAt")?,
274 record_time: json_time(tx, "recordTime")?,
275 events,
276 })
277 }
278
279 pub fn created(&self) -> impl Iterator<Item = &CreatedEvent> {
281 self.events.iter().filter_map(|e| match e {
282 Event::Created(c) => Some(c),
283 _ => None,
284 })
285 }
286
287 pub fn archived(&self) -> impl Iterator<Item = &ArchivedEvent> {
289 self.events.iter().filter_map(|e| match e {
290 Event::Archived(a) => Some(a),
291 _ => None,
292 })
293 }
294
295 pub fn exercised(&self) -> impl Iterator<Item = &ExercisedEvent> {
297 self.events.iter().filter_map(|e| match e {
298 Event::Exercised(x) => Some(x),
299 _ => None,
300 })
301 }
302}
303
304#[derive(Clone, Debug)]
306#[non_exhaustive]
307pub enum Event {
308 Created(CreatedEvent),
310 Archived(ArchivedEvent),
312 Exercised(ExercisedEvent),
314}
315
316impl Event {
317 #[must_use]
319 pub fn offset(&self) -> i64 {
320 match self {
321 Self::Created(e) => e.offset,
322 Self::Archived(e) => e.offset,
323 Self::Exercised(e) => e.offset,
324 }
325 }
326
327 #[must_use]
329 pub fn contract_id(&self) -> &str {
330 match self {
331 Self::Created(e) => &e.contract_id,
332 Self::Archived(e) => &e.contract_id,
333 Self::Exercised(e) => &e.contract_id,
334 }
335 }
336
337 #[must_use]
339 pub fn template_id(&self) -> &Identifier {
340 match self {
341 Self::Created(e) => &e.template_id,
342 Self::Archived(e) => &e.template_id,
343 Self::Exercised(e) => &e.template_id,
344 }
345 }
346
347 fn from_grpc(event: pb::event::Event) -> Result<Self> {
348 Ok(match event {
349 pb::event::Event::Created(c) => Self::Created(CreatedEvent::from_grpc(c)?),
350 pb::event::Event::Archived(a) => Self::Archived(ArchivedEvent::from_grpc(&a)?),
351 pb::event::Event::Exercised(x) => Self::Exercised(ExercisedEvent::from_grpc(x)?),
352 })
353 }
354
355 fn from_json(event: &Value) -> Result<Self> {
356 if let Some(created) = event.get("CreatedEvent") {
357 Ok(Self::Created(CreatedEvent::from_json(created)?))
358 } else if let Some(archived) = event.get("ArchivedEvent") {
359 Ok(Self::Archived(ArchivedEvent::from_json(archived)?))
360 } else if let Some(exercised) = event.get("ExercisedEvent") {
361 Ok(Self::Exercised(ExercisedEvent::from_json(exercised)?))
362 } else {
363 Err(malformed(
364 "an event that is not created, archived or exercised",
365 ))
366 }
367 }
368}
369
370#[derive(Clone, Debug)]
373enum Wire<G> {
374 Grpc(Box<G>),
375 Json(Box<Value>),
376}
377
378#[derive(Clone, Debug)]
380#[non_exhaustive]
381pub struct CreatedEvent {
382 pub offset: i64,
384 pub node_id: i32,
386 pub contract_id: String,
388 pub template_id: Identifier,
390 pub package_name: String,
392 pub witness_parties: Vec<String>,
394 pub signatories: Vec<String>,
396 pub observers: Vec<String>,
398 pub created_at: Option<rt::Timestamp>,
400 pub created_event_blob: Vec<u8>,
403 wire: Wire<pb::CreatedEvent>,
404}
405
406impl CreatedEvent {
407 pub fn decode<T>(&self) -> std::result::Result<T, rt::ValueError>
415 where
416 T: rt::Template + serde::de::DeserializeOwned,
417 {
418 match &self.wire {
419 Wire::Grpc(event) => T::from_created_event(event),
420 Wire::Json(event) => T::from_json_created_event(event),
421 }
422 }
423
424 #[must_use]
426 pub fn as_grpc(&self) -> Option<&pb::CreatedEvent> {
427 match &self.wire {
428 Wire::Grpc(event) => Some(event),
429 Wire::Json(_) => None,
430 }
431 }
432
433 #[must_use]
435 pub fn as_json(&self) -> Option<&Value> {
436 match &self.wire {
437 Wire::Grpc(_) => None,
438 Wire::Json(event) => Some(event),
439 }
440 }
441
442 fn from_grpc(event: pb::CreatedEvent) -> Result<Self> {
443 let template_id = Identifier::from_grpc(
444 event
445 .template_id
446 .as_ref()
447 .ok_or_else(|| malformed("a created event without a template id"))?,
448 );
449 Ok(Self {
450 offset: event.offset,
451 node_id: event.node_id,
452 contract_id: event.contract_id.clone(),
453 template_id,
454 package_name: event.package_name.clone(),
455 witness_parties: event.witness_parties.clone(),
456 signatories: event.signatories.clone(),
457 observers: event.observers.clone(),
458 created_at: event.created_at.as_ref().map(timestamp),
459 created_event_blob: event.created_event_blob.clone(),
460 wire: Wire::Grpc(Box::new(event)),
461 })
462 }
463
464 fn from_json(event: &Value) -> Result<Self> {
465 use base64::Engine as _;
466 let blob = text_or_empty(event, "createdEventBlob");
467 let created_event_blob = if blob.is_empty() {
468 Vec::new()
469 } else {
470 base64::engine::general_purpose::STANDARD
471 .decode(blob.as_bytes())
472 .map_err(|e| malformed(format!("a createdEventBlob that is not base64: {e}")))?
473 };
474 Ok(Self {
475 offset: int(event, "offset")?,
476 node_id: node_id(event, "nodeId")?,
477 contract_id: text(event, "contractId")?,
478 template_id: Identifier::from_json(&text(event, "templateId")?)?,
479 package_name: text_or_empty(event, "packageName"),
480 witness_parties: strings(event, "witnessParties"),
481 signatories: strings(event, "signatories"),
482 observers: strings(event, "observers"),
483 created_at: json_time(event, "createdAt")?,
484 created_event_blob,
485 wire: Wire::Json(Box::new(event.clone())),
486 })
487 }
488}
489
490#[derive(Clone, Debug)]
492#[non_exhaustive]
493pub struct ArchivedEvent {
494 pub offset: i64,
496 pub node_id: i32,
498 pub contract_id: String,
500 pub template_id: Identifier,
502 pub package_name: String,
504 pub witness_parties: Vec<String>,
506 pub implemented_interfaces: Vec<Identifier>,
508}
509
510impl ArchivedEvent {
511 fn from_grpc(event: &pb::ArchivedEvent) -> Result<Self> {
512 Ok(Self {
513 offset: event.offset,
514 node_id: event.node_id,
515 contract_id: event.contract_id.clone(),
516 template_id: Identifier::from_grpc(
517 event
518 .template_id
519 .as_ref()
520 .ok_or_else(|| malformed("an archived event without a template id"))?,
521 ),
522 package_name: event.package_name.clone(),
523 witness_parties: event.witness_parties.clone(),
524 implemented_interfaces: event
525 .implemented_interfaces
526 .iter()
527 .map(Identifier::from_grpc)
528 .collect(),
529 })
530 }
531
532 fn from_json(event: &Value) -> Result<Self> {
533 Ok(Self {
534 offset: int(event, "offset")?,
535 node_id: node_id(event, "nodeId")?,
536 contract_id: text(event, "contractId")?,
537 template_id: Identifier::from_json(&text(event, "templateId")?)?,
538 package_name: text_or_empty(event, "packageName"),
539 witness_parties: strings(event, "witnessParties"),
540 implemented_interfaces: identifiers(event, "implementedInterfaces")?,
541 })
542 }
543}
544
545#[derive(Clone, Debug)]
547enum WireValue {
548 Grpc(Box<pb::Value>),
549 Json(Box<Value>),
550}
551
552impl WireValue {
553 fn decode<T>(&self) -> std::result::Result<T, rt::ValueError>
554 where
555 T: rt::FromValue + serde::de::DeserializeOwned,
556 {
557 match self {
558 Self::Grpc(value) => T::from_value(value),
559 Self::Json(value) => serde_json::from_value((**value).clone())
560 .map_err(|e| rt::ValueError::new(format!("value did not decode: {e}"))),
561 }
562 }
563}
564
565#[derive(Clone, Debug)]
567#[non_exhaustive]
568pub struct ExercisedEvent {
569 pub offset: i64,
571 pub node_id: i32,
573 pub contract_id: String,
575 pub template_id: Identifier,
577 pub interface_id: Option<Identifier>,
579 pub choice: String,
581 pub consuming: bool,
583 pub acting_parties: Vec<String>,
585 pub witness_parties: Vec<String>,
587 pub last_descendant_node_id: i32,
589 pub package_name: String,
591 argument: Option<WireValue>,
592 result: Option<WireValue>,
593}
594
595impl ExercisedEvent {
596 pub fn decode_argument<A>(&self) -> std::result::Result<A, rt::ValueError>
602 where
603 A: rt::FromValue + serde::de::DeserializeOwned,
604 {
605 self.argument
606 .as_ref()
607 .ok_or_else(|| rt::ValueError::new("exercised event carries no choice argument"))?
608 .decode()
609 }
610
611 #[must_use]
613 pub fn decode_result<R>(&self) -> Option<std::result::Result<R, rt::ValueError>>
614 where
615 R: rt::FromValue + serde::de::DeserializeOwned,
616 {
617 self.result.as_ref().map(WireValue::decode)
618 }
619
620 fn from_grpc(event: pb::ExercisedEvent) -> Result<Self> {
621 Ok(Self {
622 offset: event.offset,
623 node_id: event.node_id,
624 template_id: Identifier::from_grpc(
625 event
626 .template_id
627 .as_ref()
628 .ok_or_else(|| malformed("an exercised event without a template id"))?,
629 ),
630 interface_id: event.interface_id.as_ref().map(Identifier::from_grpc),
631 contract_id: event.contract_id,
632 choice: event.choice,
633 consuming: event.consuming,
634 acting_parties: event.acting_parties,
635 witness_parties: event.witness_parties,
636 last_descendant_node_id: event.last_descendant_node_id,
637 package_name: event.package_name,
638 argument: event.choice_argument.map(|v| WireValue::Grpc(Box::new(v))),
639 result: event.exercise_result.map(|v| WireValue::Grpc(Box::new(v))),
640 })
641 }
642
643 fn from_json(event: &Value) -> Result<Self> {
644 let interface_id = match event.get("interfaceId").and_then(Value::as_str) {
645 Some(id) if !id.is_empty() => Some(Identifier::from_json(id)?),
646 _ => None,
647 };
648 let present = |key: &str| {
649 event
650 .get(key)
651 .filter(|v| !v.is_null())
652 .map(|v| WireValue::Json(Box::new(v.clone())))
653 };
654 Ok(Self {
655 offset: int(event, "offset")?,
656 node_id: node_id(event, "nodeId")?,
657 contract_id: text(event, "contractId")?,
658 template_id: Identifier::from_json(&text(event, "templateId")?)?,
659 interface_id,
660 choice: text(event, "choice")?,
661 consuming: event
662 .get("consuming")
663 .and_then(Value::as_bool)
664 .unwrap_or_default(),
665 acting_parties: strings(event, "actingParties"),
666 witness_parties: strings(event, "witnessParties"),
667 last_descendant_node_id: node_id(event, "lastDescendantNodeId")?,
668 package_name: text_or_empty(event, "packageName"),
669 argument: present("choiceArgument"),
670 result: present("exerciseResult"),
671 })
672 }
673}
674
675#[derive(Clone, Debug)]
678#[non_exhaustive]
679pub struct Reassignment {
680 pub update_id: String,
682 pub command_id: String,
684 pub workflow_id: String,
686 pub offset: i64,
688 pub record_time: Option<rt::Timestamp>,
690 wire: Wire<pb::Reassignment>,
691}
692
693impl Reassignment {
694 #[must_use]
696 pub fn as_grpc(&self) -> Option<&pb::Reassignment> {
697 match &self.wire {
698 Wire::Grpc(r) => Some(r),
699 Wire::Json(_) => None,
700 }
701 }
702
703 #[must_use]
705 pub fn as_json(&self) -> Option<&Value> {
706 match &self.wire {
707 Wire::Grpc(_) => None,
708 Wire::Json(r) => Some(r),
709 }
710 }
711
712 fn from_grpc(r: pb::Reassignment) -> Self {
713 Self {
714 update_id: r.update_id.clone(),
715 command_id: r.command_id.clone(),
716 workflow_id: r.workflow_id.clone(),
717 offset: r.offset,
718 record_time: r.record_time.as_ref().map(timestamp),
719 wire: Wire::Grpc(Box::new(r)),
720 }
721 }
722
723 fn from_json(r: &Value) -> Result<Self> {
724 Ok(Self {
725 update_id: text(r, "updateId")?,
726 command_id: text_or_empty(r, "commandId"),
727 workflow_id: text_or_empty(r, "workflowId"),
728 offset: int(r, "offset")?,
729 record_time: json_time(r, "recordTime")?,
730 wire: Wire::Json(Box::new(r.clone())),
731 })
732 }
733}
734
735#[derive(Clone, Debug)]
738#[non_exhaustive]
739pub struct TopologyTransaction {
740 pub update_id: String,
742 pub offset: i64,
744 pub synchronizer_id: String,
746 pub record_time: Option<rt::Timestamp>,
748 wire: Wire<pb::TopologyTransaction>,
749}
750
751impl TopologyTransaction {
752 #[must_use]
754 pub fn as_grpc(&self) -> Option<&pb::TopologyTransaction> {
755 match &self.wire {
756 Wire::Grpc(t) => Some(t),
757 Wire::Json(_) => None,
758 }
759 }
760
761 #[must_use]
764 pub fn as_json(&self) -> Option<&Value> {
765 match &self.wire {
766 Wire::Grpc(_) => None,
767 Wire::Json(t) => Some(t),
768 }
769 }
770
771 fn from_grpc(t: pb::TopologyTransaction) -> Self {
772 Self {
773 update_id: t.update_id.clone(),
774 offset: t.offset,
775 synchronizer_id: t.synchronizer_id.clone(),
776 record_time: t.record_time.as_ref().map(timestamp),
777 wire: Wire::Grpc(Box::new(t)),
778 }
779 }
780
781 fn from_json(t: &Value) -> Result<Self> {
782 Ok(Self {
783 update_id: text(t, "updateId")?,
784 offset: int(t, "offset")?,
785 synchronizer_id: text_or_empty(t, "synchronizerId"),
786 record_time: json_time(t, "recordTime")?,
787 wire: Wire::Json(Box::new(t.clone())),
788 })
789 }
790}
791
792#[derive(Clone, Debug, PartialEq, Eq)]
794#[non_exhaustive]
795pub struct OffsetCheckpoint {
796 pub offset: i64,
798 pub synchronizer_times: Vec<SynchronizerTime>,
800}
801
802#[derive(Clone, Debug, PartialEq, Eq)]
804#[non_exhaustive]
805pub struct SynchronizerTime {
806 pub synchronizer_id: String,
808 pub record_time: Option<rt::Timestamp>,
810}
811
812impl OffsetCheckpoint {
813 fn from_grpc(c: &pb::OffsetCheckpoint) -> Self {
814 Self {
815 offset: c.offset,
816 synchronizer_times: c
817 .synchronizer_times
818 .iter()
819 .map(|t| SynchronizerTime {
820 synchronizer_id: t.synchronizer_id.clone(),
821 record_time: t.record_time.as_ref().map(timestamp),
822 })
823 .collect(),
824 }
825 }
826
827 fn from_json(c: &Value) -> Result<Self> {
828 let synchronizer_times = c
829 .get("synchronizerTimes")
830 .and_then(Value::as_array)
831 .map(|times| {
832 times
833 .iter()
834 .map(|t| {
835 Ok(SynchronizerTime {
836 synchronizer_id: text_or_empty(t, "synchronizerId"),
837 record_time: json_time(t, "recordTime")?,
838 })
839 })
840 .collect::<Result<Vec<_>>>()
841 })
842 .transpose()?
843 .unwrap_or_default();
844 Ok(Self {
845 offset: int(c, "offset")?,
846 synchronizer_times,
847 })
848 }
849}
850
851fn malformed(message: impl Into<String>) -> Error {
852 Error::UnexpectedResponse(message.into())
853}
854
855fn timestamp(t: &prost_types::Timestamp) -> rt::Timestamp {
856 rt::Timestamp(t.seconds * 1_000_000 + i64::from(t.nanos) / 1_000)
857}
858
859fn text(value: &Value, key: &str) -> Result<String> {
860 value
861 .get(key)
862 .and_then(Value::as_str)
863 .map(str::to_string)
864 .ok_or_else(|| malformed(format!("a JSON update without `{key}`")))
865}
866
867fn text_or_empty(value: &Value, key: &str) -> String {
868 value
869 .get(key)
870 .and_then(Value::as_str)
871 .unwrap_or_default()
872 .to_string()
873}
874
875fn int(value: &Value, key: &str) -> Result<i64> {
876 value
877 .get(key)
878 .and_then(Value::as_i64)
879 .ok_or_else(|| malformed(format!("a JSON update without a numeric `{key}`")))
880}
881
882fn node_id(value: &Value, key: &str) -> Result<i32> {
883 i32::try_from(int(value, key)?).map_err(|_| malformed(format!("`{key}` out of range")))
884}
885
886fn strings(value: &Value, key: &str) -> Vec<String> {
887 value
888 .get(key)
889 .and_then(Value::as_array)
890 .map(|items| {
891 items
892 .iter()
893 .filter_map(Value::as_str)
894 .map(str::to_string)
895 .collect()
896 })
897 .unwrap_or_default()
898}
899
900fn identifiers(value: &Value, key: &str) -> Result<Vec<Identifier>> {
901 value.get(key).and_then(Value::as_array).map_or_else(
902 || Ok(Vec::new()),
903 |items| {
904 items
905 .iter()
906 .filter_map(Value::as_str)
907 .map(Identifier::from_json)
908 .collect()
909 },
910 )
911}
912
913fn json_time(value: &Value, key: &str) -> Result<Option<rt::Timestamp>> {
914 match value.get(key) {
915 None | Some(Value::Null) => Ok(None),
916 Some(time) => serde_json::from_value(time.clone())
917 .map(Some)
918 .map_err(|e| malformed(format!("`{key}` is not a timestamp: {e}"))),
919 }
920}
921
922#[cfg(test)]
923#[allow(clippy::unwrap_used, clippy::expect_used)]
924mod tests {
925 use super::*;
926 use serde_json::json;
927
928 #[derive(Debug, PartialEq, serde::Deserialize)]
930 struct Steward {
931 steward: String,
932 charter: String,
933 }
934 impl rt::Contract for Steward {
935 const PACKAGE_ID: &'static str = "a50e65b2";
936 const PACKAGE_NAME: &'static str = "contractkeys-h61a17c769e62";
937 const MODULE_NAME: &'static str = "ContractKeys";
938 const ENTITY_NAME: &'static str = "Steward";
939 }
940 impl rt::ToValue for Steward {
941 fn to_value(&self) -> pb::Value {
942 rt::Unit.to_value()
943 }
944 }
945 impl rt::FromValue for Steward {
946 fn from_value(value: &pb::Value) -> std::result::Result<Self, rt::ValueError> {
947 let Some(pb::value::Sum::Record(record)) = &value.sum else {
948 return Err(rt::ValueError::new("not a record"));
949 };
950 let text = |i: usize| match record.fields.get(i).and_then(|f| f.value.as_ref()) {
951 Some(pb::Value {
952 sum: Some(pb::value::Sum::Party(p) | pb::value::Sum::Text(p)),
953 }) => Ok(p.clone()),
954 _ => Err(rt::ValueError::new("field")),
955 };
956 Ok(Self {
957 steward: text(0)?,
958 charter: text(1)?,
959 })
960 }
961 }
962 impl rt::Template for Steward {
963 fn to_record(&self) -> pb::Record {
964 pb::Record::default()
965 }
966 }
967
968 #[derive(Debug, serde::Deserialize)]
970 struct Revise {
971 revised: String,
972 }
973 impl rt::FromValue for Revise {
974 fn from_value(_: &pb::Value) -> std::result::Result<Self, rt::ValueError> {
975 Err(rt::ValueError::new("unused on the JSON path"))
976 }
977 }
978
979 fn json_transaction() -> Value {
982 json!({"update": {"Transaction": {"value": {
983 "updateId": "1220d4c7",
984 "commandId": "cmd-1",
985 "workflowId": "",
986 "effectiveAt": "2026-10-23T14:52:28.949667Z",
987 "offset": 16376,
988 "synchronizerId": "global-domain::1220",
989 "recordTime": "2026-10-23T14:52:29.100000Z",
990 "events": [
991 {"ExercisedEvent": {
992 "offset": 16376, "nodeId": 0, "contractId": "009d1f",
993 "templateId": "a50e65b2:ContractKeys:Steward",
994 "interfaceId": null, "choice": "Revise",
995 "choiceArgument": {"revised": "v2"},
996 "actingParties": ["alice::1220"], "consuming": true,
997 "witnessParties": ["alice::1220"], "lastDescendantNodeId": 1,
998 "exerciseResult": "0068d4", "packageName": "contractkeys-h61a17c769e62",
999 "implementedInterfaces": [], "acsDelta": true
1000 }},
1001 {"CreatedEvent": {
1002 "offset": 16376, "nodeId": 1, "contractId": "0068d4",
1003 "templateId": "a50e65b2:ContractKeys:Steward",
1004 "contractKey": "alice::1220", "contractKeyHash": "",
1005 "createArgument": {"steward": "alice::1220", "charter": "v2"},
1006 "createdEventBlob": "", "interfaceViews": [],
1007 "witnessParties": ["alice::1220"], "signatories": ["alice::1220"],
1008 "observers": [], "createdAt": "2026-10-23T14:52:28.949667Z",
1009 "packageName": "contractkeys-h61a17c769e62",
1010 "representativePackageId": "a50e65b2", "acsDelta": true
1011 }},
1012 {"ArchivedEvent": {
1013 "offset": 16376, "nodeId": 2, "contractId": "009d1f",
1014 "templateId": "a50e65b2:ContractKeys:Steward",
1015 "witnessParties": ["alice::1220"],
1016 "packageName": "contractkeys-h61a17c769e62",
1017 "implementedInterfaces": []
1018 }}
1019 ]
1020 }}}})
1021 }
1022
1023 #[test]
1024 fn a_json_transaction_reads_into_the_sdk_types() {
1025 let update = Update::from_json(&json_transaction()).unwrap();
1026 assert_eq!(update.offset(), 16376);
1027 assert_eq!(update.update_id(), Some("1220d4c7"));
1028 let tx = update.as_transaction().unwrap();
1029 assert_eq!(tx.synchronizer_id, "global-domain::1220");
1030 assert!(tx.effective_at.is_some() && tx.record_time.is_some());
1031
1032 let exercised = tx.exercised().next().unwrap();
1033 assert_eq!(exercised.choice, "Revise");
1034 assert!(exercised.consuming);
1035 assert_eq!(
1036 exercised.template_id.to_string(),
1037 "a50e65b2:ContractKeys:Steward"
1038 );
1039 let argument: Revise = exercised.decode_argument().unwrap();
1040 assert_eq!(argument.revised, "v2");
1041
1042 let created = tx.created().next().unwrap();
1043 assert_eq!(created.contract_id, "0068d4");
1044 assert_eq!(created.package_name, "contractkeys-h61a17c769e62");
1045 assert_eq!(
1046 created.decode::<Steward>().unwrap(),
1047 Steward {
1048 steward: "alice::1220".to_string(),
1049 charter: "v2".to_string()
1050 }
1051 );
1052 assert!(created.as_json().is_some() && created.as_grpc().is_none());
1053 assert_eq!(tx.archived().count(), 1);
1054 }
1055
1056 #[test]
1057 fn the_same_transaction_over_grpc_reads_into_the_same_types() {
1058 let party = |p: &str| pb::Value {
1059 sum: Some(pb::value::Sum::Party(p.to_string())),
1060 };
1061 let text = |t: &str| pb::Value {
1062 sum: Some(pb::value::Sum::Text(t.to_string())),
1063 };
1064 let id = pb::Identifier {
1065 package_id: "a50e65b2".to_string(),
1066 module_name: "ContractKeys".to_string(),
1067 entity_name: "Steward".to_string(),
1068 };
1069 let created = pb::CreatedEvent {
1070 offset: 16376,
1071 node_id: 1,
1072 contract_id: "0068d4".to_string(),
1073 template_id: Some(id.clone()),
1074 create_arguments: Some(pb::Record {
1075 record_id: None,
1076 fields: vec![
1077 pb::RecordField {
1078 label: "steward".to_string(),
1079 value: Some(party("alice::1220")),
1080 },
1081 pb::RecordField {
1082 label: "charter".to_string(),
1083 value: Some(text("v2")),
1084 },
1085 ],
1086 }),
1087 witness_parties: vec!["alice::1220".to_string()],
1088 package_name: "contractkeys-h61a17c769e62".to_string(),
1089 created_at: Some(prost_types::Timestamp {
1090 seconds: 1_792_853_548,
1091 nanos: 949_667_000,
1092 }),
1093 ..Default::default()
1094 };
1095 let tx = pb::Transaction {
1096 update_id: "1220d4c7".to_string(),
1097 offset: 16376,
1098 events: vec![pb::Event {
1099 event: Some(pb::event::Event::Created(created)),
1100 }],
1101 ..Default::default()
1102 };
1103 let update = Update::from_grpc(pb::get_updates_response::Update::Transaction(tx)).unwrap();
1104 let created = update.as_transaction().unwrap().created().next().unwrap();
1105 assert_eq!(
1106 created.template_id.to_string(),
1107 "a50e65b2:ContractKeys:Steward"
1108 );
1109 assert_eq!(
1110 created.created_at,
1111 Some(rt::Timestamp(1_792_853_548_949_667))
1112 );
1113 assert_eq!(created.decode::<Steward>().unwrap().charter, "v2");
1114 assert!(created.as_grpc().is_some());
1115 }
1116
1117 #[test]
1118 fn a_checkpoint_and_the_bare_ws_shape_read_too() {
1119 let checkpoint = json!({"update": {"OffsetCheckpoint": {"value": {
1120 "offset": 99,
1121 "synchronizerTimes": [{"synchronizerId": "s::1", "recordTime": "2026-10-23T14:52:29Z"}]
1122 }}}});
1123 let Update::OffsetCheckpoint(c) = Update::from_json(&checkpoint).unwrap() else {
1124 panic!("a checkpoint");
1125 };
1126 assert_eq!(c.offset, 99);
1127 assert_eq!(c.synchronizer_times[0].synchronizer_id, "s::1");
1128
1129 let bare = json_transaction()["update"].clone();
1131 assert_eq!(Update::from_json(&bare).unwrap().offset(), 16376);
1132 }
1133
1134 #[test]
1135 fn a_malformed_element_is_an_error_not_a_default() {
1136 assert!(Update::from_json(&json!({"update": {"Mystery": {"value": {}}}})).is_err());
1137 assert!(Update::from_json(&json!({})).is_err());
1138 let mut no_id = json_transaction();
1139 no_id["update"]["Transaction"]["value"]["events"][1]["CreatedEvent"]["templateId"] =
1140 json!("not-an-id");
1141 assert!(Update::from_json(&no_id).is_err());
1142 }
1143}