Skip to main content

canton_ledger/
client.rs

1//! The async Ledger API client.
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use canton_core::auth::{self, Intercepted};
7use canton_core::telemetry::{self, TRANSPORT_GRPC};
8use canton_core::{Config, Error, Result};
9use canton_proto::com::daml::ledger::api::v2 as pb;
10use canton_proto::grpc::health::v1 as health_pb;
11use canton_proto::grpc::health::v1::health_check_response::ServingStatus;
12use futures_core::Stream;
13use tokio_stream::StreamExt as _;
14use tonic::transport::Channel;
15
16/// An `EventFormat` with a wildcard (all-templates) filter for each party.
17fn wildcard_event_format(parties: &[String]) -> pb::EventFormat {
18    let filters_by_party = parties
19        .iter()
20        .map(|party| {
21            (
22                party.clone(),
23                pb::Filters {
24                    cumulative: vec![pb::CumulativeFilter {
25                        identifier_filter: Some(
26                            pb::cumulative_filter::IdentifierFilter::WildcardFilter(
27                                pb::WildcardFilter {
28                                    include_created_event_blob: false,
29                                },
30                            ),
31                        ),
32                    }],
33                },
34            )
35        })
36        .collect();
37
38    pb::EventFormat {
39        filters_by_party,
40        filters_for_any_party: None,
41        verbose: true,
42    }
43}
44
45/// Give an execute request a submission id if it has none.
46///
47/// The change ID de-duplicates a submission; the submission id only tells the
48/// completions of separate *attempts* apart, and the Ledger API asks for a
49/// different one on each retry. Generating it per attempt rather than when the
50/// request is built is what makes attempts distinguishable while leaving the
51/// change ID — and therefore the de-duplication — untouched.
52trait HasSubmissionId {
53    fn submission_id_mut(&mut self) -> &mut String;
54}
55
56macro_rules! has_submission_id {
57    ($($t:ty),+) => {$(
58        impl HasSubmissionId for $t {
59            fn submission_id_mut(&mut self) -> &mut String {
60                &mut self.submission_id
61            }
62        }
63    )+};
64}
65
66has_submission_id!(
67    pb::interactive::ExecuteSubmissionRequest,
68    pb::interactive::ExecuteSubmissionAndWaitRequest,
69    pb::interactive::ExecuteSubmissionAndWaitForTransactionRequest
70);
71
72fn fresh_submission_id<T: HasSubmissionId>(mut request: T) -> T {
73    let id = request.submission_id_mut();
74    if id.is_empty() {
75        *id = uuid::Uuid::new_v4().to_string();
76    }
77    request
78}
79
80/// Whether a failed submission is the participant refusing a command it already
81/// has — and, on a retry, one this client is responsible for having sent.
82///
83/// `ALREADY_EXISTS` is what Canton's `DUPLICATE_COMMAND` arrives as. On a first
84/// attempt it means the *caller* reused a change ID from an earlier submission,
85/// which is a genuine rejection they must see. On a retry it means our own
86/// previous attempt was accepted, which is the opposite of a failure — so the
87/// caller of this predicate checks that first.
88fn is_duplicate_of_our_own(status: &tonic::Status) -> bool {
89    status.code() == tonic::Code::AlreadyExists
90}
91
92/// Refuse a command set the participant is certain to refuse.
93///
94/// Every one of these costs a round trip and comes back as a server-side error
95/// that reads like the ledger's fault. They are the caller's, and they are
96/// knowable here.
97fn validate_commands(commands: &pb::Commands) -> Result<()> {
98    if commands.act_as.is_empty() {
99        return Err(Error::InvalidRequest(
100            "a submission needs at least one acting party".to_string(),
101        ));
102    }
103    if commands.commands.is_empty() {
104        return Err(Error::InvalidRequest(
105            "a submission needs at least one command".to_string(),
106        ));
107    }
108    if commands.min_ledger_time_abs.is_some() && commands.min_ledger_time_rel.is_some() {
109        return Err(Error::InvalidRequest(
110            "min_ledger_time_abs and min_ledger_time_rel are mutually exclusive: set one"
111                .to_string(),
112        ));
113    }
114    Ok(())
115}
116
117/// One entry of an Active Contract Set snapshot.
118///
119/// A snapshot is not only active contracts. A reassignment that is half-done at
120/// the snapshot offset appears as an *incomplete* entry — unassigned from one
121/// synchronizer with no matching assignment yet, or assigned to one with the
122/// unassignment out of view — and an application that reads only
123/// [`Active`](Self::Active) sees a contract that has left one synchronizer and
124/// not arrived at the other simply vanish. The Ledger API sends these precisely
125/// so a multi-synchronizer application can bootstrap without that hole, which is
126/// why there is a lossless read.
127///
128/// `#[non_exhaustive]`: the Ledger API may add an entry kind.
129#[derive(Clone, Debug, PartialEq)]
130#[non_exhaustive]
131pub enum AcsEntry {
132    /// A contract active on the synchronizer named in the entry.
133    Active(pb::ActiveContract),
134    /// Unassigned before the snapshot offset with no assignment before it —
135    /// the contract is in flight *out of* this synchronizer.
136    IncompleteUnassigned(pb::IncompleteUnassigned),
137    /// Assigned before the snapshot offset with no unassignment before it.
138    /// Note this does **not** say the contract is active on the target
139    /// synchronizer; the proto is explicit about that.
140    IncompleteAssigned(pb::IncompleteAssigned),
141}
142
143impl AcsEntry {
144    /// The active contract, if this entry is one.
145    #[must_use]
146    pub fn active(&self) -> Option<&pb::ActiveContract> {
147        match self {
148            AcsEntry::Active(active) => Some(active),
149            _ => None,
150        }
151    }
152
153    /// Consume the entry, yielding the active contract if it is one.
154    #[must_use]
155    pub fn into_active(self) -> Option<pb::ActiveContract> {
156        match self {
157            AcsEntry::Active(active) => Some(active),
158            _ => None,
159        }
160    }
161}
162
163/// Convert one wire entry, dropping only the case the participant left empty
164/// (the `oneof` is required, so an empty entry is a participant that sent
165/// nothing to report).
166fn acs_entry(entry: Option<pb::get_active_contracts_response::ContractEntry>) -> Option<AcsEntry> {
167    use pb::get_active_contracts_response::ContractEntry;
168    match entry {
169        Some(ContractEntry::ActiveContract(active)) => Some(AcsEntry::Active(active)),
170        Some(ContractEntry::IncompleteUnassigned(unassigned)) => {
171            Some(AcsEntry::IncompleteUnassigned(unassigned))
172        }
173        Some(ContractEntry::IncompleteAssigned(assigned)) => {
174            Some(AcsEntry::IncompleteAssigned(assigned))
175        }
176        None => None,
177    }
178}
179
180/// The offset of an update, for resumable-stream position tracking.
181fn update_offset(update: &pb::get_updates_response::Update) -> i64 {
182    use pb::get_updates_response::Update;
183    match update {
184        Update::Transaction(t) => t.offset,
185        Update::Reassignment(r) => r.offset,
186        Update::TopologyTransaction(t) => t.offset,
187        Update::OffsetCheckpoint(c) => c.offset,
188    }
189}
190
191/// Build a gRPC service client on the authenticated channel, with this
192/// client's decode limit applied.
193///
194/// A macro rather than a function because `max_decoding_message_size` is an
195/// inherent method on each generated client — tonic exposes no trait for it —
196/// so there is nothing to be generic over. Keeping every construction site
197/// behind one expansion is the point: `tonic`'s 4 MiB default is small enough
198/// that a real ACS page trips it, and a new RPC added later would otherwise
199/// pick the default up silently.
200macro_rules! service {
201    ($self:ident, $ctor:expr) => {
202        $ctor($self.intercepted().await?)
203            .max_decoding_message_size($self.config.max_decoding_message_size())
204    };
205}
206
207/// An async client for the Canton Ledger API over gRPC.
208///
209/// The client owns a lazily-connected [`Channel`]; cloning it is cheap and
210/// clones share the underlying connection pool, so it is safe to hand a clone
211/// to each task.
212#[derive(Clone, Debug)]
213pub struct CantonClient {
214    channel: Channel,
215    config: Arc<Config>,
216}
217
218impl CantonClient {
219    /// Build a lazily-connected client. Returns immediately; the TCP/TLS
220    /// handshake happens on the first RPC.
221    ///
222    /// # Errors
223    /// Returns [`Error::InvalidRequest`] if the endpoint is not a valid URI.
224    pub fn connect_lazy(config: Config) -> Result<Self> {
225        Ok(Self {
226            channel: config.connect_channel()?,
227            config: Arc::new(config),
228        })
229    }
230
231    /// A channel wrapped with a fresh bearer-token interceptor for this call.
232    async fn intercepted(&self) -> Result<Intercepted> {
233        auth::intercepted(&self.channel, self.config.auth()).await
234    }
235
236    /// Run a unary operation under the configured retry policy (no-op when
237    /// retrying is not enabled).
238    async fn with_retry<T, F, Fut>(&self, op: F) -> Result<T>
239    where
240        F: FnMut() -> Fut,
241        Fut: std::future::Future<Output = Result<T>>,
242    {
243        canton_core::retry::run_with_retry(self.config.retry(), op).await
244    }
245
246    /// Return the participant's Ledger API version string (e.g. `"3.5.7"`).
247    ///
248    /// # Errors
249    /// Returns an [`Error`] if the RPC fails.
250    pub async fn version(&self) -> Result<String> {
251        telemetry::instrument("version", TRANSPORT_GRPC, async {
252            self.with_retry(|| async {
253                let mut client =
254                    service!(self, pb::version_service_client::VersionServiceClient::new);
255                let response = client
256                    .get_ledger_api_version(pb::GetLedgerApiVersionRequest {})
257                    .await?
258                    .into_inner();
259                Ok(response.version)
260            })
261            .await
262        })
263        .await
264    }
265
266    /// Probe the participant's overall serving status via the standard
267    /// `grpc.health.v1.Health` service (served on the Ledger API port).
268    ///
269    /// Poll this to react to intermittent or permanent node failure: a healthy
270    /// participant answers [`ServingStatus::Serving`]; an unreachable one
271    /// surfaces a transport [`Error`] (see [`Error::is_retriable`]).
272    ///
273    /// # Errors
274    /// Returns an [`Error`] if the health RPC fails (e.g. the node is down).
275    pub async fn health_check(&self) -> Result<ServingStatus> {
276        telemetry::instrument("health_check", TRANSPORT_GRPC, async {
277            self.with_retry(|| async {
278                let mut client = service!(self, health_pb::health_client::HealthClient::new);
279                // Empty `service` = the server's overall status (Canton does not
280                // register per-service health entries on the Ledger API port).
281                let response = client
282                    .check(health_pb::HealthCheckRequest {
283                        service: String::new(),
284                    })
285                    .await?
286                    .into_inner();
287                Ok(response.status())
288            })
289            .await
290        })
291        .await
292    }
293
294    /// Submit commands **fire-and-forget** (`CommandSubmissionService.Submit`):
295    /// hand the commands to the participant and return promptly without waiting
296    /// for the transaction. Returns the change-ID `command_id` used, so the
297    /// caller can recover the outcome later with [`Self::await_completion`] (or
298    /// the [`Self::completions`] stream).
299    ///
300    /// A fresh UUID `command_id` is generated when the caller did not set one,
301    /// so ledger-side de-duplication behaves correctly across retries.
302    ///
303    /// # Errors
304    /// Returns an [`Error`] if authentication fails or the participant rejects
305    /// the submission synchronously (e.g. a preprocessing error).
306    pub async fn submit(&self, submit: crate::command::Submit) -> Result<String> {
307        let submission = self.submission(submit);
308        let command_id = submission.change_id().command_id().to_string();
309        submission.submit().await?;
310        Ok(command_id)
311    }
312
313    /// Fix a submission's identity **before** sending it, returning a
314    /// [`Submission`](crate::Submission) that carries its
315    /// [`ChangeId`](crate::ChangeId).
316    ///
317    /// This is the handle to reach for when losing the outcome is not an
318    /// option. A submission whose response is lost — a dropped connection, a
319    /// timeout, a retry the participant de-duplicated — may well have
320    /// committed, and the only way back to the answer is the change ID. If the
321    /// SDK generated the command id inside the call that failed, there is no
322    /// change ID to go back with.
323    ///
324    /// ```no_run
325    /// # async fn run(client: canton_ledger::CantonClient, submit: canton_ledger::Submit)
326    /// #     -> canton_ledger::Result<()> {
327    /// use std::time::Duration;
328    ///
329    /// // Where to start reading completions from, taken before submitting.
330    /// let offset = client.ledger_end().await?;
331    /// let submission = client.submission(submit);
332    ///
333    /// if submission.submit_and_wait().await.is_err() {
334    ///     // Ambiguous: ask the ledger what actually happened.
335    ///     let completion = submission.recover(offset, Duration::from_secs(30)).await?;
336    ///     println!("committed after all: {}", completion.update_id);
337    /// }
338    /// # Ok(()) }
339    /// ```
340    #[must_use]
341    pub fn submission(&self, submit: crate::command::Submit) -> crate::submission::Submission {
342        let shape = submit.transaction_shape;
343        let (change_id, commands) = submit.into_commands();
344        crate::submission::Submission::new(self.clone(), change_id, commands, shape)
345    }
346
347    pub(crate) async fn submit_commands(&self, commands: pb::Commands) -> Result<()> {
348        validate_commands(&commands)?;
349        telemetry::instrument("submit", TRANSPORT_GRPC, async move {
350            let attempt = std::sync::atomic::AtomicU32::new(0);
351            let command_id = commands.command_id.clone();
352            self.with_retry(|| {
353                let commands = commands.clone();
354                let command_id = command_id.clone();
355                let retry = attempt.fetch_add(1, std::sync::atomic::Ordering::Relaxed) > 0;
356                async move {
357                    let mut client = service!(
358                        self,
359                        pb::command_submission_service_client::CommandSubmissionServiceClient::new
360                    );
361                    match client
362                        .submit(pb::SubmitRequest {
363                            commands: Some(commands),
364                        })
365                        .await
366                    {
367                        Ok(_) => Ok(()),
368                        Err(status) if retry && is_duplicate_of_our_own(&status) => {
369                            // The command we are retrying is already at the
370                            // participant, and we are the ones who put it
371                            // there: a previous attempt of this same retry loop
372                            // was accepted and its response was lost. Reporting
373                            // the duplicate rejection would tell the caller
374                            // their command failed when it did precisely the
375                            // opposite — the failure mode this whole change ID
376                            // exists to prevent.
377                            tracing::debug!(
378                                %command_id,
379                                "submission retry was de-duplicated; the earlier attempt is the one that landed"
380                            );
381                            Ok(())
382                        }
383                        Err(status) => Err(Error::from(status)),
384                    }
385                }
386            })
387            .await
388        })
389        .await
390    }
391
392    /// Submit commands and wait for the result **without** fetching the
393    /// transaction (`CommandService.SubmitAndWait`): blocks until the command
394    /// commits (or is rejected) and returns the `update_id` and completion
395    /// offset. Lighter than [`Self::submit_and_wait_for_transaction`] when the
396    /// caller does not need the event payload.
397    ///
398    /// # Errors
399    /// Returns an [`Error`] if authentication fails or the command is rejected.
400    /// The retry caveat on [`Self::submit_and_wait_for_transaction`] applies.
401    pub async fn submit_and_wait(
402        &self,
403        submit: crate::command::Submit,
404    ) -> Result<pb::SubmitAndWaitResponse> {
405        self.submission(submit).submit_and_wait().await
406    }
407
408    pub(crate) async fn submit_and_wait_commands(
409        &self,
410        commands: pb::Commands,
411    ) -> Result<pb::SubmitAndWaitResponse> {
412        validate_commands(&commands)?;
413        telemetry::instrument("submit_and_wait", TRANSPORT_GRPC, async move {
414            let request = pb::SubmitAndWaitRequest {
415                commands: Some(commands),
416            };
417            self.with_retry(|| {
418                let request = request.clone();
419                async move {
420                    let mut client =
421                        service!(self, pb::command_service_client::CommandServiceClient::new);
422                    Ok(client.submit_and_wait(request).await?.into_inner())
423                }
424            })
425            .await
426        })
427        .await
428    }
429
430    /// Submit commands and wait for the resulting transaction.
431    ///
432    /// Fills the change ID's `command_id` with a fresh UUID when the caller did
433    /// not set one, so ledger-side de-duplication behaves correctly. The
434    /// returned transaction is shaped as `LEDGER_EFFECTS` and filtered to the
435    /// acting party (wildcard), so created events are visible in the response.
436    ///
437    /// # Errors
438    /// Returns an [`Error`] if authentication fails, the command is rejected, or
439    /// the response contains no transaction.
440    ///
441    /// # Example
442    /// ```no_run
443    /// # async fn run(client: canton_ledger::CantonClient, party: &str, pkg: &str)
444    /// #     -> canton_ledger::Result<()> {
445    /// use canton_ledger::{Submit, create, identifier, record};
446    ///
447    /// let tx = client
448    ///     .submit_and_wait_for_transaction(
449    ///         Submit::new(party)
450    ///             .add_command(create(identifier(pkg, "M", "T"), record(vec![]))),
451    ///     )
452    ///     .await?;
453    /// println!("committed {} at offset {}", tx.update_id, tx.offset);
454    /// # Ok(()) }
455    /// ```
456    ///
457    /// # Retry caveat (exactly-once)
458    /// With retry enabled ([`Config::with_retry`]), a submission that commits
459    /// on the ledger but whose response is lost to a retriable error is re-sent
460    /// with the same `command_id` and de-duplicated by the participant.
461    ///
462    /// [`Self::submit`] can answer that on its own: a duplicate rejection of a
463    /// retry it made itself means its earlier attempt was accepted, which is
464    /// success, so it reports success. **This method cannot.** Its result is
465    /// the committed transaction, and a de-duplicated retry does not carry one
466    /// — so the duplicate rejection reaches the caller, and the transaction has
467    /// to be read back rather than invented.
468    ///
469    /// Take a [`Submission`](crate::Submission) from [`Self::submission`]
470    /// before submitting, and on any error recover the outcome with
471    /// [`Submission::recover`](crate::Submission::recover) — it knows the
472    /// change ID, which is the only way back to a command whose response was
473    /// lost. See the `recover_a_submission` example.
474    pub async fn submit_and_wait_for_transaction(
475        &self,
476        submit: crate::command::Submit,
477    ) -> Result<pb::Transaction> {
478        self.submission(submit)
479            .submit_and_wait_for_transaction()
480            .await
481    }
482
483    pub(crate) async fn submit_and_wait_for_transaction_commands(
484        &self,
485        commands: pb::Commands,
486        shape: crate::request::TransactionShape,
487    ) -> Result<pb::Transaction> {
488        validate_commands(&commands)?;
489        // The filter covers `read_as` as well as `act_as`: the Ledger API's own
490        // default for a submission is both, and a command submitted with
491        // `read_as` set is one whose result the caller expects to see through
492        // those parties too. Filtering to `act_as` alone silently returned a
493        // transaction with events missing.
494        let mut parties = commands.act_as.clone();
495        parties.extend(commands.read_as.iter().cloned());
496        parties.sort_unstable();
497        parties.dedup();
498        let request = pb::SubmitAndWaitForTransactionRequest {
499            transaction_format: Some(pb::TransactionFormat {
500                event_format: Some(wildcard_event_format(&parties)),
501                transaction_shape: shape.as_grpc() as i32,
502            }),
503            commands: Some(commands),
504        };
505        telemetry::instrument(
506            "submit_and_wait_for_transaction",
507            TRANSPORT_GRPC,
508            async move {
509                let response = self
510                    .with_retry(|| {
511                        let request = request.clone();
512                        async move {
513                            let mut client = service!(
514                                self,
515                                pb::command_service_client::CommandServiceClient::new
516                            );
517                            Ok(client
518                                .submit_and_wait_for_transaction(request)
519                                .await?
520                                .into_inner())
521                        }
522                    })
523                    .await?;
524
525                response.transaction.ok_or_else(|| {
526                    Error::UnexpectedResponse("response contained no transaction".to_string())
527                })
528            },
529        )
530        .await
531    }
532
533    /// The synchronizers this participant is connected to for `party`.
534    ///
535    /// Allocating an external party and preparing a submission both name a
536    /// synchronizer, and a caller has no other way to discover one — so this
537    /// is what turns "which synchronizer?" from configuration into a question
538    /// the participant answers.
539    ///
540    /// # Errors
541    /// Returns an [`Error`] if the RPC fails.
542    pub async fn connected_synchronizers(
543        &self,
544        party: &str,
545    ) -> Result<Vec<pb::get_connected_synchronizers_response::ConnectedSynchronizer>> {
546        let request = pb::GetConnectedSynchronizersRequest {
547            party: party.to_string(),
548            ..Default::default()
549        };
550        telemetry::instrument("connected_synchronizers", TRANSPORT_GRPC, async move {
551            let response = self
552                .with_retry(|| {
553                    let request = request.clone();
554                    async move {
555                        let mut client =
556                            service!(self, pb::state_service_client::StateServiceClient::new);
557                        Ok(client
558                            .get_connected_synchronizers(request)
559                            .await?
560                            .into_inner())
561                    }
562                })
563                .await?;
564            Ok(response.connected_synchronizers)
565        })
566        .await
567    }
568
569    /// Interpret a submission without authorizing it, returning the transaction
570    /// and the hash to sign.
571    ///
572    /// The first half of interactive submission: the participant works out what
573    /// the commands do, and hands back a hash for the party's key — which it
574    /// does not hold — to sign. See [`crate::interactive`] for the flow, and
575    /// for how long a prepared transaction stays executable.
576    ///
577    /// Retried like any other read: preparation commits nothing.
578    ///
579    /// # Errors
580    /// As any gRPC call, plus [`Error::UnexpectedResponse`] if the participant
581    /// returns no hash — which would leave nothing to sign.
582    pub async fn prepare_submission(
583        &self,
584        prepare: crate::interactive::Prepare,
585    ) -> Result<crate::interactive::Prepared> {
586        // Built once, outside the instrumented future, so retries reuse one
587        // command id — the change ID stays stable and the submission remains
588        // de-duplication-safe across attempts, exactly as `submit` does.
589        let act_as = prepare.act_as().to_vec();
590        // Carried through: the Ledger API's default event format for
591        // `…_and_wait_for_transaction` covers `act_as` *and* `read_as`, so
592        // dropping it here would silently return a narrower transaction.
593        let read_as = prepare.read_as().to_vec();
594        let requested_scheme = prepare.requested_hashing_scheme();
595        let (command_id, user_id, request) = prepare.into_request();
596        telemetry::instrument("prepare_submission", TRANSPORT_GRPC, async move {
597            let response = self
598                .with_retry(|| {
599                    let request = request.clone();
600                    async move {
601                        let mut client = service!(
602                            self,
603                            pb::interactive::interactive_submission_service_client::InteractiveSubmissionServiceClient::new
604                        );
605                        Ok(client.prepare_submission(request).await?.into_inner())
606                    }
607                })
608                .await?;
609            crate::interactive::Prepared::from_response(
610                response,
611                requested_scheme,
612                act_as,
613                read_as,
614                command_id,
615                user_id,
616            )
617        })
618        .await
619    }
620
621    /// Submit a signed prepared transaction and return once the participant has
622    /// accepted it for processing.
623    ///
624    /// Acceptance is not commitment: the outcome arrives on the completion
625    /// stream. Use [`execute_submission_and_wait`](Self::execute_submission_and_wait)
626    /// to wait for it.
627    ///
628    /// # Retry caveat
629    /// With retry enabled, an attempt whose response was lost is retried under
630    /// a fresh submission id and the same change id, and the participant
631    /// answers `ALREADY_EXISTS`: that is the earlier attempt having landed,
632    /// and it is reported as success here, as on
633    /// [`submit`](Self::submit). An `Ok` still means only
634    /// *accepted*; whether it committed is a completion, reachable by
635    /// [`await_completion`](Self::await_completion) with the
636    /// [`Executable::change_id`](crate::interactive::Executable::change_id)
637    /// taken before the call.
638    ///
639    /// # Errors
640    /// As any gRPC call. A missing or wrong signature is a rejection from the
641    /// participant, not a local error.
642    pub async fn execute_submission(
643        &self,
644        executable: crate::interactive::Executable,
645    ) -> Result<()> {
646        let request = executable.into_execute_request();
647        telemetry::instrument("execute_submission", TRANSPORT_GRPC, async move {
648            let attempt = std::sync::atomic::AtomicU32::new(0);
649            self.with_retry(|| {
650                let request = fresh_submission_id(request.clone());
651                let retry = attempt.fetch_add(1, std::sync::atomic::Ordering::Relaxed) > 0;
652                async move {
653                    let mut client = service!(
654                        self,
655                        pb::interactive::interactive_submission_service_client::InteractiveSubmissionServiceClient::new
656                    );
657                    match client.execute_submission(request).await {
658                        Ok(_) => Ok(()),
659                        Err(status) if retry && is_duplicate_of_our_own(&status) => {
660                            // Same reasoning as the ordinary submit: a
661                            // previous attempt of this loop was accepted and
662                            // its answer lost, and reporting the duplicate as
663                            // a failure would say the opposite of what
664                            // happened.
665                            tracing::debug!(
666                                "execute retry was de-duplicated; the earlier attempt is the one that landed"
667                            );
668                            Ok(())
669                        }
670                        Err(status) => Err(Error::from(status)),
671                    }
672                }
673            })
674            .await
675        })
676        .await
677    }
678
679    /// Submit a signed prepared transaction and wait for it to commit.
680    ///
681    /// The response carries the update id **and** the completion offset — the
682    /// offset a follow-up completion or update read starts from, which is why
683    /// it is returned whole rather than as the id alone.
684    ///
685    /// # Retry caveat
686    /// As [`execute_submission`](Self::execute_submission): a retried attempt
687    /// that the participant de-duplicates is reported as the rejection it
688    /// answers with, so on an ambiguous outcome recover through
689    /// [`await_completion`](Self::await_completion) with the change id taken
690    /// before the call.
691    ///
692    /// # Errors
693    /// As any gRPC call, plus a rejection if the transaction fails.
694    pub async fn execute_submission_and_wait(
695        &self,
696        executable: crate::interactive::Executable,
697    ) -> Result<pb::interactive::ExecuteSubmissionAndWaitResponse> {
698        let request = executable.into_execute_and_wait_request();
699        telemetry::instrument("execute_submission_and_wait", TRANSPORT_GRPC, async move {
700            let response = self
701                .with_retry(|| {
702                    let request = fresh_submission_id(request.clone());
703                    async move {
704                        let mut client = service!(
705                            self,
706                            pb::interactive::interactive_submission_service_client::InteractiveSubmissionServiceClient::new
707                        );
708                        Ok(client
709                            .execute_submission_and_wait(request)
710                            .await?
711                            .into_inner())
712                    }
713                })
714                .await?;
715            Ok(response)
716        })
717        .await
718    }
719
720    /// Submit a signed prepared transaction and wait for the committed
721    /// transaction itself.
722    ///
723    /// The events it carries follow
724    /// [`Executable::with_transaction_shape`](crate::interactive::Executable::with_transaction_shape).
725    ///
726    /// # Errors
727    /// As any gRPC call, plus [`Error::UnexpectedResponse`] if the participant
728    /// reports success without a transaction.
729    pub async fn execute_submission_and_wait_for_transaction(
730        &self,
731        executable: crate::interactive::Executable,
732    ) -> Result<pb::Transaction> {
733        let event_format = wildcard_event_format(&executable.witnesses());
734        let request = executable.into_execute_and_wait_for_transaction_request(event_format);
735        telemetry::instrument(
736            "execute_submission_and_wait_for_transaction",
737            TRANSPORT_GRPC,
738            async move {
739                let response = self
740                    .with_retry(|| {
741                        let request = fresh_submission_id(request.clone());
742                        async move {
743                            let mut client = service!(
744                                self,
745                                pb::interactive::interactive_submission_service_client::InteractiveSubmissionServiceClient::new
746                            );
747                            Ok(client
748                                .execute_submission_and_wait_for_transaction(request)
749                                .await?
750                                .into_inner())
751                        }
752                    })
753                    .await?;
754
755                response.transaction.ok_or_else(|| {
756                    Error::UnexpectedResponse("response contained no transaction".to_string())
757                })
758            },
759        )
760        .await
761    }
762
763    /// Subscribe to the command-completion stream for `parties`, starting after
764    /// `begin_offset` (exclusive). Offset checkpoints are filtered out, so the
765    /// stream yields only [`pb::Completion`]s.
766    ///
767    /// # Errors
768    /// Returns an [`Error`] if authentication or opening the stream fails.
769    pub async fn completions(
770        &self,
771        parties: Vec<String>,
772        begin_offset: i64,
773    ) -> Result<impl Stream<Item = Result<pb::Completion>> + Send + use<>> {
774        self.completions_with(crate::request::CompletionsRequest::new(
775            parties,
776            begin_offset,
777        ))
778        .await
779    }
780
781    /// Like [`Self::completions`], with the full request surface: a
782    /// [`CompletionsRequest`](crate::request::CompletionsRequest) additionally
783    /// selects the `user_id` whose command completions to stream (pair it with
784    /// [`Submit::with_user_id`](crate::Submit::with_user_id)).
785    ///
786    /// # Errors
787    /// Returns an [`Error`] if authentication or opening the stream fails.
788    pub async fn completions_with(
789        &self,
790        request: crate::request::CompletionsRequest,
791    ) -> Result<impl Stream<Item = Result<pb::Completion>> + Send + use<>> {
792        request.validate()?;
793        telemetry::instrument("completions", TRANSPORT_GRPC, async move {
794            let mut client = service!(
795                self,
796                pb::command_completion_service_client::CommandCompletionServiceClient::new
797            );
798            let stream = client
799                .completion_stream(request.into_grpc())
800                .await?
801                .into_inner();
802
803            let stream = stream.filter_map(|item| match item {
804                Ok(response) => match response.completion_response {
805                    Some(pb::completion_stream_response::CompletionResponse::Completion(
806                        completion,
807                    )) => Some(Ok(completion)),
808                    _ => None, // skip offset checkpoints
809                },
810                Err(status) => Some(Err(Error::from(status))),
811            });
812            Ok(telemetry::instrument_stream(
813                "completions",
814                TRANSPORT_GRPC,
815                stream,
816            ))
817        })
818        .await
819    }
820
821    /// Recover the completion for a specific command by scanning the completion
822    /// stream from `begin_offset`, up to `timeout`.
823    ///
824    /// This is the command-recovery path: after a crash, lost connection, or
825    /// timeout, the outcome of a pending command is read back from the
826    /// completion endpoint instead of blindly re-submitting. If the command's
827    /// completion reports a non-OK status, this returns [`Error::CommandRejected`].
828    ///
829    /// Matching is on the whole [`ChangeId`](crate::ChangeId) — user, acting
830    /// parties and command id — because that is what identifies a command to
831    /// Canton. A command id on its own is not unique across the users of a
832    /// participant, and answering with somebody else's completion is worse
833    /// than answering with none.
834    ///
835    /// The completion stream is a live subscription that does not self-terminate,
836    /// so `timeout` bounds how long to wait for the target completion.
837    ///
838    /// # Errors
839    /// Returns [`Error::Timeout`] if the completion is not seen within `timeout`,
840    /// [`Error::CommandRejected`] if the ledger rejected the command, or another
841    /// [`Error`] if the stream fails.
842    pub async fn await_completion(
843        &self,
844        change_id: &crate::command::ChangeId,
845        begin_offset: i64,
846        timeout: Duration,
847    ) -> Result<pb::Completion> {
848        let scan = async {
849            let stream = self
850                .completions(change_id.act_as().to_vec(), begin_offset)
851                .await?;
852            tokio::pin!(stream);
853            while let Some(item) = stream.next().await {
854                let completion = item?;
855                if change_id.matches(&completion) {
856                    // A non-OK gRPC status on the completion means the ledger
857                    // rejected the command for business/interpretation reasons.
858                    if let Some(status) = &completion.status {
859                        // google.rpc.Status code 0 == OK; anything else is a rejection.
860                        if status.code != 0 {
861                            return Err(Error::CommandRejected {
862                                code: format!("{:?}", tonic::Code::from(status.code)),
863                                message: status.message.clone(),
864                            });
865                        }
866                    }
867                    return Ok(completion);
868                }
869            }
870            Err(Error::UnexpectedResponse(format!(
871                "completion stream ended before command {} was seen",
872                change_id.command_id()
873            )))
874        };
875
876        tokio::time::timeout(timeout, scan)
877            .await
878            .map_err(|_| Error::Timeout)?
879    }
880
881    /// Return the current ledger end offset as seen by the participant.
882    ///
883    /// A value of `0` means the participant's view of the ledger is empty.
884    /// This is an authenticated endpoint.
885    ///
886    /// # Errors
887    /// Returns an [`Error`] if authentication or the RPC fails.
888    pub async fn ledger_end(&self) -> Result<i64> {
889        telemetry::instrument("ledger_end", TRANSPORT_GRPC, async {
890            self.with_retry(|| async {
891                let mut client = service!(self, pb::state_service_client::StateServiceClient::new);
892                let response = client
893                    .get_ledger_end(pb::GetLedgerEndRequest {})
894                    .await?
895                    .into_inner();
896                Ok(response.offset)
897            })
898            .await
899        })
900        .await
901    }
902
903    /// Fetch the created and/or archived events for a contract by id
904    /// (`EventQueryService.GetEventsByContractId`), with verbose records and
905    /// no created-event blob. To obtain a contract's `created_event_blob`
906    /// (for disclosure), use a template-filtered
907    /// [`Self::active_contracts_with`] read with
908    /// [`ActiveContractsRequest::with_created_event_blobs`](crate::request::ActiveContractsRequest::with_created_event_blobs).
909    ///
910    /// # Errors
911    /// Returns an [`Error`] if authentication or the RPC fails.
912    pub async fn events_by_contract_id(
913        &self,
914        contract_id: impl Into<String>,
915        parties: Vec<String>,
916    ) -> Result<pb::GetEventsByContractIdResponse> {
917        let contract_id = contract_id.into();
918        telemetry::instrument("events_by_contract_id", TRANSPORT_GRPC, async move {
919            // Retried like the other reads: this is a lookup, so a transient
920            // failure is worth another attempt rather than a caller's error
921            // path. Only submissions have a reason to be careful here.
922            self.with_retry(|| {
923                let request = pb::GetEventsByContractIdRequest {
924                    contract_id: contract_id.clone(),
925                    event_format: Some(wildcard_event_format(&parties)),
926                };
927                async move {
928                    let mut client = service!(
929                        self,
930                        pb::event_query_service_client::EventQueryServiceClient::new
931                    );
932                    Ok(client
933                        .get_events_by_contract_id(request)
934                        .await?
935                        .into_inner())
936                }
937            })
938            .await
939        })
940        .await
941    }
942
943    /// Fetch one **page** of the Active Contract Set for `parties` as of
944    /// `active_at_offset`. Returns the page's active contracts and the next page
945    /// token (`None` once the last page has been read); pass the token back in
946    /// to fetch the following page.
947    ///
948    /// # Errors
949    /// Returns an [`Error`] if authentication or the RPC fails.
950    pub async fn active_contracts_page(
951        &self,
952        parties: Vec<String>,
953        active_at_offset: i64,
954        max_page_size: i32,
955        page_token: Option<Vec<u8>>,
956    ) -> Result<(Vec<pb::ActiveContract>, Option<Vec<u8>>)> {
957        let request = crate::request::ActiveContractsRequest::new(parties, active_at_offset);
958        self.active_contracts_page_with(&request, max_page_size, page_token)
959            .await
960    }
961
962    /// Like [`Self::active_contracts_page`], with the full request surface of
963    /// an [`ActiveContractsRequest`](crate::request::ActiveContractsRequest)
964    /// (template/interface filters, created-event blobs, non-verbose records).
965    ///
966    /// # Errors
967    /// Returns an [`Error`] if authentication or the RPC fails.
968    pub async fn active_contracts_page_with(
969        &self,
970        request: &crate::request::ActiveContractsRequest,
971        max_page_size: i32,
972        page_token: Option<Vec<u8>>,
973    ) -> Result<(Vec<pb::ActiveContract>, Option<Vec<u8>>)> {
974        let (entries, next) = self
975            .acs_page_with(request, max_page_size, page_token)
976            .await?;
977        Ok((
978            entries
979                .into_iter()
980                .filter_map(AcsEntry::into_active)
981                .collect(),
982            next,
983        ))
984    }
985
986    /// One page of the Active Contract Set for `parties`, **losslessly**: every
987    /// entry the participant sent, active or incomplete (see [`AcsEntry`]).
988    ///
989    /// # Errors
990    /// Returns an [`Error`] if authentication or the RPC fails.
991    pub async fn acs_page(
992        &self,
993        parties: Vec<String>,
994        active_at_offset: i64,
995        max_page_size: i32,
996        page_token: Option<Vec<u8>>,
997    ) -> Result<(Vec<AcsEntry>, Option<Vec<u8>>)> {
998        let request = crate::request::ActiveContractsRequest::new(parties, active_at_offset);
999        self.acs_page_with(&request, max_page_size, page_token)
1000            .await
1001    }
1002
1003    /// Like [`Self::acs_page`], with the full request surface of an
1004    /// [`ActiveContractsRequest`](crate::request::ActiveContractsRequest).
1005    ///
1006    /// # Errors
1007    /// Returns an [`Error`] if authentication or the RPC fails.
1008    pub async fn acs_page_with(
1009        &self,
1010        request: &crate::request::ActiveContractsRequest,
1011        max_page_size: i32,
1012        page_token: Option<Vec<u8>>,
1013    ) -> Result<(Vec<AcsEntry>, Option<Vec<u8>>)> {
1014        request.validate()?;
1015        telemetry::instrument("active_contracts_page", TRANSPORT_GRPC, async move {
1016            self.with_retry(|| {
1017                let page_request = pb::GetActiveContractsPageRequest {
1018                    active_at_offset: Some(request.active_at_offset),
1019                    event_format: Some(request.event_format()),
1020                    max_page_size: Some(max_page_size),
1021                    page_token: page_token.clone(),
1022                };
1023                async move {
1024                    let mut client =
1025                        service!(self, pb::state_service_client::StateServiceClient::new);
1026                    let response = client
1027                        .get_active_contracts_page(page_request)
1028                        .await?
1029                        .into_inner();
1030
1031                    let entries = response
1032                        .active_contracts
1033                        .into_iter()
1034                        .filter_map(|entry| acs_entry(entry.contract_entry))
1035                        .collect();
1036                    Ok((entries, response.next_page_token))
1037                }
1038            })
1039            .await
1040        })
1041        .await
1042    }
1043
1044    /// Fetch one **page** of updates in the offset range
1045    /// `(begin_offset_exclusive, end_offset_inclusive]`, optionally in reverse
1046    /// (`descending`) order. Returns the page items and the next page token
1047    /// (`None` once the last page has been read).
1048    ///
1049    /// # Errors
1050    /// Returns an [`Error`] if authentication or the RPC fails.
1051    pub async fn updates_page(
1052        &self,
1053        parties: Vec<String>,
1054        begin_offset_exclusive: i64,
1055        end_offset_inclusive: i64,
1056        max_page_size: i32,
1057        descending: bool,
1058        page_token: Option<Vec<u8>>,
1059    ) -> Result<(Vec<pb::GetUpdateResponse>, Option<Vec<u8>>)> {
1060        let mut request = crate::request::UpdatesRequest::new(parties, begin_offset_exclusive)
1061            .until(end_offset_inclusive);
1062        if descending {
1063            request = request.descending();
1064        }
1065        self.updates_page_with(&request, max_page_size, page_token)
1066            .await
1067    }
1068
1069    /// Like [`Self::updates_page`], with the full request surface of an
1070    /// [`UpdatesRequest`](crate::request::UpdatesRequest) (template/interface
1071    /// filters, transaction shape, descending order, created-event blobs,
1072    /// topology events, non-verbose records). The request's bounds supply the
1073    /// page range, so
1074    /// [`UpdatesRequest::until`](crate::request::UpdatesRequest::until) is
1075    /// required here — the paged read is inherently bounded.
1076    ///
1077    /// # Errors
1078    /// Returns [`Error::InvalidRequest`] if the request has no end offset, or
1079    /// another [`Error`] if authentication or the RPC fails.
1080    pub async fn updates_page_with(
1081        &self,
1082        request: &crate::request::UpdatesRequest,
1083        max_page_size: i32,
1084        page_token: Option<Vec<u8>>,
1085    ) -> Result<(Vec<pb::GetUpdateResponse>, Option<Vec<u8>>)> {
1086        request.validate()?;
1087        let (begin_exclusive, end_inclusive) = request.bounds();
1088        let Some(end_inclusive) = end_inclusive else {
1089            return Err(Error::InvalidRequest(
1090                "updates_page_with requires a bounded request: set UpdatesRequest::until"
1091                    .to_string(),
1092            ));
1093        };
1094        telemetry::instrument("updates_page", TRANSPORT_GRPC, async move {
1095            self.with_retry(|| {
1096                let page_request = pb::GetUpdatesPageRequest {
1097                    begin_offset_exclusive: Some(begin_exclusive),
1098                    end_offset_inclusive: Some(end_inclusive),
1099                    max_page_size: Some(max_page_size),
1100                    update_format: Some(request.update_format()),
1101                    descending_order: request.is_descending(),
1102                    page_token: page_token.clone(),
1103                };
1104                async move {
1105                    let mut client =
1106                        service!(self, pb::update_service_client::UpdateServiceClient::new);
1107                    let response = client.get_updates_page(page_request).await?.into_inner();
1108                    Ok((response.updates, response.next_page_token))
1109                }
1110            })
1111            .await
1112        })
1113        .await
1114    }
1115
1116    /// Stream the Active Contract Set for `parties` as of `active_at_offset`
1117    /// (typically the current ledger end). Yields the active contracts,
1118    /// wildcard-filtered to the given parties.
1119    ///
1120    /// # Errors
1121    /// Returns an [`Error`] if authentication or opening the stream fails.
1122    pub async fn active_contracts(
1123        &self,
1124        parties: Vec<String>,
1125        active_at_offset: i64,
1126    ) -> Result<impl Stream<Item = Result<pb::ActiveContract>> + Send + use<>> {
1127        self.active_contracts_with(crate::request::ActiveContractsRequest::new(
1128            parties,
1129            active_at_offset,
1130        ))
1131        .await
1132    }
1133
1134    /// Like [`Self::active_contracts`], with the full request surface: an
1135    /// [`ActiveContractsRequest`](crate::request::ActiveContractsRequest)
1136    /// additionally filters by template or interface, includes created-event
1137    /// blobs, and drops record labels.
1138    ///
1139    /// # Errors
1140    /// Returns an [`Error`] if authentication or opening the stream fails.
1141    pub async fn active_contracts_with(
1142        &self,
1143        request: crate::request::ActiveContractsRequest,
1144    ) -> Result<impl Stream<Item = Result<pb::ActiveContract>> + Send + use<>> {
1145        let stream = self.acs_entries_with(request).await?;
1146        Ok(stream.filter_map(|item| match item {
1147            Ok(entry) => entry.into_active().map(Ok),
1148            Err(err) => Some(Err(err)),
1149        }))
1150    }
1151
1152    /// Stream the Active Contract Set for `parties` **losslessly**: every entry
1153    /// the participant sends, active or incomplete (see [`AcsEntry`]).
1154    ///
1155    /// # Errors
1156    /// Returns an [`Error`] if authentication or opening the stream fails.
1157    pub async fn acs_entries(
1158        &self,
1159        parties: Vec<String>,
1160        active_at_offset: i64,
1161    ) -> Result<impl Stream<Item = Result<AcsEntry>> + Send + use<>> {
1162        self.acs_entries_with(crate::request::ActiveContractsRequest::new(
1163            parties,
1164            active_at_offset,
1165        ))
1166        .await
1167    }
1168
1169    /// Like [`Self::acs_entries`], with the full request surface of an
1170    /// [`ActiveContractsRequest`](crate::request::ActiveContractsRequest).
1171    ///
1172    /// # Errors
1173    /// Returns an [`Error`] if authentication or opening the stream fails.
1174    pub async fn acs_entries_with(
1175        &self,
1176        request: crate::request::ActiveContractsRequest,
1177    ) -> Result<impl Stream<Item = Result<AcsEntry>> + Send + use<>> {
1178        request.validate()?;
1179        telemetry::instrument("active_contracts", TRANSPORT_GRPC, async move {
1180            let mut client = service!(self, pb::state_service_client::StateServiceClient::new);
1181            let stream = client
1182                .get_active_contracts(pb::GetActiveContractsRequest {
1183                    active_at_offset: request.active_at_offset,
1184                    event_format: Some(request.event_format()),
1185                    stream_continuation_token: None,
1186                })
1187                .await?
1188                .into_inner();
1189
1190            let stream = stream.filter_map(|item| match item {
1191                Ok(response) => acs_entry(response.contract_entry).map(Ok),
1192                Err(status) => Some(Err(Error::from(status))),
1193            });
1194            Ok(telemetry::instrument_stream(
1195                "active_contracts",
1196                TRANSPORT_GRPC,
1197                stream,
1198            ))
1199        })
1200        .await
1201    }
1202
1203    /// Like [`Self::active_contracts`], but **resumable**: reads the ACS
1204    /// snapshot page-by-page (continuation tokens), retrying a failed page on
1205    /// retriable errors from the last token instead of restarting the snapshot
1206    /// from zero. `max_page_size` bounds each page RPC.
1207    pub fn active_contracts_resumable(
1208        &self,
1209        parties: Vec<String>,
1210        active_at_offset: i64,
1211        max_page_size: i32,
1212    ) -> impl Stream<Item = Result<pb::ActiveContract>> + Send + use<> {
1213        self.active_contracts_resumable_with(
1214            crate::request::ActiveContractsRequest::new(parties, active_at_offset),
1215            max_page_size,
1216        )
1217    }
1218
1219    /// Like [`Self::active_contracts_resumable`], with the full request
1220    /// surface of an
1221    /// [`ActiveContractsRequest`](crate::request::ActiveContractsRequest).
1222    pub fn active_contracts_resumable_with(
1223        &self,
1224        request: crate::request::ActiveContractsRequest,
1225        max_page_size: i32,
1226    ) -> impl Stream<Item = Result<pb::ActiveContract>> + Send + use<> {
1227        self.acs_entries_resumable_with(request, max_page_size)
1228            .filter_map(|item| match item {
1229                Ok(entry) => entry.into_active().map(Ok),
1230                Err(err) => Some(Err(err)),
1231            })
1232    }
1233
1234    /// Like [`Self::acs_entries`], but **resumable** and page-based: reads the
1235    /// snapshot page by page and retries a failed page from the last
1236    /// continuation token instead of restarting from zero. Every entry the
1237    /// participant sent is yielded (see [`AcsEntry`]).
1238    pub fn acs_entries_resumable(
1239        &self,
1240        parties: Vec<String>,
1241        active_at_offset: i64,
1242        max_page_size: i32,
1243    ) -> impl Stream<Item = Result<AcsEntry>> + Send + use<> {
1244        self.acs_entries_resumable_with(
1245            crate::request::ActiveContractsRequest::new(parties, active_at_offset),
1246            max_page_size,
1247        )
1248    }
1249
1250    /// Like [`Self::acs_entries_resumable`], with the full request surface of
1251    /// an [`ActiveContractsRequest`](crate::request::ActiveContractsRequest).
1252    pub fn acs_entries_resumable_with(
1253        &self,
1254        request: crate::request::ActiveContractsRequest,
1255        max_page_size: i32,
1256    ) -> impl Stream<Item = Result<AcsEntry>> + Send + use<> {
1257        let client = self.clone();
1258        let (max_reconnects, backoff_unit) = client.reconnect_policy();
1259        async_stream::stream! {
1260            let mut page_token: Option<Vec<u8>> = None;
1261            let mut reconnects = 0u32;
1262            loop {
1263                match client
1264                    .acs_page_with(
1265                        &request,
1266                        max_page_size,
1267                        page_token.clone(),
1268                    )
1269                    .await
1270                {
1271                    Ok((entries, next)) => {
1272                        reconnects = 0;
1273                        for entry in entries {
1274                            yield Ok(entry);
1275                        }
1276                        match next {
1277                            Some(next) => page_token = Some(next),
1278                            None => return, // snapshot fully delivered
1279                        }
1280                    }
1281                    Err(err) if err.is_retriable() => {
1282                        reconnects += 1;
1283                        if reconnects > max_reconnects {
1284                            // The participant's own failure, for the reason the
1285                            // update stream keeps it: the status and its
1286                            // classification are what the caller acts on.
1287                            tracing::warn!(
1288                                max_reconnects,
1289                                "acs page failed to resume; reporting the failure that caused it"
1290                            );
1291                            yield Err(err);
1292                            return;
1293                        }
1294                        tokio::time::sleep(backoff_unit * reconnects).await;
1295                    }
1296                    Err(err) => {
1297                        yield Err(err);
1298                        return;
1299                    }
1300                }
1301            }
1302        }
1303    }
1304
1305    /// The reconnect policy for resumable streams: `(max_reconnects,
1306    /// backoff_unit)`. Derived from the client's [`RetryConfig`] when one is
1307    /// configured (attempts → reconnect budget, initial backoff → step), else
1308    /// the defaults (5 reconnects, 250ms step).
1309    ///
1310    /// [`RetryConfig`]: canton_core::RetryConfig
1311    fn reconnect_policy(&self) -> (u32, Duration) {
1312        match self.config.retry() {
1313            Some(retry) => (retry.max_attempts, retry.initial_backoff),
1314            None => (5, Duration::from_millis(250)),
1315        }
1316    }
1317
1318    /// Stream ledger updates — transactions and reassignments — for `parties`,
1319    /// starting after `begin_offset` (exclusive). Offset checkpoints are
1320    /// filtered out. Reassignments are surfaced as their own case (each
1321    /// carrying the distinct `Unassigned`/`Assigned` events). Topology events
1322    /// are **not** included; ask for them with
1323    /// [`UpdatesRequest::with_topology_events`](crate::request::UpdatesRequest::with_topology_events)
1324    /// on [`Self::updates_with`].
1325    ///
1326    /// # Errors
1327    /// Returns an [`Error`] if authentication or opening the stream fails.
1328    pub async fn updates(
1329        &self,
1330        parties: Vec<String>,
1331        begin_offset: i64,
1332    ) -> Result<impl Stream<Item = Result<pb::get_updates_response::Update>> + Send + use<>> {
1333        self.updates_with(crate::request::UpdatesRequest::new(parties, begin_offset))
1334            .await
1335    }
1336
1337    /// Like [`Self::updates`], with the full request surface: an
1338    /// [`UpdatesRequest`](crate::request::UpdatesRequest) additionally bounds
1339    /// the stream at an end offset (`until` — the catch-up/sync form, after
1340    /// which the stream terminates), filters by template or interface, selects
1341    /// the ACS-delta shape, includes created-event blobs or topology events,
1342    /// and drops reassignments or record labels.
1343    ///
1344    /// # Errors
1345    /// Returns an [`Error`] if authentication or opening the stream fails.
1346    pub async fn updates_with(
1347        &self,
1348        request: crate::request::UpdatesRequest,
1349    ) -> Result<impl Stream<Item = Result<pb::get_updates_response::Update>> + Send + use<>> {
1350        let stream = self.updates_including_checkpoints(request).await?;
1351        Ok(stream.filter(|item| {
1352            !matches!(
1353                item,
1354                Ok(pb::get_updates_response::Update::OffsetCheckpoint(_))
1355            )
1356        }))
1357    }
1358
1359    /// The update stream with `OffsetCheckpoint` frames left in.
1360    ///
1361    /// A checkpoint is not an update anyone subscribed for, which is why
1362    /// [`Self::updates_with`] drops it. It is, however, the participant naming
1363    /// an offset that is safe to restart from — and it arrives on a quiet
1364    /// stream, where no transaction has moved the position for a while. The
1365    /// resumable path needs exactly that, so it reads this and filters for
1366    /// itself.
1367    async fn updates_including_checkpoints(
1368        &self,
1369        request: crate::request::UpdatesRequest,
1370    ) -> Result<impl Stream<Item = Result<pb::get_updates_response::Update>> + Send + use<>> {
1371        request.validate()?;
1372        let (_, end_inclusive) = request.bounds();
1373        if request.is_descending() && end_inclusive.is_none() {
1374            return Err(Error::InvalidRequest(
1375                "descending order requires a bounded request: set UpdatesRequest::until"
1376                    .to_string(),
1377            ));
1378        }
1379        telemetry::instrument("updates", TRANSPORT_GRPC, async move {
1380            let mut client = service!(self, pb::update_service_client::UpdateServiceClient::new);
1381            let stream = client.get_updates(request.into_grpc()).await?.into_inner();
1382
1383            let stream = stream.filter_map(|item| match item {
1384                Ok(response) => response.update.map(Ok),
1385                Err(status) => Some(Err(Error::from(status))),
1386            });
1387            // Instrumented for the life of the stream, not just its opening:
1388            // a subscription that fails an hour in is the failure that matters.
1389            Ok(telemetry::instrument_stream(
1390                "updates",
1391                TRANSPORT_GRPC,
1392                stream,
1393            ))
1394        })
1395        .await
1396    }
1397
1398    /// Like [`Self::updates`], but **resumable**: on a retriable stream error it
1399    /// reconnects from the last offset it *observed* (rather than restarting
1400    /// from `begin_offset` or losing position), with a short backoff and a
1401    /// bounded number of consecutive reconnects (see the client's
1402    /// [`RetryConfig`](canton_core::RetryConfig)).
1403    ///
1404    /// Observed includes the participant's `OffsetCheckpoint` frames, which are
1405    /// filtered out of what this yields but are the only thing that advances the
1406    /// resume point on a stream where nothing is happening. Once the reconnect
1407    /// budget is spent the stream yields **the failure that caused it** — the
1408    /// participant's status, details and retriable classification intact —
1409    /// rather than an error of the SDK's own.
1410    ///
1411    /// # Example
1412    /// ```no_run
1413    /// # async fn run(client: canton_ledger::CantonClient, party: String)
1414    /// #     -> canton_ledger::Result<()> {
1415    /// use tokio_stream::StreamExt as _;
1416    ///
1417    /// let stream = client.updates_resumable(vec![party], 0);
1418    /// tokio::pin!(stream);
1419    /// while let Some(update) = stream.next().await {
1420    ///     println!("update: {:?}", update?);
1421    /// }
1422    /// # Ok(()) }
1423    /// ```
1424    pub fn updates_resumable(
1425        &self,
1426        parties: Vec<String>,
1427        begin_offset: i64,
1428    ) -> impl Stream<Item = Result<pb::get_updates_response::Update>> + Send + use<> {
1429        self.updates_resumable_with(crate::request::UpdatesRequest::new(parties, begin_offset))
1430    }
1431
1432    /// Like [`Self::updates_resumable`], with the full request surface of an
1433    /// [`UpdatesRequest`](crate::request::UpdatesRequest). A bounded request
1434    /// (`until`) makes this a *resilient catch-up read*: reconnects resume
1435    /// from the last observed offset, and the stream ends once the participant
1436    /// closes it at the end offset.
1437    pub fn updates_resumable_with(
1438        &self,
1439        request: crate::request::UpdatesRequest,
1440    ) -> impl Stream<Item = Result<pb::get_updates_response::Update>> + Send + use<> {
1441        let client = self.clone();
1442        let (max_reconnects, backoff_unit) = client.reconnect_policy();
1443        async_stream::stream! {
1444            if let Err(err) = request.validate() {
1445                yield Err(err);
1446                return;
1447            }
1448            // Resume tracking assumes ascending offsets; a descending request
1449            // would silently re-read on every reconnect, so refuse it.
1450            if request.is_descending() {
1451                yield Err(Error::InvalidRequest(
1452                    "the resumable stream requires ascending order; use updates_with \
1453                     (bounded) for descending reads".to_string(),
1454                ));
1455                return;
1456            }
1457            let mut offset = request.begin_exclusive;
1458            let mut reconnects = 0u32;
1459            loop {
1460                // What made this reconnect necessary. Carried out of the inner
1461                // loop so that giving up reports the participant's own failure
1462                // rather than a fresh error of ours: the status, its structured
1463                // details, the correlation id and the retriable classification
1464                // are what an application branches on, and the moment the
1465                // stream gives up is when it needs them most.
1466                let cause = match client
1467                    .updates_including_checkpoints(request.resume_after(offset))
1468                    .await
1469                {
1470                    Ok(stream) => {
1471                        tokio::pin!(stream);
1472                        loop {
1473                            match stream.next().await {
1474                                Some(Ok(update)) => {
1475                                    // Checkpoints are included here: on a quiet
1476                                    // stream a checkpoint is the only thing that
1477                                    // moves the resume point, and after pruning
1478                                    // the older offset may not be servable at all.
1479                                    offset = update_offset(&update);
1480                                    reconnects = 0;
1481                                    if matches!(
1482                                        update,
1483                                        pb::get_updates_response::Update::OffsetCheckpoint(_)
1484                                    ) {
1485                                        continue;
1486                                    }
1487                                    yield Ok(update);
1488                                }
1489                                Some(Err(err)) if err.is_retriable() => break err,
1490                                Some(Err(err)) => {
1491                                    yield Err(err);
1492                                    return;
1493                                }
1494                                None => return, // server closed the stream cleanly
1495                            }
1496                        }
1497                    }
1498                    Err(err) if err.is_retriable() => err,
1499                    Err(err) => {
1500                        yield Err(err);
1501                        return;
1502                    }
1503                };
1504
1505                reconnects += 1;
1506                if reconnects > max_reconnects {
1507                    tracing::warn!(
1508                        max_reconnects,
1509                        offset,
1510                        "update stream gave up resuming; reporting the failure that caused it"
1511                    );
1512                    yield Err(cause);
1513                    return;
1514                }
1515                tokio::time::sleep(backoff_unit * reconnects).await;
1516            }
1517        }
1518    }
1519}
1520
1521#[cfg(test)]
1522mod tests {
1523    use super::CantonClient;
1524    use crate::Config;
1525    use canton_core::Error;
1526
1527    // Compile-time guarantee that the client is Send + Sync, so consumers can
1528    // share it across tasks and `tokio::spawn` work that holds it. (The streaming
1529    // methods carry `+ Send` in their signatures, which the compiler enforces at
1530    // this crate's build time.)
1531    #[test]
1532    fn client_is_send_and_sync() {
1533        fn assert_send_sync<T: Send + Sync>() {}
1534        assert_send_sync::<CantonClient>();
1535    }
1536
1537    #[tokio::test]
1538    async fn connect_lazy_accepts_a_valid_endpoint() {
1539        // connect_lazy builds a lazy Channel, which needs a Tokio runtime.
1540        assert!(CantonClient::connect_lazy(Config::new("http://localhost:3901")).is_ok());
1541    }
1542
1543    #[tokio::test]
1544    async fn connect_lazy_rejects_a_malformed_endpoint() {
1545        for bad in ["", "not a uri", "http://[bad"] {
1546            let result = CantonClient::connect_lazy(Config::new(bad));
1547            assert!(
1548                matches!(result, Err(Error::InvalidRequest(_))),
1549                "endpoint {bad:?} should be rejected as InvalidRequest, got {result:?}"
1550            );
1551        }
1552    }
1553
1554    // The paged read is inherently bounded, so a request without an end offset
1555    // must be refused before any RPC is attempted.
1556    // Every one of these is refused before a connection is even attempted: the
1557    // client is pointed at a port nothing listens on, so anything that reaches
1558    // the wire fails differently.
1559    #[tokio::test]
1560    async fn requests_the_participant_would_refuse_are_refused_here() {
1561        use crate::request::{ActiveContractsRequest, CompletionsRequest, UpdatesRequest};
1562
1563        let Ok(client) = CantonClient::connect_lazy(Config::new("http://localhost:1")) else {
1564            panic!("lazy connect accepts a valid endpoint");
1565        };
1566        let party = || vec!["alice".to_string()];
1567        let invalid = |result: &Result<_, Error>| matches!(result, Err(Error::InvalidRequest(_)));
1568
1569        // Negative offsets.
1570        assert!(invalid(
1571            &client
1572                .updates_with(UpdatesRequest::new(party(), -1))
1573                .await
1574                .map(|_| ())
1575        ));
1576        assert!(invalid(
1577            &client
1578                .acs_entries_with(ActiveContractsRequest::new(party(), -5))
1579                .await
1580                .map(|_| ())
1581        ));
1582        assert!(invalid(
1583            &client
1584                .completions_with(CompletionsRequest::new(party(), -1))
1585                .await
1586                .map(|_| ())
1587        ));
1588
1589        // A range that ends before it begins.
1590        assert!(invalid(
1591            &client
1592                .updates_with(UpdatesRequest::new(party(), 100).until(50))
1593                .await
1594                .map(|_| ())
1595        ));
1596
1597        // No parties: a subscription that looks alive and can never yield.
1598        assert!(invalid(
1599            &client
1600                .updates_with(UpdatesRequest::new(vec![], 0))
1601                .await
1602                .map(|_| ())
1603        ));
1604    }
1605
1606    #[tokio::test]
1607    async fn submissions_that_cannot_succeed_are_refused_here() {
1608        use crate::command::Submit;
1609
1610        let Ok(client) = CantonClient::connect_lazy(Config::new("http://localhost:1")) else {
1611            panic!("lazy connect accepts a valid endpoint");
1612        };
1613
1614        // No commands: a round trip to be told nothing was asked for.
1615        let empty = client.submission(Submit::new("alice"));
1616        assert!(matches!(
1617            empty.submit().await,
1618            Err(Error::InvalidRequest(_))
1619        ));
1620
1621        // The two minimum-ledger-time forms are mutually exclusive.
1622        let both = client.submission(
1623            Submit::new("alice")
1624                .add_command(crate::command::create(
1625                    crate::command::identifier("pkg", "M", "T"),
1626                    crate::command::record(vec![]),
1627                ))
1628                .with_min_ledger_time_rel(std::time::Duration::from_secs(1))
1629                .with_min_ledger_time_abs(prost_types::Timestamp::default()),
1630        );
1631        assert!(matches!(both.submit().await, Err(Error::InvalidRequest(_))));
1632    }
1633
1634    #[tokio::test]
1635    async fn updates_page_with_requires_a_bounded_request() {
1636        let Ok(client) = CantonClient::connect_lazy(Config::new("http://localhost:1")) else {
1637            panic!("lazy connect accepts a valid endpoint");
1638        };
1639        let unbounded = crate::request::UpdatesRequest::new(vec!["p".to_string()], 0);
1640        let result = client.updates_page_with(&unbounded, 10, None).await;
1641        assert!(
1642            matches!(result, Err(Error::InvalidRequest(_))),
1643            "expected InvalidRequest, got {result:?}"
1644        );
1645    }
1646
1647    // Descending order needs an end offset (streams), and never composes with
1648    // resume tracking — both must fail fast, before any RPC.
1649    #[tokio::test]
1650    async fn descending_requires_bounds_and_is_refused_on_resumable() {
1651        use tokio_stream::StreamExt as _;
1652
1653        let Ok(client) = CantonClient::connect_lazy(Config::new("http://localhost:1")) else {
1654            panic!("lazy connect accepts a valid endpoint");
1655        };
1656        let descending_unbounded =
1657            crate::request::UpdatesRequest::new(vec!["p".to_string()], 0).descending();
1658
1659        let result = client.updates_with(descending_unbounded.clone()).await;
1660        assert!(matches!(result.err(), Some(Error::InvalidRequest(_))));
1661
1662        let stream = client.updates_resumable_with(descending_unbounded.until(10));
1663        tokio::pin!(stream);
1664        let Some(first) = stream.next().await else {
1665            panic!("the resumable stream should yield the rejection");
1666        };
1667        assert!(matches!(first, Err(Error::InvalidRequest(_))));
1668    }
1669
1670    // A reassignment carries its `Unassigned` (source) and `Assigned` (target)
1671    // events as distinct cases rather than collapsing them into one "reassign"
1672    // event — the multi-synchronizer faithfulness the update stream promises.
1673    // (LocalNet is single-synchronizer, so this is verified structurally.)
1674    #[test]
1675    fn reassignment_preserves_the_unassigned_assigned_split() {
1676        use super::pb;
1677        use pb::get_updates_response::Update;
1678        use pb::reassignment_event::Event;
1679
1680        let update = Update::Reassignment(pb::Reassignment {
1681            update_id: "u1".to_string(),
1682            events: vec![
1683                pb::ReassignmentEvent {
1684                    event: Some(Event::Unassigned(pb::UnassignedEvent::default())),
1685                },
1686                pb::ReassignmentEvent {
1687                    event: Some(Event::Assigned(pb::AssignedEvent::default())),
1688                },
1689            ],
1690            ..Default::default()
1691        });
1692
1693        let Update::Reassignment(reassignment) = update else {
1694            panic!("expected a reassignment update");
1695        };
1696        assert_eq!(reassignment.events.len(), 2, "both legs are surfaced");
1697        assert!(matches!(
1698            reassignment.events[0].event,
1699            Some(Event::Unassigned(_))
1700        ));
1701        assert!(matches!(
1702            reassignment.events[1].event,
1703            Some(Event::Assigned(_))
1704        ));
1705    }
1706}