Skip to main content

canton_ledger/
interactive.rs

1//! Interactive submission: prepare, sign elsewhere, execute.
2//!
3//! An ordinary submission asks the participant to interpret *and* authorize a
4//! command, which means the participant holds the party's key. Interactive
5//! submission splits that: the participant interprets and returns the
6//! transaction with its hash, the key signs the hash wherever it lives, and the
7//! signature comes back to be executed.
8//!
9//! The three types here are the three states of that flow, and each one only
10//! offers what is possible in it:
11//!
12//! * [`Prepare`] — what to interpret. Goes to
13//!   [`prepare_submission`](crate::CantonClient::prepare_submission).
14//! * [`Prepared`] — the interpreted transaction and its hash. Sign it.
15//! * [`Executable`] — signed, and therefore submittable.
16//!
17//! # The window
18//!
19//! A prepared transaction does not stay executable indefinitely. If it depends
20//! on time, it must be executed within the ledger-time tolerance of the ledger
21//! time assigned during preparation — usually about a minute. If it does not,
22//! the bound is the much larger preparation-time skew, in the order of hours.
23//! Holding a `Prepared` across a long signing ceremony is a design decision to
24//! make deliberately, not an accident to discover in production.
25
26use canton_core::{Error, Result};
27use canton_proto::com::daml::ledger::api::v2 as pb;
28// The interactive submission service has a package of its own; `pb` stays
29// the root, where `Command`, `Signature` and the transaction types live.
30use canton_proto::com::daml::ledger::api::v2::interactive as ipb;
31use canton_signer::Signer;
32
33/// A submission to be interpreted but not authorized: the *prepare* half of
34/// interactive submission.
35///
36/// This carries what `PrepareSubmissionRequest` carries, and nothing else.
37/// [`Submit`](crate::Submit) is deliberately not reused: it has a
38/// `deduplication` period and a `workflow_id` that preparation has no field
39/// for, and accepting them here would silently drop them. The de-duplication
40/// period, the transaction shape and the submission id belong to *execution*
41/// and are set on [`Executable`]. In the other direction, preparation has four
42/// fields a plain submission does not: `verbose_hashing`, `max_record_time`,
43/// the hashing scheme, and traffic-cost estimation.
44#[derive(Clone, Debug)]
45pub struct Prepare {
46    act_as: Vec<String>,
47    commands: Vec<pb::Command>,
48    command_id: Option<String>,
49    user_id: Option<String>,
50    read_as: Vec<String>,
51    synchronizer_id: Option<String>,
52    disclosed_contracts: Vec<pb::DisclosedContract>,
53    package_id_selection_preference: Vec<String>,
54    prefetch_contract_keys: Vec<pb::PrefetchContractKey>,
55    min_ledger_time_abs: Option<prost_types::Timestamp>,
56    min_ledger_time_rel: Option<std::time::Duration>,
57    verbose_hashing: bool,
58    max_record_time: Option<prost_types::Timestamp>,
59    hashing_scheme_version: Option<i32>,
60    taps_max_passes: Option<u32>,
61    estimate_traffic_cost: bool,
62}
63
64impl Prepare {
65    /// Prepare a submission acting as a single party.
66    pub fn new(act_as: impl Into<String>) -> Self {
67        Self::new_multi(vec![act_as.into()])
68    }
69
70    /// Prepare a submission acting as several parties.
71    ///
72    /// Each of them signs: the participant requires a signature per party in
73    /// `act_as`, so a transaction prepared for two parties needs two.
74    #[must_use]
75    pub fn new_multi(act_as: Vec<String>) -> Self {
76        Self {
77            act_as,
78            commands: Vec::new(),
79            command_id: None,
80            user_id: None,
81            read_as: Vec::new(),
82            synchronizer_id: None,
83            disclosed_contracts: Vec::new(),
84            package_id_selection_preference: Vec::new(),
85            prefetch_contract_keys: Vec::new(),
86            min_ledger_time_abs: None,
87            min_ledger_time_rel: None,
88            verbose_hashing: false,
89            max_record_time: None,
90            hashing_scheme_version: None,
91            taps_max_passes: None,
92            // The participant estimates by default; only an explicit opt-out
93            // is sent, so leaving this alone changes nothing.
94            estimate_traffic_cost: true,
95        }
96    }
97
98    /// Add a command — build them with
99    /// [`create`](crate::create) / [`exercise`](crate::exercise), or from
100    /// generated bindings.
101    #[must_use]
102    pub fn add_command(mut self, command: pb::Command) -> Self {
103        self.commands.push(command);
104        self
105    }
106
107    /// Add several commands, executed atomically.
108    #[must_use]
109    pub fn add_commands(mut self, commands: impl IntoIterator<Item = pb::Command>) -> Self {
110        self.commands.extend(commands);
111        self
112    }
113
114    /// Set the command id, one component of the change ID the Ledger API
115    /// de-duplicates on (`user_id`, `act_as`, `command_id`).
116    ///
117    /// Left unset, a fresh UUID is generated at prepare time and *carried
118    /// through execution*, so re-executing the same [`Executable`] is
119    /// de-duplicated rather than committed twice.
120    #[must_use]
121    pub fn with_command_id(mut self, command_id: impl Into<String>) -> Self {
122        self.command_id = Some(command_id.into());
123        self
124    }
125
126    /// Set the user id. Defaults to the one derived from the bearer token.
127    #[must_use]
128    pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
129        self.user_id = Some(user_id.into());
130        self
131    }
132
133    /// Parties to read as, in addition to those acting.
134    #[must_use]
135    pub fn with_read_as(mut self, read_as: Vec<String>) -> Self {
136        self.read_as = read_as;
137        self
138    }
139
140    /// Pin the submission to a synchronizer.
141    #[must_use]
142    pub fn with_synchronizer_id(mut self, synchronizer_id: impl Into<String>) -> Self {
143        self.synchronizer_id = Some(synchronizer_id.into());
144        self
145    }
146
147    /// Attach explicitly disclosed contracts — the `created_event_blob` path
148    /// for contracts the submitting party cannot see on its own.
149    #[must_use]
150    pub fn with_disclosed_contracts(mut self, contracts: Vec<pb::DisclosedContract>) -> Self {
151        self.disclosed_contracts = contracts;
152        self
153    }
154
155    /// Prefer these package ids when several satisfy a package-name reference.
156    #[must_use]
157    pub fn with_package_id_selection_preference(mut self, package_ids: Vec<String>) -> Self {
158        self.package_id_selection_preference = package_ids;
159        self
160    }
161
162    /// Fetch these contract keys into the participant's cache before
163    /// interpretation.
164    #[must_use]
165    pub fn with_prefetch_contract_keys(mut self, keys: Vec<pb::PrefetchContractKey>) -> Self {
166        self.prefetch_contract_keys = keys;
167        self
168    }
169
170    /// Lower bound for the transaction's ledger time, absolute.
171    ///
172    /// Mutually exclusive with [`with_min_ledger_time_rel`](Self::with_min_ledger_time_rel);
173    /// the participant rejects both at once.
174    #[must_use]
175    pub fn with_min_ledger_time_abs(mut self, time: prost_types::Timestamp) -> Self {
176        self.min_ledger_time_abs = Some(time);
177        // Clearing the other one is what makes "the later call wins" true. The
178        // wire has a `oneof`, so keeping both would mean silently dropping
179        // whichever this code happened to prefer.
180        self.min_ledger_time_rel = None;
181        self
182    }
183
184    /// Lower bound for the transaction's ledger time, relative to when the
185    /// participant receives the request.
186    ///
187    /// Mutually exclusive with [`with_min_ledger_time_abs`](Self::with_min_ledger_time_abs).
188    #[must_use]
189    pub fn with_min_ledger_time_rel(mut self, duration: std::time::Duration) -> Self {
190        self.min_ledger_time_rel = Some(duration);
191        self.min_ledger_time_abs = None;
192        self
193    }
194
195    /// Ask the participant to return the hashing details it used.
196    ///
197    /// For debugging a hash mismatch. The participant may refuse — it is
198    /// configurable server-side — and what comes back describes internal
199    /// structure, so it is off by default.
200    #[must_use]
201    pub fn with_verbose_hashing(mut self, verbose: bool) -> Self {
202        self.verbose_hashing = verbose;
203        self
204    }
205
206    /// Bound how long the prepared transaction can be executed for.
207    ///
208    /// Past this record time the participant will certainly refuse it, which is
209    /// what makes preparing a fresh transaction for the same intent safe again:
210    /// without a bound, "it might still commit" and "prepare another" are both
211    /// true at once.
212    #[must_use]
213    pub fn with_max_record_time(mut self, time: prost_types::Timestamp) -> Self {
214        self.max_record_time = Some(time);
215        self
216    }
217
218    /// Choose the hashing scheme (defaults to the participant's, `V2`).
219    ///
220    /// The scheme decides what the hash covers, so it is echoed in the response
221    /// and sent back with the signature — the participant must verify under the
222    /// same scheme it hashed with.
223    #[must_use]
224    pub fn with_hashing_scheme_version(mut self, version: ipb::HashingSchemeVersion) -> Self {
225        self.hashing_scheme_version = Some(version as i32);
226        self
227    }
228
229    /// Cap the topology-aware package selection passes (defaults to the
230    /// participant's configured value).
231    #[must_use]
232    pub fn with_taps_max_passes(mut self, passes: u32) -> Self {
233        self.taps_max_passes = Some(passes);
234        self
235    }
236
237    /// Turn off the traffic-cost estimate the participant returns by default.
238    #[must_use]
239    pub fn without_traffic_cost_estimation(mut self) -> Self {
240        self.estimate_traffic_cost = false;
241        self
242    }
243
244    /// The parties this will act as.
245    #[must_use]
246    pub fn act_as(&self) -> &[String] {
247        &self.act_as
248    }
249
250    /// The parties this will read as, beyond those acting.
251    #[must_use]
252    pub fn read_as(&self) -> &[String] {
253        &self.read_as
254    }
255
256    /// The contracts this will disclose.
257    ///
258    /// A token-standard choice is meaningless without the registry's
259    /// disclosures, so a caller assembling one wants to see them before
260    /// sending, not after a rejection.
261    #[must_use]
262    pub fn disclosed_contracts(&self) -> &[pb::DisclosedContract] {
263        &self.disclosed_contracts
264    }
265
266    /// Build the wire request, returning the command id that went into it.
267    ///
268    /// The id is returned rather than read back off the request so a caller can
269    /// still name the change ID after preparation — to recover a completion, or
270    /// to prepare the same command again deliberately. Execution does not carry
271    /// it as a field: the participant reads it from the prepared transaction's
272    /// own `submitter_info`, which is what makes a repeated execution a
273    /// duplicate rather than a second transaction.
274    pub(crate) fn into_request(self) -> (String, Option<String>, ipb::PrepareSubmissionRequest) {
275        let command_id = self
276            .command_id
277            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
278        let user_id = self.user_id;
279        let min_ledger_time = min_ledger_time(self.min_ledger_time_abs, self.min_ledger_time_rel);
280        let request = ipb::PrepareSubmissionRequest {
281            user_id: user_id.clone().unwrap_or_default(),
282            command_id: command_id.clone(),
283            commands: self.commands,
284            min_ledger_time,
285            act_as: self.act_as,
286            read_as: self.read_as,
287            disclosed_contracts: self.disclosed_contracts,
288            synchronizer_id: self.synchronizer_id.unwrap_or_default(),
289            package_id_selection_preference: self.package_id_selection_preference,
290            verbose_hashing: self.verbose_hashing,
291            prefetch_contract_keys: self.prefetch_contract_keys,
292            max_record_time: self.max_record_time,
293            hashing_scheme_version: self.hashing_scheme_version,
294            taps_max_passes: self.taps_max_passes,
295            estimate_traffic_cost: (!self.estimate_traffic_cost).then_some(
296                ipb::CostEstimationHints {
297                    disabled: true,
298                    // Only meaningful when estimation is on, and it is off here.
299                    expected_signatures: Vec::new(),
300                },
301            ),
302        };
303        (command_id, user_id, request)
304    }
305}
306
307/// A `std::time::Duration` as protobuf carries one.
308///
309/// Saturating rather than wrapping: a duration past `i64::MAX` seconds is a
310/// caller mistake, and the participant refusing a very long period is a better
311/// outcome than a negative one it accepts.
312fn duration(value: std::time::Duration) -> prost_types::Duration {
313    prost_types::Duration {
314        seconds: i64::try_from(value.as_secs()).unwrap_or(i64::MAX),
315        nanos: i32::try_from(value.subsec_nanos()).unwrap_or(0),
316    }
317}
318
319/// `MinLedgerTime` is a `oneof`, so at most one of the two can be set.
320///
321/// Both set is rejected by the participant; expressing it as a `oneof` here
322/// means the later call simply wins rather than producing a request that
323/// cannot succeed.
324fn min_ledger_time(
325    abs: Option<prost_types::Timestamp>,
326    rel: Option<std::time::Duration>,
327) -> Option<ipb::MinLedgerTime> {
328    use ipb::min_ledger_time::Time;
329    let time = match (abs, rel) {
330        (Some(abs), _) => Time::MinLedgerTimeAbs(abs),
331        (None, Some(rel)) => Time::MinLedgerTimeRel(duration(rel)),
332        (None, None) => return None,
333    };
334    Some(ipb::MinLedgerTime { time: Some(time) })
335}
336
337/// An interpreted transaction, waiting for signatures.
338///
339/// [`hash`](Self::hash) is what gets signed — it is the participant's hash of
340/// the transaction, not a message to hash again.
341///
342/// # What signing this does and does not prove
343///
344/// The hash is the **preparing participant's word**. This crate does not
345/// recompute it from [`transaction`](Self::transaction), so signing a
346/// `Prepared` authorizes whatever that participant hashed, which is not
347/// necessarily what [`transaction`](Self::transaction) shows.
348///
349/// The Ledger API is explicit about the two obligations that follow, and both
350/// are the caller's:
351///
352/// * show the transaction to whoever authorizes it, before signing;
353/// * **recompute the hash from the transaction if the participant is not
354///   trusted** — the API calls the hash "provided for convenience" for exactly
355///   this reason.
356///
357/// Interactive submission keeps the key away from the participant. It does not,
358/// on its own, make a dishonest participant harmless.
359#[derive(Clone, Debug)]
360pub struct Prepared {
361    transaction: Option<ipb::PreparedTransaction>,
362    hash: Vec<u8>,
363    hashing_scheme_version: i32,
364    hashing_details: Option<String>,
365    cost_estimation: Option<ipb::CostEstimation>,
366    act_as: Vec<String>,
367    read_as: Vec<String>,
368    command_id: String,
369    user_id: Option<String>,
370}
371
372impl Prepared {
373    pub(crate) fn from_response(
374        response: ipb::PrepareSubmissionResponse,
375        act_as: Vec<String>,
376        read_as: Vec<String>,
377        command_id: String,
378        user_id: Option<String>,
379    ) -> Result<Self> {
380        // The transaction is what the proto marks `Required`, and it is where
381        // the change ID lives — without it an execute request is invalid and
382        // de-duplicates against nothing. The hash beside it is documented as
383        // "provided for convenience" and "may be removed in future versions",
384        // so checking only that one guards the wrong field.
385        if response.prepared_transaction.is_none() {
386            return Err(Error::UnexpectedResponse(
387                "prepare returned no prepared transaction, so there is nothing to execute"
388                    .to_string(),
389            ));
390        }
391        if response.prepared_transaction_hash.is_empty() {
392            return Err(Error::UnexpectedResponse(
393                "prepare returned no transaction hash".to_string(),
394            ));
395        }
396        Ok(Self {
397            transaction: response.prepared_transaction,
398            hash: response.prepared_transaction_hash,
399            hashing_scheme_version: response.hashing_scheme_version,
400            hashing_details: response.hashing_details,
401            cost_estimation: response.cost_estimation,
402            act_as,
403            read_as,
404            command_id,
405            user_id,
406        })
407    }
408
409    /// The hash to sign.
410    #[must_use]
411    pub fn hash(&self) -> &[u8] {
412        &self.hash
413    }
414
415    /// The interpreted transaction, for a caller that wants to inspect what it
416    /// is about to authorize before signing it.
417    #[must_use]
418    pub fn transaction(&self) -> Option<&ipb::PreparedTransaction> {
419        self.transaction.as_ref()
420    }
421
422    /// The change ID's command component, carried from preparation.
423    #[must_use]
424    pub fn command_id(&self) -> &str {
425        &self.command_id
426    }
427
428    /// The identity the participant de-duplicates this submission by —
429    /// (`user_id`, `act_as`, `command_id`). Keep it: it is what
430    /// [`CantonClient::await_completion`](crate::CantonClient::await_completion)
431    /// recovers an ambiguous execute by, and `Executable` is consumed on
432    /// execute.
433    #[must_use]
434    pub fn change_id(&self) -> crate::command::ChangeId {
435        crate::command::ChangeId::new(
436            self.user_id.clone().unwrap_or_default(),
437            self.act_as.clone(),
438            self.command_id.clone(),
439        )
440    }
441
442    /// The parties that must sign.
443    #[must_use]
444    pub fn act_as(&self) -> &[String] {
445        &self.act_as
446    }
447
448    /// What the participant says about how it hashed, when preparation asked
449    /// for it with [`Prepare::with_verbose_hashing`].
450    ///
451    /// For troubleshooting a hash mismatch and nothing else: the proto is
452    /// explicit that neither the format nor the content is stable, so this is
453    /// a string to read, never one to parse.
454    #[must_use]
455    pub fn hashing_details(&self) -> Option<&str> {
456        self.hashing_details.as_deref()
457    }
458
459    /// The participant's estimate of what executing this will cost in traffic,
460    /// unless preparation turned the estimate off.
461    #[must_use]
462    pub fn cost_estimation(&self) -> Option<&ipb::CostEstimation> {
463        self.cost_estimation.as_ref()
464    }
465
466    /// The hashing scheme the participant hashed under, echoed back with the
467    /// signature so it verifies under the same one.
468    #[must_use]
469    pub fn hashing_scheme_version(&self) -> ipb::HashingSchemeVersion {
470        ipb::HashingSchemeVersion::try_from(self.hashing_scheme_version)
471            .unwrap_or(ipb::HashingSchemeVersion::Unspecified)
472    }
473
474    /// Sign as the single acting party.
475    ///
476    /// # Errors
477    /// [`Error::InvalidRequest`] if the submission acts as more than one party
478    /// — then it is ambiguous which one this signature is for, and
479    /// [`sign_as`](Self::sign_as) says so explicitly.
480    pub async fn sign_with(self, signer: &dyn Signer) -> Result<Executable> {
481        let [party] = self.act_as.as_slice() else {
482            return Err(Error::InvalidRequest(format!(
483                "this submission acts as {} parties, so a signature must name \
484                 one: use `sign_as`",
485                self.act_as.len()
486            )));
487        };
488        let party = party.clone();
489        self.sign_as(&party, signer).await
490    }
491
492    /// Sign on behalf of `party`.
493    ///
494    /// # Errors
495    /// [`Error::InvalidRequest`] if `party` is not among the acting parties —
496    /// the participant would reject the execution, and catching it here says
497    /// which party was meant. Otherwise, whatever the signer returns.
498    pub async fn sign_as(self, party: &str, signer: &dyn Signer) -> Result<Executable> {
499        let signature = self.sign_one(party, signer).await?;
500        Ok(Executable {
501            prepared: self,
502            signatures: vec![signature],
503            submission_id: None,
504            transaction_shape: crate::request::TransactionShape::default(),
505            deduplication: None,
506        })
507    }
508
509    async fn sign_one(
510        &self,
511        party: &str,
512        signer: &dyn Signer,
513    ) -> Result<ipb::SinglePartySignatures> {
514        if !self.act_as.iter().any(|p| p == party) {
515            return Err(Error::InvalidRequest(format!(
516                "`{party}` is not among the parties this submission acts as ({})",
517                self.act_as.join(", ")
518            )));
519        }
520        let signature = signer.sign(&self.hash).await?;
521        Ok(ipb::SinglePartySignatures {
522            party: party.to_string(),
523            signatures: vec![pb::Signature::from(&signature)],
524        })
525    }
526
527    /// Every party this reads as, beyond those acting.
528    #[must_use]
529    pub fn read_as(&self) -> &[String] {
530        &self.read_as
531    }
532}
533
534/// How long a change ID stays a duplicate.
535///
536/// On the wire this is a `oneof` per execute variant, so it is held in one
537/// neutral form here and converted at the point of sending.
538#[derive(Clone, Copy, Debug)]
539enum Deduplication {
540    Duration(std::time::Duration),
541    Offset(i64),
542}
543
544/// A prepared transaction with the signatures that authorize it.
545#[derive(Clone, Debug)]
546pub struct Executable {
547    prepared: Prepared,
548    signatures: Vec<ipb::SinglePartySignatures>,
549    submission_id: Option<String>,
550    transaction_shape: crate::request::TransactionShape,
551    deduplication: Option<Deduplication>,
552}
553
554impl Executable {
555    /// Add another party's signature — for a multi-party submission, where the
556    /// participant requires one per acting party.
557    ///
558    /// # Errors
559    /// As [`Prepared::sign_as`].
560    pub async fn and_sign_as(mut self, party: &str, signer: &dyn Signer) -> Result<Self> {
561        let signature = self.prepared.sign_one(party, signer).await?;
562        self.add(signature);
563        Ok(self)
564    }
565
566    /// Fold a signature into the entry for its party.
567    ///
568    /// The wire has one entry per party carrying *several* signatures — which
569    /// is how a party whose topology sets a threshold above one authorizes at
570    /// all. Pushing a second entry for the same party would send a shape the
571    /// standard does not describe, and would make the threshold case
572    /// unreachable.
573    fn add(&mut self, signature: ipb::SinglePartySignatures) {
574        if let Some(existing) = self
575            .signatures
576            .iter_mut()
577            .find(|entry| entry.party == signature.party)
578        {
579            existing.signatures.extend(signature.signatures);
580        } else {
581            self.signatures.push(signature);
582        }
583    }
584
585    /// Attach a signature produced elsewhere — an offline ceremony, or a
586    /// service that returns signatures rather than implementing
587    /// [`Signer`].
588    ///
589    /// # Errors
590    /// [`Error::InvalidRequest`] if `party` is not among the acting parties.
591    pub fn with_signature(
592        mut self,
593        party: &str,
594        signature: &canton_signer::Signature,
595    ) -> Result<Self> {
596        if !self.prepared.act_as.iter().any(|p| p == party) {
597            return Err(Error::InvalidRequest(format!(
598                "`{party}` is not among the parties this submission acts as ({})",
599                self.prepared.act_as.join(", ")
600            )));
601        }
602        self.add(ipb::SinglePartySignatures {
603            party: party.to_string(),
604            signatures: vec![pb::Signature::from(signature)],
605        });
606        Ok(self)
607    }
608
609    /// Set the de-duplication period as a wall-clock duration: an execution
610    /// with the same change ID within this window is rejected as a duplicate.
611    ///
612    /// The change ID comes from preparation (`user_id`, `act_as`,
613    /// `command_id`), so this de-duplicates against ordinary submissions of the
614    /// same command too, not only against other executions.
615    #[must_use]
616    pub fn with_deduplication_duration(mut self, duration: std::time::Duration) -> Self {
617        self.deduplication = Some(Deduplication::Duration(duration));
618        self
619    }
620
621    /// Set the de-duplication period as a ledger offset: executions with the
622    /// same change ID since that offset are rejected as duplicates.
623    #[must_use]
624    pub fn with_deduplication_offset(mut self, offset: i64) -> Self {
625        self.deduplication = Some(Deduplication::Offset(offset));
626        self
627    }
628
629    /// Set the submission id — the identifier that distinguishes completions
630    /// for different submissions of the same change ID.
631    ///
632    /// Left unset, a fresh UUID is generated **per attempt**, which is what the
633    /// Ledger API expects of a retried submission. Setting one fixes it across
634    /// retries, which is a deliberate choice rather than a default.
635    #[must_use]
636    pub fn with_submission_id(mut self, submission_id: impl Into<String>) -> Self {
637        self.submission_id = Some(submission_id.into());
638        self
639    }
640
641    /// Which events the returned transaction carries — only meaningful for
642    /// [`execute_submission_and_wait_for_transaction`].
643    ///
644    /// [`execute_submission_and_wait_for_transaction`]:
645    ///     crate::CantonClient::execute_submission_and_wait_for_transaction
646    #[must_use]
647    pub fn with_transaction_shape(mut self, shape: crate::request::TransactionShape) -> Self {
648        self.transaction_shape = shape;
649        self
650    }
651
652    /// The command id, carried from preparation.
653    ///
654    /// The change id the participant will de-duplicate by; see
655    /// [`Prepared::change_id`].
656    #[must_use]
657    pub fn change_id(&self) -> crate::command::ChangeId {
658        self.prepared.change_id()
659    }
660
661    /// One component of the change ID (`user_id`, `act_as`, `command_id`), and
662    /// what [`await_completion`](crate::CantonClient::await_completion) matches
663    /// on — so a caller using the fire-and-forget
664    /// [`execute_submission`](crate::CantonClient::execute_submission) needs it
665    /// to recover the outcome.
666    #[must_use]
667    pub fn command_id(&self) -> &str {
668        &self.prepared.command_id
669    }
670
671    /// The parties that have signed so far.
672    #[must_use]
673    pub fn signed_by(&self) -> Vec<&str> {
674        self.signatures.iter().map(|s| s.party.as_str()).collect()
675    }
676
677    /// Every acting party that has not signed yet.
678    ///
679    /// The participant requires a signature per acting party; this is that
680    /// check, before the round trip that would report it as a rejection.
681    #[must_use]
682    pub fn unsigned_parties(&self) -> Vec<&str> {
683        self.prepared
684            .act_as
685            .iter()
686            .filter(|party| !self.signatures.iter().any(|s| &&s.party == party))
687            .map(String::as_str)
688            .collect()
689    }
690
691    /// The parts every execute variant shares.
692    fn parts(
693        self,
694    ) -> (
695        Option<ipb::PreparedTransaction>,
696        Option<ipb::PartySignatures>,
697        String,
698        String,
699        i32,
700        crate::request::TransactionShape,
701        Option<Deduplication>,
702    ) {
703        // Left as the caller set it — possibly empty. The client fills a fresh
704        // UUID per *attempt*, because the proto says "applications are expected
705        // to use a different UUID for each retry of a submission with the same
706        // change ID". Baking one in here would give every retry the same id and
707        // defeat the field's only purpose: telling those completions apart.
708        let submission_id = self.submission_id.unwrap_or_default();
709        (
710            self.prepared.transaction,
711            Some(ipb::PartySignatures {
712                signatures: self.signatures,
713            }),
714            submission_id,
715            self.prepared.user_id.unwrap_or_default(),
716            self.prepared.hashing_scheme_version,
717            self.transaction_shape,
718            self.deduplication,
719        )
720    }
721
722    pub(crate) fn into_execute_request(self) -> ipb::ExecuteSubmissionRequest {
723        use ipb::execute_submission_request::DeduplicationPeriod as P;
724        let (transaction, party_signatures, submission_id, user_id, hashing, _, dedup) =
725            self.parts();
726        ipb::ExecuteSubmissionRequest {
727            prepared_transaction: transaction,
728            party_signatures,
729            deduplication_period: dedup.map(|d| match d {
730                Deduplication::Duration(d) => P::DeduplicationDuration(duration(d)),
731                Deduplication::Offset(o) => P::DeduplicationOffset(o),
732            }),
733            submission_id,
734            user_id,
735            hashing_scheme_version: hashing,
736            min_ledger_time: None,
737        }
738    }
739
740    pub(crate) fn into_execute_and_wait_request(self) -> ipb::ExecuteSubmissionAndWaitRequest {
741        use ipb::execute_submission_and_wait_request::DeduplicationPeriod as P;
742        let (transaction, party_signatures, submission_id, user_id, hashing, _, dedup) =
743            self.parts();
744        ipb::ExecuteSubmissionAndWaitRequest {
745            prepared_transaction: transaction,
746            party_signatures,
747            deduplication_period: dedup.map(|d| match d {
748                Deduplication::Duration(d) => P::DeduplicationDuration(duration(d)),
749                Deduplication::Offset(o) => P::DeduplicationOffset(o),
750            }),
751            submission_id,
752            user_id,
753            hashing_scheme_version: hashing,
754            min_ledger_time: None,
755        }
756    }
757
758    pub(crate) fn into_execute_and_wait_for_transaction_request(
759        self,
760        event_format: pb::EventFormat,
761    ) -> ipb::ExecuteSubmissionAndWaitForTransactionRequest {
762        use ipb::execute_submission_and_wait_for_transaction_request::DeduplicationPeriod as P;
763        let (transaction, party_signatures, submission_id, user_id, hashing, shape, dedup) =
764            self.parts();
765        ipb::ExecuteSubmissionAndWaitForTransactionRequest {
766            prepared_transaction: transaction,
767            party_signatures,
768            deduplication_period: dedup.map(|d| match d {
769                Deduplication::Duration(d) => P::DeduplicationDuration(duration(d)),
770                Deduplication::Offset(o) => P::DeduplicationOffset(o),
771            }),
772            submission_id,
773            user_id,
774            hashing_scheme_version: hashing,
775            min_ledger_time: None,
776            transaction_format: Some(pb::TransactionFormat {
777                event_format: Some(event_format),
778                transaction_shape: shape.as_grpc() as i32,
779            }),
780        }
781    }
782
783    /// Every party the returned transaction should be filtered for.
784    ///
785    /// `act_as` *and* `read_as`: the Ledger API documents its default event
786    /// format as covering both, so filtering on the acting parties alone would
787    /// drop events only a reading party witnesses.
788    pub(crate) fn witnesses(&self) -> Vec<String> {
789        let mut parties = self.prepared.act_as.clone();
790        parties.extend(self.prepared.read_as.iter().cloned());
791        parties.sort_unstable();
792        parties.dedup();
793        parties
794    }
795}
796
797#[cfg(test)]
798#[allow(clippy::unwrap_used, clippy::expect_used)]
799mod tests {
800    use super::*;
801    use canton_signer::Ed25519Key;
802
803    fn signer(fingerprint: &str) -> canton_signer::Ed25519Signer {
804        Ed25519Key::from_seed(&[4; 32])
805            .expect("a valid seed")
806            .into_signer(fingerprint)
807    }
808
809    fn prepared(act_as: &[&str]) -> Prepared {
810        let act_as: Vec<String> = act_as.iter().map(ToString::to_string).collect();
811        Prepared::from_response(
812            ipb::PrepareSubmissionResponse {
813                // A real transaction, because `from_response` now requires one:
814                // it is where the change ID lives, and the fixture previously
815                // let six tests assert on execute requests that had none.
816                prepared_transaction: Some(ipb::PreparedTransaction {
817                    transaction: None,
818                    metadata: Some(ipb::Metadata {
819                        submitter_info: Some(ipb::metadata::SubmitterInfo {
820                            act_as: act_as.clone(),
821                            command_id: "command-1".to_string(),
822                        }),
823                        ..Default::default()
824                    }),
825                }),
826                prepared_transaction_hash: vec![1, 2, 3],
827                hashing_scheme_version: ipb::HashingSchemeVersion::V2 as i32,
828                hashing_details: None,
829                cost_estimation: None,
830            },
831            act_as,
832            Vec::new(),
833            "command-1".to_string(),
834            Some("user-1".to_string()),
835        )
836        .expect("a response carrying a transaction and a hash")
837    }
838
839    /// A response with no hash leaves nothing to sign, so it is an error here
840    /// rather than an empty signature the participant rejects later.
841    #[test]
842    fn a_prepare_response_without_a_hash_is_refused() {
843        let err = Prepared::from_response(
844            ipb::PrepareSubmissionResponse {
845                prepared_transaction: Some(ipb::PreparedTransaction::default()),
846                prepared_transaction_hash: Vec::new(),
847                hashing_scheme_version: 0,
848                hashing_details: None,
849                cost_estimation: None,
850            },
851            vec!["alice".to_string()],
852            Vec::new(),
853            "c".to_string(),
854            None,
855        )
856        .expect_err("no hash");
857        assert!(err.to_string().contains("no transaction hash"), "{err}");
858    }
859
860    /// And a response with no *transaction* is refused too — that is the field
861    /// the API marks required, and it carries the change ID. Checking only the
862    /// hash guarded the field the API calls "provided for convenience" and
863    /// documents as removable, while letting the required one through as an
864    /// execute request that de-duplicates against nothing.
865    #[test]
866    fn a_prepare_response_without_a_transaction_is_refused() {
867        let err = Prepared::from_response(
868            ipb::PrepareSubmissionResponse {
869                prepared_transaction: None,
870                prepared_transaction_hash: vec![1, 2, 3],
871                hashing_scheme_version: 0,
872                hashing_details: None,
873                cost_estimation: None,
874            },
875            vec!["alice".to_string()],
876            Vec::new(),
877            "c".to_string(),
878            None,
879        )
880        .expect_err("no transaction");
881        assert!(err.to_string().contains("nothing to execute"), "{err}");
882    }
883
884    #[tokio::test]
885    async fn signing_a_multi_party_submission_must_name_the_party() {
886        let err = prepared(&["alice", "bob"])
887            .sign_with(&signer("1220aa"))
888            .await
889            .expect_err("ambiguous");
890        assert!(err.to_string().contains("sign_as"), "{err}");
891    }
892
893    #[tokio::test]
894    async fn signing_as_a_party_that_is_not_acting_is_refused() {
895        let err = prepared(&["alice"])
896            .sign_as("mallory", &signer("1220aa"))
897            .await
898            .expect_err("not an acting party");
899        assert!(err.to_string().contains("mallory"), "{err}");
900        assert!(err.to_string().contains("alice"), "{err}");
901    }
902
903    /// The participant requires a signature per acting party. Asking locally
904    /// costs nothing; finding out from a rejection costs a round trip.
905    #[tokio::test]
906    async fn a_multi_party_submission_reports_who_has_not_signed() {
907        let executable = prepared(&["alice", "bob"])
908            .sign_as("alice", &signer("1220aa"))
909            .await
910            .expect("alice signs");
911        assert_eq!(executable.signed_by(), vec!["alice"]);
912        assert_eq!(executable.unsigned_parties(), vec!["bob"]);
913
914        let executable = executable
915            .and_sign_as("bob", &signer("1220bb"))
916            .await
917            .expect("bob signs");
918        assert!(executable.unsigned_parties().is_empty());
919        assert_eq!(executable.signed_by(), vec!["alice", "bob"]);
920    }
921
922    /// A party with a signing threshold above one provides several signatures,
923    /// and the wire carries them inside that party's single entry. Pushing a
924    /// second entry for the same party sent a shape the API does not describe —
925    /// and left the threshold case unreachable.
926    #[tokio::test]
927    async fn several_signatures_for_one_party_share_its_entry() {
928        let executable = prepared(&["alice"])
929            .sign_as("alice", &signer("1220aa"))
930            .await
931            .expect("first key")
932            .and_sign_as("alice", &signer("1220bb"))
933            .await
934            .expect("second key");
935
936        assert_eq!(executable.signed_by(), vec!["alice"], "one entry");
937        let wire = executable.into_execute_request();
938        let signatures = wire.party_signatures.expect("signatures").signatures;
939        assert_eq!(signatures.len(), 1, "one entry per party");
940        assert_eq!(signatures[0].signatures.len(), 2, "both keys inside it");
941        assert_eq!(signatures[0].signatures[0].signed_by, "1220aa");
942        assert_eq!(signatures[0].signatures[1].signed_by, "1220bb");
943    }
944
945    /// Setting one bound clears the other, so "the later call wins" is true
946    /// rather than aspirational — the wire has a `oneof` and one of the two had
947    /// to be dropped.
948    #[test]
949    fn the_later_min_ledger_time_wins_in_either_order() {
950        use ipb::min_ledger_time::Time;
951        let abs_then_rel = Prepare::new("alice")
952            .with_min_ledger_time_abs(prost_types::Timestamp {
953                seconds: 42,
954                nanos: 0,
955            })
956            .with_min_ledger_time_rel(std::time::Duration::from_secs(30))
957            .into_request()
958            .2
959            .min_ledger_time
960            .and_then(|t| t.time);
961        assert!(
962            matches!(abs_then_rel, Some(Time::MinLedgerTimeRel(_))),
963            "rel was set last: {abs_then_rel:?}"
964        );
965    }
966
967    /// The change ID is `(user_id, act_as, command_id)`. If the command id did
968    /// not survive preparation, executing twice would commit twice instead of
969    /// being de-duplicated — the failure mode the id exists to prevent.
970    #[tokio::test]
971    async fn the_change_id_survives_from_preparation_into_execution() {
972        let (command_id, user_id, request) = Prepare::new("alice")
973            .with_command_id("stable-id")
974            .with_user_id("user-1")
975            .into_request();
976        assert_eq!(command_id, "stable-id");
977        assert_eq!(request.command_id, "stable-id");
978        assert_eq!(user_id.as_deref(), Some("user-1"));
979
980        let executable = prepared(&["alice"])
981            .sign_as("alice", &signer("1220aa"))
982            .await
983            .expect("sign");
984        let wire = executable.into_execute_request();
985        assert_eq!(
986            wire.user_id, "user-1",
987            "the user id is part of the change ID"
988        );
989
990        // And the rest of the change ID travels inside the prepared
991        // transaction, which is where the participant reads it from — not in a
992        // field of the execute request. Asserting on the user id alone let this
993        // test pass with `Prepared::command_id` deleted entirely.
994        let submitter = wire
995            .prepared_transaction
996            .expect("the transaction is required")
997            .metadata
998            .expect("metadata")
999            .submitter_info
1000            .expect("submitter info");
1001        assert_eq!(submitter.command_id, "command-1");
1002        assert_eq!(submitter.act_as, ["alice"]);
1003    }
1004
1005    /// Left unset it is still stable across a retry, because it is generated
1006    /// once at build time rather than per attempt.
1007    #[test]
1008    fn an_unset_command_id_is_generated_once() {
1009        let (id, _, request) = Prepare::new("alice").into_request();
1010        assert!(!id.is_empty());
1011        assert_eq!(id, request.command_id);
1012    }
1013
1014    /// `MinLedgerTime` is a `oneof`: the participant rejects both at once, so
1015    /// the builder cannot produce both.
1016    #[test]
1017    fn the_two_min_ledger_times_are_mutually_exclusive() {
1018        use ipb::min_ledger_time::Time;
1019
1020        let (_, _, request) = Prepare::new("alice")
1021            .with_min_ledger_time_rel(std::time::Duration::from_secs(30))
1022            .into_request();
1023        assert!(matches!(
1024            request.min_ledger_time.and_then(|t| t.time),
1025            Some(Time::MinLedgerTimeRel(_))
1026        ));
1027
1028        let (_, _, request) = Prepare::new("alice")
1029            .with_min_ledger_time_rel(std::time::Duration::from_secs(30))
1030            .with_min_ledger_time_abs(prost_types::Timestamp {
1031                seconds: 42,
1032                nanos: 0,
1033            })
1034            .into_request();
1035        let time = request.min_ledger_time.and_then(|t| t.time);
1036        assert!(
1037            matches!(time, Some(Time::MinLedgerTimeAbs(_))),
1038            "one of the two, never both: {time:?}"
1039        );
1040    }
1041
1042    /// The estimate is on by default and only an opt-out is sent, so an
1043    /// untouched builder must not carry the hints at all.
1044    #[test]
1045    fn traffic_cost_estimation_is_only_sent_when_turned_off() {
1046        let (_, _, request) = Prepare::new("alice").into_request();
1047        assert!(request.estimate_traffic_cost.is_none());
1048
1049        let (_, _, request) = Prepare::new("alice")
1050            .without_traffic_cost_estimation()
1051            .into_request();
1052        assert_eq!(
1053            request.estimate_traffic_cost.map(|h| h.disabled),
1054            Some(true)
1055        );
1056    }
1057
1058    /// A signature produced somewhere this crate cannot reach — an offline
1059    /// ceremony — reaches the wire under the party it was made for.
1060    #[tokio::test]
1061    async fn a_signature_made_elsewhere_reaches_the_wire() {
1062        let external = canton_signer::Signature::new(
1063            canton_signer::SignatureFormat::Concat,
1064            vec![8; 64],
1065            "1220cc",
1066            canton_signer::SigningAlgorithm::Ed25519,
1067        )
1068        .expect("a well-formed signature");
1069        let executable = prepared(&["alice", "bob"])
1070            .sign_as("alice", &signer("1220aa"))
1071            .await
1072            .expect("alice signs")
1073            .with_signature("bob", &external)
1074            .expect("bob's signature arrives from elsewhere");
1075
1076        let wire = executable.into_execute_and_wait_request();
1077        let signatures = wire.party_signatures.expect("signatures").signatures;
1078        let bob = signatures
1079            .iter()
1080            .find(|s| s.party == "bob")
1081            .expect("bob's entry");
1082        assert_eq!(bob.signatures[0].signature, vec![8; 64]);
1083        assert_eq!(bob.signatures[0].signed_by, "1220cc");
1084    }
1085
1086    #[tokio::test]
1087    async fn a_signature_for_a_party_that_is_not_acting_is_refused() {
1088        let external = canton_signer::Signature::new(
1089            canton_signer::SignatureFormat::Concat,
1090            vec![8; 64],
1091            "1220cc",
1092            canton_signer::SigningAlgorithm::Ed25519,
1093        )
1094        .expect("a well-formed signature");
1095        let err = prepared(&["alice"])
1096            .sign_as("alice", &signer("1220aa"))
1097            .await
1098            .expect("sign")
1099            .with_signature("mallory", &external)
1100            .expect_err("not an acting party");
1101        assert!(err.to_string().contains("mallory"), "{err}");
1102    }
1103
1104    /// The scheme the participant hashed under has to come back with the
1105    /// signature: verifying under a different one fails.
1106    #[tokio::test]
1107    async fn the_hashing_scheme_is_echoed_back_to_the_participant() {
1108        let prepared = prepared(&["alice"]);
1109        assert_eq!(
1110            prepared.hashing_scheme_version(),
1111            ipb::HashingSchemeVersion::V2
1112        );
1113
1114        let wire = prepared
1115            .sign_as("alice", &signer("1220aa"))
1116            .await
1117            .expect("sign")
1118            .into_execute_request();
1119        assert_eq!(
1120            wire.hashing_scheme_version,
1121            ipb::HashingSchemeVersion::V2 as i32
1122        );
1123    }
1124
1125    /// The hash the signer sees is the participant's, unmodified.
1126    #[tokio::test]
1127    async fn the_signer_is_handed_the_participants_hash() {
1128        let prepared = prepared(&["alice"]);
1129        assert_eq!(prepared.hash(), &[1, 2, 3]);
1130
1131        let key = Ed25519Key::from_seed(&[4; 32]).expect("seed");
1132        let expected = key.sign_raw(&[1, 2, 3]);
1133        let wire = prepared
1134            .sign_as("alice", &signer("1220aa"))
1135            .await
1136            .expect("sign")
1137            .into_execute_request();
1138        let signatures = wire.party_signatures.expect("signatures").signatures;
1139        assert_eq!(signatures[0].signatures[0].signature, expected);
1140    }
1141
1142    /// De-duplication lives on execution, not preparation — the wire has it
1143    /// there, and a period set before signing would be silently lost.
1144    #[tokio::test]
1145    async fn the_deduplication_period_reaches_every_execute_variant() {
1146        use ipb::execute_submission_request::DeduplicationPeriod;
1147
1148        let executable = prepared(&["alice"])
1149            .sign_as("alice", &signer("1220aa"))
1150            .await
1151            .expect("sign")
1152            .with_deduplication_duration(std::time::Duration::from_secs(60));
1153
1154        let wire = executable.clone().into_execute_request();
1155        assert!(matches!(
1156            wire.deduplication_period,
1157            Some(DeduplicationPeriod::DeduplicationDuration(d)) if d.seconds == 60
1158        ));
1159
1160        let wire = executable
1161            .with_deduplication_offset(99)
1162            .into_execute_and_wait_request();
1163        assert!(matches!(
1164            wire.deduplication_period,
1165            Some(
1166                ipb::execute_submission_and_wait_request::DeduplicationPeriod::DeduplicationOffset(
1167                    99
1168                )
1169            )
1170        ));
1171    }
1172
1173    /// What a caller can read off each state, and that each reader returns the
1174    /// value that was put in — not a default the type happens to have.
1175    #[test]
1176    fn prepare_reports_what_it_was_given() {
1177        let disclosed = pb::DisclosedContract {
1178            contract_id: "00d".to_string(),
1179            ..Default::default()
1180        };
1181        let prepare = Prepare::new_multi(vec!["alice".to_string(), "bob".to_string()])
1182            .with_read_as(vec!["carol".to_string()])
1183            .with_disclosed_contracts(vec![disclosed]);
1184        assert_eq!(prepare.act_as(), ["alice".to_string(), "bob".to_string()]);
1185        assert_eq!(prepare.read_as(), ["carol".to_string()]);
1186        assert_eq!(prepare.disclosed_contracts().len(), 1);
1187        assert_eq!(prepare.disclosed_contracts()[0].contract_id, "00d");
1188    }
1189
1190    #[tokio::test]
1191    async fn prepared_and_executable_report_what_the_participant_returned() {
1192        let prepared = Prepared::from_response(
1193            ipb::PrepareSubmissionResponse {
1194                prepared_transaction: Some(ipb::PreparedTransaction {
1195                    transaction: None,
1196                    metadata: Some(ipb::Metadata {
1197                        submitter_info: Some(ipb::metadata::SubmitterInfo {
1198                            act_as: vec!["alice".to_string()],
1199                            command_id: "command-7".to_string(),
1200                        }),
1201                        ..Default::default()
1202                    }),
1203                }),
1204                prepared_transaction_hash: vec![9, 9, 9],
1205                hashing_scheme_version: ipb::HashingSchemeVersion::V2 as i32,
1206                hashing_details: Some("hashed thus".to_string()),
1207                cost_estimation: Some(ipb::CostEstimation {
1208                    confirmation_request_traffic_cost_estimation: 1234,
1209                    ..Default::default()
1210                }),
1211            },
1212            vec!["alice".to_string()],
1213            vec!["carol".to_string()],
1214            "command-7".to_string(),
1215            Some("user-1".to_string()),
1216        )
1217        .expect("a full response");
1218
1219        assert_eq!(prepared.command_id(), "command-7");
1220        assert_eq!(prepared.act_as(), ["alice".to_string()]);
1221        assert_eq!(prepared.read_as(), ["carol".to_string()]);
1222        assert_eq!(prepared.hashing_details(), Some("hashed thus"));
1223        assert_eq!(
1224            prepared
1225                .cost_estimation()
1226                .map(|c| c.confirmation_request_traffic_cost_estimation),
1227            Some(1234)
1228        );
1229        let transaction = prepared.transaction().expect("the transaction is kept");
1230        let info = transaction
1231            .metadata
1232            .as_ref()
1233            .and_then(|m| m.submitter_info.as_ref())
1234            .expect("its submitter info");
1235        assert_eq!(info.command_id, "command-7");
1236        assert_eq!(
1237            prepared.change_id(),
1238            crate::command::ChangeId::new(
1239                "user-1".to_string(),
1240                vec!["alice".to_string()],
1241                "command-7".to_string()
1242            )
1243        );
1244
1245        let executable = prepared
1246            .sign_with(&signer("1220aa"))
1247            .await
1248            .expect("one acting party signs without naming itself");
1249        assert_eq!(executable.command_id(), "command-7");
1250        assert_eq!(
1251            executable.change_id(),
1252            crate::command::ChangeId::new(
1253                "user-1".to_string(),
1254                vec!["alice".to_string()],
1255                "command-7".to_string()
1256            )
1257        );
1258        assert_eq!(executable.signed_by(), ["alice"]);
1259        // Both acting and reading parties witness the returned transaction.
1260        let mut witnesses = executable.witnesses();
1261        witnesses.sort();
1262        assert_eq!(witnesses, ["alice".to_string(), "carol".to_string()]);
1263    }
1264
1265    /// The wait-for-transaction request carries everything the other two do,
1266    /// plus the format that decides which events come back — for every party
1267    /// that witnesses the transaction, in the shape the caller chose.
1268    #[tokio::test]
1269    async fn the_wait_for_transaction_request_carries_the_signature_and_the_format() {
1270        let executable = prepared(&["alice"])
1271            .sign_as("alice", &signer("1220aa"))
1272            .await
1273            .expect("sign")
1274            .with_submission_id("sub-1")
1275            .with_transaction_shape(crate::request::TransactionShape::LedgerEffects);
1276
1277        let wire = executable.into_execute_and_wait_for_transaction_request(pb::EventFormat {
1278            verbose: true,
1279            ..Default::default()
1280        });
1281
1282        assert!(
1283            wire.prepared_transaction.is_some(),
1284            "the transaction travels"
1285        );
1286        let signatures = wire.party_signatures.expect("signatures");
1287        assert_eq!(signatures.signatures.len(), 1);
1288        assert_eq!(signatures.signatures[0].party, "alice");
1289        assert_eq!(wire.submission_id, "sub-1");
1290        assert_eq!(wire.user_id, "user-1");
1291        assert_eq!(
1292            wire.hashing_scheme_version,
1293            ipb::HashingSchemeVersion::V2 as i32
1294        );
1295        let format = wire.transaction_format.expect("a transaction format");
1296        assert!(format.event_format.expect("event format").verbose);
1297        assert_eq!(
1298            format.transaction_shape,
1299            pb::TransactionShape::LedgerEffects as i32
1300        );
1301    }
1302}