Skip to main content

canton_ledger/
update.rs

1//! Updates as the SDK's own types, the same whichever transport read them.
2//!
3//! The update reads hand out what the wire carries: `prost` messages on gRPC,
4//! JSON values on the JSON Ledger API. Code written against one does not read
5//! the other, and code that matches on the `prost` types follows every
6//! re-vendored proto without a version signal (ADR-0002, ADR-0010 §4). The
7//! types here are the envelope both transports describe, built from either:
8//!
9//! ```no_run
10//! # async fn run(client: canton_ledger::CantonClient, party: String)
11//! #     -> canton_ledger::Result<()> {
12//! use canton_ledger::update::{Event, Update};
13//! use tokio_stream::StreamExt as _;
14//!
15//! let stream = client
16//!     .updates_typed_with(canton_ledger::UpdatesRequest::new(vec![party], 0))
17//!     .await?;
18//! tokio::pin!(stream);
19//! while let Some(update) = stream.next().await {
20//!     if let Update::Transaction(tx) = update? {
21//!         for event in &tx.events {
22//!             if let Event::Created(created) = event {
23//!                 println!("{} created {}", tx.update_id, created.contract_id);
24//!             }
25//!         }
26//!     }
27//! }
28//! # Ok(()) }
29//! ```
30//!
31//! A payload is decoded into a generated type with
32//! [`CreatedEvent::decode`], which runs the identity checks of
33//! [`Template::from_created_event`](canton_daml::Template::from_created_event)
34//! on either transport. The wire form stays reachable
35//! ([`CreatedEvent::as_grpc`], [`CreatedEvent::as_json`]) for what these
36//! types do not model.
37
38use 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/// A template, interface or choice-owner id: `package:Module:Entity`.
44#[derive(Clone, Debug, PartialEq, Eq, Hash)]
45#[non_exhaustive]
46pub struct Identifier {
47    /// The package id (or a `#<package-name>` reference).
48    pub package_id: String,
49    /// The dotted module name.
50    pub module_name: String,
51    /// The entity name.
52    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    /// `package:Module:Entity`, as the JSON API spells it. Module names are
65    /// dotted and entity names carry no colon, so the package is what precedes
66    /// the last two parts.
67    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/// One update, as a stream, a page or a point read returns it.
97#[derive(Clone, Debug)]
98#[non_exhaustive]
99pub enum Update {
100    /// A committed transaction.
101    Transaction(Transaction),
102    /// A contract moving between synchronizers.
103    Reassignment(Reassignment),
104    /// A topology change affecting the subscribed parties.
105    TopologyTransaction(TopologyTransaction),
106    /// A position the participant names as safe to resume from; carries no
107    /// change.
108    OffsetCheckpoint(OffsetCheckpoint),
109}
110
111impl Update {
112    /// The ledger offset of the update.
113    #[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    /// The update id; a checkpoint has none.
124    #[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    /// The transaction, when this update is one.
135    #[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    /// An element of a gRPC update stream or page.
144    ///
145    /// # Errors
146    /// [`Error::UnexpectedResponse`] if the update is malformed.
147    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    /// The result of a gRPC point read
160    /// ([`update_by_id`](crate::CantonClient::update_by_id),
161    /// [`update_by_offset`](crate::CantonClient::update_by_offset)).
162    ///
163    /// # Errors
164    /// [`Error::UnexpectedResponse`] if the update is malformed.
165    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    /// An element as the JSON API returns it: `{"update": {"Transaction":
177    /// {"value": …}}}` from [`updates`](crate::JsonClient::updates) and the
178    /// point reads, or the inner `{"Transaction": {"value": …}}` a WebSocket
179    /// frame carries.
180    ///
181    /// # Errors
182    /// [`Error::UnexpectedResponse`] if the element is not an update the JSON
183    /// API describes.
184    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/// A committed transaction.
204#[derive(Clone, Debug)]
205#[non_exhaustive]
206pub struct Transaction {
207    /// The update id.
208    pub update_id: String,
209    /// The command id, visible to the submitting parties only.
210    pub command_id: String,
211    /// The workflow id, if one was set.
212    pub workflow_id: String,
213    /// The ledger offset.
214    pub offset: i64,
215    /// The synchronizer the transaction was committed on.
216    pub synchronizer_id: String,
217    /// Ledger effective time.
218    pub effective_at: Option<rt::Timestamp>,
219    /// The synchronizer's record time.
220    pub record_time: Option<rt::Timestamp>,
221    /// The events visible to the subscribed parties, in node order.
222    pub events: Vec<Event>,
223}
224
225impl Transaction {
226    /// A gRPC transaction, as a stream, a point read or `submit-and-wait`
227    /// returns it.
228    ///
229    /// # Errors
230    /// [`Error::UnexpectedResponse`] if an event is malformed.
231    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    /// A JSON transaction (the `value` of a `Transaction` update, or the
251    /// `transaction` of a `submit-and-wait-for-transaction` response).
252    ///
253    /// # Errors
254    /// [`Error::UnexpectedResponse`] if a field is malformed.
255    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    /// The created events.
280    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    /// The archived events.
288    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    /// The exercised events (present under the ledger-effects shape).
296    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/// An event of a transaction.
305#[derive(Clone, Debug)]
306#[non_exhaustive]
307pub enum Event {
308    /// A contract was created.
309    Created(CreatedEvent),
310    /// A contract was archived (the ACS-delta shape).
311    Archived(ArchivedEvent),
312    /// A choice was exercised (the ledger-effects shape).
313    Exercised(ExercisedEvent),
314}
315
316impl Event {
317    /// The offset of the transaction the event belongs to.
318    #[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    /// The contract the event is about.
328    #[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    /// The template of that contract.
338    #[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/// The wire form an event was read from, kept so a payload can be decoded by
371/// the codec of the transport that carried it.
372#[derive(Clone, Debug)]
373enum Wire<G> {
374    Grpc(Box<G>),
375    Json(Box<Value>),
376}
377
378/// A contract was created.
379#[derive(Clone, Debug)]
380#[non_exhaustive]
381pub struct CreatedEvent {
382    /// The offset of the transaction.
383    pub offset: i64,
384    /// The node id within the transaction.
385    pub node_id: i32,
386    /// The new contract's id.
387    pub contract_id: String,
388    /// Its template.
389    pub template_id: Identifier,
390    /// The package name of its template.
391    pub package_name: String,
392    /// The subscribed parties the event is visible to.
393    pub witness_parties: Vec<String>,
394    /// The contract's signatories.
395    pub signatories: Vec<String>,
396    /// The contract's observers.
397    pub observers: Vec<String>,
398    /// When the contract was created.
399    pub created_at: Option<rt::Timestamp>,
400    /// The blob that lets another party submit with this contract disclosed;
401    /// empty unless the read asked for it.
402    pub created_event_blob: Vec<u8>,
403    wire: Wire<pb::CreatedEvent>,
404}
405
406impl CreatedEvent {
407    /// Decode the payload as the generated type `T`, checking first that the
408    /// event is `T` (module, entity and package name; the package id is not
409    /// compared, so an upgraded version decodes).
410    ///
411    /// # Errors
412    /// [`ValueError`](rt::ValueError) if the event is another template or its
413    /// payload does not decode as `T`.
414    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    /// The event as gRPC delivered it, if it came over gRPC.
425    #[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    /// The event as the JSON API delivered it, if it came over JSON.
434    #[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/// A contract was archived.
491#[derive(Clone, Debug)]
492#[non_exhaustive]
493pub struct ArchivedEvent {
494    /// The offset of the transaction.
495    pub offset: i64,
496    /// The node id within the transaction.
497    pub node_id: i32,
498    /// The archived contract's id.
499    pub contract_id: String,
500    /// Its template.
501    pub template_id: Identifier,
502    /// The package name of its template.
503    pub package_name: String,
504    /// The subscribed parties the event is visible to.
505    pub witness_parties: Vec<String>,
506    /// The interfaces the template implements that the read filtered on.
507    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/// A Daml value as the transport carried it, decoded on demand.
546#[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/// A choice was exercised.
566#[derive(Clone, Debug)]
567#[non_exhaustive]
568pub struct ExercisedEvent {
569    /// The offset of the transaction.
570    pub offset: i64,
571    /// The node id within the transaction.
572    pub node_id: i32,
573    /// The contract the choice was exercised on.
574    pub contract_id: String,
575    /// Its template.
576    pub template_id: Identifier,
577    /// The interface the choice was exercised through, if any.
578    pub interface_id: Option<Identifier>,
579    /// The choice.
580    pub choice: String,
581    /// Whether the choice consumed the contract.
582    pub consuming: bool,
583    /// The parties that exercised it.
584    pub acting_parties: Vec<String>,
585    /// The subscribed parties the event is visible to.
586    pub witness_parties: Vec<String>,
587    /// The last node of the exercise's subtree.
588    pub last_descendant_node_id: i32,
589    /// The package name of the template.
590    pub package_name: String,
591    argument: Option<WireValue>,
592    result: Option<WireValue>,
593}
594
595impl ExercisedEvent {
596    /// Decode the choice argument as `A` (the generated choice type).
597    ///
598    /// # Errors
599    /// [`ValueError`](rt::ValueError) if there is no argument or it does not
600    /// decode as `A`.
601    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    /// Decode the choice's result as `R`; `None` when the event carries none.
612    #[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/// A contract moving between synchronizers. Its events are reachable in the
676/// wire form ([`Reassignment::as_grpc`], [`Reassignment::as_json`]).
677#[derive(Clone, Debug)]
678#[non_exhaustive]
679pub struct Reassignment {
680    /// The update id.
681    pub update_id: String,
682    /// The command id, visible to the submitting parties only.
683    pub command_id: String,
684    /// The workflow id, if one was set.
685    pub workflow_id: String,
686    /// The ledger offset.
687    pub offset: i64,
688    /// The synchronizer's record time.
689    pub record_time: Option<rt::Timestamp>,
690    wire: Wire<pb::Reassignment>,
691}
692
693impl Reassignment {
694    /// The reassignment as gRPC delivered it, if it came over gRPC.
695    #[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    /// The reassignment as the JSON API delivered it, if it came over JSON.
704    #[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/// A topology change affecting the subscribed parties. Its events are
736/// reachable in the wire form.
737#[derive(Clone, Debug)]
738#[non_exhaustive]
739pub struct TopologyTransaction {
740    /// The update id.
741    pub update_id: String,
742    /// The ledger offset.
743    pub offset: i64,
744    /// The synchronizer the change was sequenced on.
745    pub synchronizer_id: String,
746    /// The synchronizer's record time.
747    pub record_time: Option<rt::Timestamp>,
748    wire: Wire<pb::TopologyTransaction>,
749}
750
751impl TopologyTransaction {
752    /// The topology transaction as gRPC delivered it, if it came over gRPC.
753    #[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    /// The topology transaction as the JSON API delivered it, if it came over
762    /// JSON.
763    #[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/// A position the participant names as safe to resume from.
793#[derive(Clone, Debug, PartialEq, Eq)]
794#[non_exhaustive]
795pub struct OffsetCheckpoint {
796    /// The offset.
797    pub offset: i64,
798    /// The record time each synchronizer has reached.
799    pub synchronizer_times: Vec<SynchronizerTime>,
800}
801
802/// A synchronizer's record time at a checkpoint.
803#[derive(Clone, Debug, PartialEq, Eq)]
804#[non_exhaustive]
805pub struct SynchronizerTime {
806    /// The synchronizer.
807    pub synchronizer_id: String,
808    /// Its record time.
809    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    /// A minimal template, enough to drive `decode` on both transports.
929    #[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    /// The choice argument, decoded through the JSON codec in these tests.
969    #[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    /// The shape the LocalNet's JSON API returned for a keyed exercise under
980    /// the ledger-effects shape (offsets and ids shortened).
981    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        // A WebSocket frame carries the update without the outer `update` key.
1130        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}