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 trust_participant_hash = prepare.trusts_participant_hash();
596 let (command_id, user_id, request) = prepare.into_request();
597 telemetry::instrument("prepare_submission", TRANSPORT_GRPC, async move {
598 let response = self
599 .with_retry(|| {
600 let request = request.clone();
601 async move {
602 let mut client = service!(
603 self,
604 pb::interactive::interactive_submission_service_client::InteractiveSubmissionServiceClient::new
605 );
606 Ok(client.prepare_submission(request).await?.into_inner())
607 }
608 })
609 .await?;
610 crate::interactive::Prepared::from_response(
611 response,
612 requested_scheme,
613 trust_participant_hash,
614 act_as,
615 read_as,
616 command_id,
617 user_id,
618 )
619 })
620 .await
621 }
622
623 /// Submit a signed prepared transaction and return once the participant has
624 /// accepted it for processing.
625 ///
626 /// Acceptance is not commitment: the outcome arrives on the completion
627 /// stream. Use [`execute_submission_and_wait`](Self::execute_submission_and_wait)
628 /// to wait for it.
629 ///
630 /// # Retry caveat
631 /// With retry enabled, an attempt whose response was lost is retried under
632 /// a fresh submission id and the same change id, and the participant
633 /// answers `ALREADY_EXISTS`: that is the earlier attempt having landed,
634 /// and it is reported as success here, as on
635 /// [`submit`](Self::submit). An `Ok` still means only
636 /// *accepted*; whether it committed is a completion, reachable by
637 /// [`await_completion`](Self::await_completion) with the
638 /// [`Executable::change_id`](crate::interactive::Executable::change_id)
639 /// taken before the call.
640 ///
641 /// # Errors
642 /// As any gRPC call. A missing or wrong signature is a rejection from the
643 /// participant, not a local error.
644 pub async fn execute_submission(
645 &self,
646 executable: crate::interactive::Executable,
647 ) -> Result<()> {
648 let request = executable.into_execute_request();
649 telemetry::instrument("execute_submission", TRANSPORT_GRPC, async move {
650 let attempt = std::sync::atomic::AtomicU32::new(0);
651 self.with_retry(|| {
652 let request = fresh_submission_id(request.clone());
653 let retry = attempt.fetch_add(1, std::sync::atomic::Ordering::Relaxed) > 0;
654 async move {
655 let mut client = service!(
656 self,
657 pb::interactive::interactive_submission_service_client::InteractiveSubmissionServiceClient::new
658 );
659 match client.execute_submission(request).await {
660 Ok(_) => Ok(()),
661 Err(status) if retry && is_duplicate_of_our_own(&status) => {
662 // Same reasoning as the ordinary submit: a
663 // previous attempt of this loop was accepted and
664 // its answer lost, and reporting the duplicate as
665 // a failure would say the opposite of what
666 // happened.
667 tracing::debug!(
668 "execute retry was de-duplicated; the earlier attempt is the one that landed"
669 );
670 Ok(())
671 }
672 Err(status) => Err(Error::from(status)),
673 }
674 }
675 })
676 .await
677 })
678 .await
679 }
680
681 /// Submit a signed prepared transaction and wait for it to commit.
682 ///
683 /// The response carries the update id **and** the completion offset — the
684 /// offset a follow-up completion or update read starts from, which is why
685 /// it is returned whole rather than as the id alone.
686 ///
687 /// # Retry caveat
688 /// As [`execute_submission`](Self::execute_submission): a retried attempt
689 /// that the participant de-duplicates is reported as the rejection it
690 /// answers with, so on an ambiguous outcome recover through
691 /// [`await_completion`](Self::await_completion) with the change id taken
692 /// before the call.
693 ///
694 /// # Errors
695 /// As any gRPC call, plus a rejection if the transaction fails.
696 pub async fn execute_submission_and_wait(
697 &self,
698 executable: crate::interactive::Executable,
699 ) -> Result<pb::interactive::ExecuteSubmissionAndWaitResponse> {
700 let request = executable.into_execute_and_wait_request();
701 telemetry::instrument("execute_submission_and_wait", TRANSPORT_GRPC, async move {
702 let response = self
703 .with_retry(|| {
704 let request = fresh_submission_id(request.clone());
705 async move {
706 let mut client = service!(
707 self,
708 pb::interactive::interactive_submission_service_client::InteractiveSubmissionServiceClient::new
709 );
710 Ok(client
711 .execute_submission_and_wait(request)
712 .await?
713 .into_inner())
714 }
715 })
716 .await?;
717 Ok(response)
718 })
719 .await
720 }
721
722 /// Submit a signed prepared transaction and wait for the committed
723 /// transaction itself.
724 ///
725 /// The events it carries follow
726 /// [`Executable::with_transaction_shape`](crate::interactive::Executable::with_transaction_shape).
727 ///
728 /// # Errors
729 /// As any gRPC call, plus [`Error::UnexpectedResponse`] if the participant
730 /// reports success without a transaction.
731 pub async fn execute_submission_and_wait_for_transaction(
732 &self,
733 executable: crate::interactive::Executable,
734 ) -> Result<pb::Transaction> {
735 let event_format = wildcard_event_format(&executable.witnesses());
736 let request = executable.into_execute_and_wait_for_transaction_request(event_format);
737 telemetry::instrument(
738 "execute_submission_and_wait_for_transaction",
739 TRANSPORT_GRPC,
740 async move {
741 let response = self
742 .with_retry(|| {
743 let request = fresh_submission_id(request.clone());
744 async move {
745 let mut client = service!(
746 self,
747 pb::interactive::interactive_submission_service_client::InteractiveSubmissionServiceClient::new
748 );
749 Ok(client
750 .execute_submission_and_wait_for_transaction(request)
751 .await?
752 .into_inner())
753 }
754 })
755 .await?;
756
757 response.transaction.ok_or_else(|| {
758 Error::UnexpectedResponse("response contained no transaction".to_string())
759 })
760 },
761 )
762 .await
763 }
764
765 /// Subscribe to the command-completion stream for `parties`, starting after
766 /// `begin_offset` (exclusive). Offset checkpoints are filtered out, so the
767 /// stream yields only [`pb::Completion`]s.
768 ///
769 /// # Errors
770 /// Returns an [`Error`] if authentication or opening the stream fails.
771 pub async fn completions(
772 &self,
773 parties: Vec<String>,
774 begin_offset: i64,
775 ) -> Result<impl Stream<Item = Result<pb::Completion>> + Send + use<>> {
776 self.completions_with(crate::request::CompletionsRequest::new(
777 parties,
778 begin_offset,
779 ))
780 .await
781 }
782
783 /// Like [`Self::completions`], with the full request surface: a
784 /// [`CompletionsRequest`](crate::request::CompletionsRequest) additionally
785 /// selects the `user_id` whose command completions to stream (pair it with
786 /// [`Submit::with_user_id`](crate::Submit::with_user_id)).
787 ///
788 /// # Errors
789 /// Returns an [`Error`] if authentication or opening the stream fails.
790 pub async fn completions_with(
791 &self,
792 request: crate::request::CompletionsRequest,
793 ) -> Result<impl Stream<Item = Result<pb::Completion>> + Send + use<>> {
794 request.validate()?;
795 telemetry::instrument("completions", TRANSPORT_GRPC, async move {
796 let mut client = service!(
797 self,
798 pb::command_completion_service_client::CommandCompletionServiceClient::new
799 );
800 let stream = client
801 .completion_stream(request.into_grpc())
802 .await?
803 .into_inner();
804
805 let stream = stream.filter_map(|item| match item {
806 Ok(response) => match response.completion_response {
807 Some(pb::completion_stream_response::CompletionResponse::Completion(
808 completion,
809 )) => Some(Ok(completion)),
810 _ => None, // skip offset checkpoints
811 },
812 Err(status) => Some(Err(Error::from(status))),
813 });
814 Ok(telemetry::instrument_stream(
815 "completions",
816 TRANSPORT_GRPC,
817 stream,
818 ))
819 })
820 .await
821 }
822
823 /// Recover the completion for a specific command by scanning the completion
824 /// stream from `begin_offset`, up to `timeout`.
825 ///
826 /// This is the command-recovery path: after a crash, lost connection, or
827 /// timeout, the outcome of a pending command is read back from the
828 /// completion endpoint instead of blindly re-submitting. If the command's
829 /// completion reports a non-OK status, this returns [`Error::CommandRejected`].
830 ///
831 /// Matching is on the whole [`ChangeId`](crate::ChangeId) — user, acting
832 /// parties and command id — because that is what identifies a command to
833 /// Canton. A command id on its own is not unique across the users of a
834 /// participant, and answering with somebody else's completion is worse
835 /// than answering with none.
836 ///
837 /// The completion stream is a live subscription that does not self-terminate,
838 /// so `timeout` bounds how long to wait for the target completion.
839 ///
840 /// # Errors
841 /// Returns [`Error::Timeout`] if the completion is not seen within `timeout`,
842 /// [`Error::CommandRejected`] if the ledger rejected the command, or another
843 /// [`Error`] if the stream fails.
844 pub async fn await_completion(
845 &self,
846 change_id: &crate::command::ChangeId,
847 begin_offset: i64,
848 timeout: Duration,
849 ) -> Result<pb::Completion> {
850 let scan = async {
851 let stream = self
852 .completions(change_id.act_as().to_vec(), begin_offset)
853 .await?;
854 tokio::pin!(stream);
855 while let Some(item) = stream.next().await {
856 let completion = item?;
857 if change_id.matches(&completion) {
858 // A non-OK gRPC status on the completion means the ledger
859 // rejected the command for business/interpretation reasons.
860 if let Some(status) = &completion.status {
861 // google.rpc.Status code 0 == OK; anything else is a rejection.
862 if status.code != 0 {
863 return Err(Error::CommandRejected {
864 code: format!("{:?}", tonic::Code::from(status.code)),
865 message: status.message.clone(),
866 });
867 }
868 }
869 return Ok(completion);
870 }
871 }
872 Err(Error::UnexpectedResponse(format!(
873 "completion stream ended before command {} was seen",
874 change_id.command_id()
875 )))
876 };
877
878 tokio::time::timeout(timeout, scan)
879 .await
880 .map_err(|_| Error::Timeout)?
881 }
882
883 /// Return the current ledger end offset as seen by the participant.
884 ///
885 /// A value of `0` means the participant's view of the ledger is empty.
886 /// This is an authenticated endpoint.
887 ///
888 /// # Errors
889 /// Returns an [`Error`] if authentication or the RPC fails.
890 pub async fn ledger_end(&self) -> Result<i64> {
891 telemetry::instrument("ledger_end", TRANSPORT_GRPC, async {
892 self.with_retry(|| async {
893 let mut client = service!(self, pb::state_service_client::StateServiceClient::new);
894 let response = client
895 .get_ledger_end(pb::GetLedgerEndRequest {})
896 .await?
897 .into_inner();
898 Ok(response.offset)
899 })
900 .await
901 })
902 .await
903 }
904
905 /// Fetch the created and/or archived events for a contract by id
906 /// (`EventQueryService.GetEventsByContractId`), with verbose records and
907 /// no created-event blob. To obtain a contract's `created_event_blob`
908 /// (for disclosure), use a template-filtered
909 /// [`Self::active_contracts_with`] read with
910 /// [`ActiveContractsRequest::with_created_event_blobs`](crate::request::ActiveContractsRequest::with_created_event_blobs).
911 ///
912 /// # Errors
913 /// Returns an [`Error`] if authentication or the RPC fails.
914 pub async fn events_by_contract_id(
915 &self,
916 contract_id: impl Into<String>,
917 parties: Vec<String>,
918 ) -> Result<pb::GetEventsByContractIdResponse> {
919 let contract_id = contract_id.into();
920 telemetry::instrument("events_by_contract_id", TRANSPORT_GRPC, async move {
921 // Retried like the other reads: this is a lookup, so a transient
922 // failure is worth another attempt rather than a caller's error
923 // path. Only submissions have a reason to be careful here.
924 self.with_retry(|| {
925 let request = pb::GetEventsByContractIdRequest {
926 contract_id: contract_id.clone(),
927 event_format: Some(wildcard_event_format(&parties)),
928 };
929 async move {
930 let mut client = service!(
931 self,
932 pb::event_query_service_client::EventQueryServiceClient::new
933 );
934 Ok(client
935 .get_events_by_contract_id(request)
936 .await?
937 .into_inner())
938 }
939 })
940 .await
941 })
942 .await
943 }
944
945 /// How far the participant has pruned (`StateService.GetLatestPrunedOffsets`).
946 ///
947 /// A read that starts at or below
948 /// [`participant_pruned_up_to_inclusive`](PrunedOffsets::participant_pruned_up_to_inclusive)
949 /// is refused; [`Error::pruned_earliest_offset`] says the same thing after
950 /// the fact, on the error such a read returns.
951 ///
952 /// # Errors
953 /// Returns an [`Error`] if authentication or the RPC fails.
954 pub async fn latest_pruned_offsets(&self) -> Result<PrunedOffsets> {
955 telemetry::instrument("latest_pruned_offsets", TRANSPORT_GRPC, async {
956 self.with_retry(|| async {
957 let mut client = service!(self, pb::state_service_client::StateServiceClient::new);
958 let response = client
959 .get_latest_pruned_offsets(pb::GetLatestPrunedOffsetsRequest {})
960 .await?
961 .into_inner();
962 Ok(PrunedOffsets {
963 participant_pruned_up_to_inclusive: response.participant_pruned_up_to_inclusive,
964 all_divulged_contracts_pruned_up_to_inclusive: response
965 .all_divulged_contracts_pruned_up_to_inclusive,
966 })
967 })
968 .await
969 })
970 .await
971 }
972
973 /// The update at `offset`, as `parties` see it (`UpdateService.GetUpdateByOffset`),
974 /// in the default format of [`UpdatesRequest::new`](crate::request::UpdatesRequest::new).
975 ///
976 /// Reading back a stored offset without scanning a stream to it. `Ok(None)`
977 /// when there is no update at that offset visible to `parties`.
978 ///
979 /// # Errors
980 /// Returns an [`Error`] if authentication or the RPC fails.
981 pub async fn update_by_offset(
982 &self,
983 offset: i64,
984 parties: Vec<String>,
985 ) -> Result<Option<pb::get_update_response::Update>> {
986 self.update_by_offset_with(offset, &crate::request::UpdatesRequest::new(parties, 0))
987 .await
988 }
989
990 /// Like [`Self::update_by_offset`], in the format an
991 /// [`UpdatesRequest`](crate::request::UpdatesRequest) describes (parties,
992 /// template or interface filters, transaction shape, created-event blobs,
993 /// reassignments, topology events, verbosity). Its offsets are not used.
994 ///
995 /// # Errors
996 /// Returns an [`Error`] if the request names no party, or authentication or
997 /// the RPC fails.
998 pub async fn update_by_offset_with(
999 &self,
1000 offset: i64,
1001 format: &crate::request::UpdatesRequest,
1002 ) -> Result<Option<pb::get_update_response::Update>> {
1003 format.validate()?;
1004 let request = pb::GetUpdateByOffsetRequest {
1005 offset,
1006 update_format: Some(format.update_format()),
1007 };
1008 telemetry::instrument("update_by_offset", TRANSPORT_GRPC, async move {
1009 let response = self
1010 .with_retry(|| {
1011 let request = request.clone();
1012 async move {
1013 let mut client =
1014 service!(self, pb::update_service_client::UpdateServiceClient::new);
1015 Ok(client.get_update_by_offset(request).await?.into_inner())
1016 }
1017 })
1018 .await;
1019 found(response)?.map(present_update).transpose()
1020 })
1021 .await
1022 }
1023
1024 /// The update with id `update_id`, as `parties` see it
1025 /// (`UpdateService.GetUpdateById`), in the default format of
1026 /// [`UpdatesRequest::new`](crate::request::UpdatesRequest::new).
1027 ///
1028 /// The id is what a submission's completion and its transaction carry, so
1029 /// this reads back the outcome of a command without scanning for it.
1030 /// `Ok(None)` when no update with that id is visible to `parties`.
1031 ///
1032 /// # Errors
1033 /// Returns an [`Error`] if authentication or the RPC fails.
1034 pub async fn update_by_id(
1035 &self,
1036 update_id: impl Into<String>,
1037 parties: Vec<String>,
1038 ) -> Result<Option<pb::get_update_response::Update>> {
1039 self.update_by_id_with(update_id, &crate::request::UpdatesRequest::new(parties, 0))
1040 .await
1041 }
1042
1043 /// Like [`Self::update_by_id`], in the format an
1044 /// [`UpdatesRequest`](crate::request::UpdatesRequest) describes. Its
1045 /// offsets are not used.
1046 ///
1047 /// # Errors
1048 /// Returns an [`Error`] if the request names no party, or authentication or
1049 /// the RPC fails.
1050 pub async fn update_by_id_with(
1051 &self,
1052 update_id: impl Into<String>,
1053 format: &crate::request::UpdatesRequest,
1054 ) -> Result<Option<pb::get_update_response::Update>> {
1055 format.validate()?;
1056 let request = pb::GetUpdateByIdRequest {
1057 update_id: update_id.into(),
1058 update_format: Some(format.update_format()),
1059 };
1060 telemetry::instrument("update_by_id", TRANSPORT_GRPC, async move {
1061 let response = self
1062 .with_retry(|| {
1063 let request = request.clone();
1064 async move {
1065 let mut client =
1066 service!(self, pb::update_service_client::UpdateServiceClient::new);
1067 Ok(client.get_update_by_id(request).await?.into_inner())
1068 }
1069 })
1070 .await;
1071 found(response)?.map(present_update).transpose()
1072 })
1073 .await
1074 }
1075
1076 /// Fetch one **page** of the Active Contract Set for `parties` as of
1077 /// `active_at_offset`. Returns the page's active contracts and the next page
1078 /// token (`None` once the last page has been read); pass the token back in
1079 /// to fetch the following page.
1080 ///
1081 /// # Errors
1082 /// Returns an [`Error`] if authentication or the RPC fails.
1083 pub async fn active_contracts_page(
1084 &self,
1085 parties: Vec<String>,
1086 active_at_offset: i64,
1087 max_page_size: i32,
1088 page_token: Option<Vec<u8>>,
1089 ) -> Result<(Vec<pb::ActiveContract>, Option<Vec<u8>>)> {
1090 let request = crate::request::ActiveContractsRequest::new(parties, active_at_offset);
1091 self.active_contracts_page_with(&request, max_page_size, page_token)
1092 .await
1093 }
1094
1095 /// Like [`Self::active_contracts_page`], with the full request surface of
1096 /// an [`ActiveContractsRequest`](crate::request::ActiveContractsRequest)
1097 /// (template/interface filters, created-event blobs, non-verbose records).
1098 ///
1099 /// # Errors
1100 /// Returns an [`Error`] if authentication or the RPC fails.
1101 pub async fn active_contracts_page_with(
1102 &self,
1103 request: &crate::request::ActiveContractsRequest,
1104 max_page_size: i32,
1105 page_token: Option<Vec<u8>>,
1106 ) -> Result<(Vec<pb::ActiveContract>, Option<Vec<u8>>)> {
1107 let (entries, next) = self
1108 .acs_page_with(request, max_page_size, page_token)
1109 .await?;
1110 Ok((
1111 entries
1112 .into_iter()
1113 .filter_map(AcsEntry::into_active)
1114 .collect(),
1115 next,
1116 ))
1117 }
1118
1119 /// One page of the Active Contract Set for `parties`, **losslessly**: every
1120 /// entry the participant sent, active or incomplete (see [`AcsEntry`]).
1121 ///
1122 /// # Errors
1123 /// Returns an [`Error`] if authentication or the RPC fails.
1124 pub async fn acs_page(
1125 &self,
1126 parties: Vec<String>,
1127 active_at_offset: i64,
1128 max_page_size: i32,
1129 page_token: Option<Vec<u8>>,
1130 ) -> Result<(Vec<AcsEntry>, Option<Vec<u8>>)> {
1131 let request = crate::request::ActiveContractsRequest::new(parties, active_at_offset);
1132 self.acs_page_with(&request, max_page_size, page_token)
1133 .await
1134 }
1135
1136 /// Like [`Self::acs_page`], with the full request surface of an
1137 /// [`ActiveContractsRequest`](crate::request::ActiveContractsRequest).
1138 ///
1139 /// # Errors
1140 /// Returns an [`Error`] if authentication or the RPC fails.
1141 pub async fn acs_page_with(
1142 &self,
1143 request: &crate::request::ActiveContractsRequest,
1144 max_page_size: i32,
1145 page_token: Option<Vec<u8>>,
1146 ) -> Result<(Vec<AcsEntry>, Option<Vec<u8>>)> {
1147 request.validate()?;
1148 telemetry::instrument("active_contracts_page", TRANSPORT_GRPC, async move {
1149 self.with_retry(|| {
1150 let page_request = pb::GetActiveContractsPageRequest {
1151 active_at_offset: Some(request.active_at_offset),
1152 event_format: Some(request.event_format()),
1153 max_page_size: Some(max_page_size),
1154 page_token: page_token.clone(),
1155 };
1156 async move {
1157 let mut client =
1158 service!(self, pb::state_service_client::StateServiceClient::new);
1159 let response = client
1160 .get_active_contracts_page(page_request)
1161 .await?
1162 .into_inner();
1163
1164 let entries = response
1165 .active_contracts
1166 .into_iter()
1167 .filter_map(|entry| acs_entry(entry.contract_entry))
1168 .collect();
1169 Ok((entries, response.next_page_token))
1170 }
1171 })
1172 .await
1173 })
1174 .await
1175 }
1176
1177 /// Fetch one **page** of updates in the offset range
1178 /// `(begin_offset_exclusive, end_offset_inclusive]`, optionally in reverse
1179 /// (`descending`) order. Returns the page items and the next page token
1180 /// (`None` once the last page has been read).
1181 ///
1182 /// # Errors
1183 /// Returns an [`Error`] if authentication or the RPC fails.
1184 pub async fn updates_page(
1185 &self,
1186 parties: Vec<String>,
1187 begin_offset_exclusive: i64,
1188 end_offset_inclusive: i64,
1189 max_page_size: i32,
1190 descending: bool,
1191 page_token: Option<Vec<u8>>,
1192 ) -> Result<(Vec<pb::GetUpdateResponse>, Option<Vec<u8>>)> {
1193 let mut request = crate::request::UpdatesRequest::new(parties, begin_offset_exclusive)
1194 .until(end_offset_inclusive);
1195 if descending {
1196 request = request.descending();
1197 }
1198 self.updates_page_with(&request, max_page_size, page_token)
1199 .await
1200 }
1201
1202 /// Like [`Self::updates_page`], with the full request surface of an
1203 /// [`UpdatesRequest`](crate::request::UpdatesRequest) (template/interface
1204 /// filters, transaction shape, descending order, created-event blobs,
1205 /// topology events, non-verbose records). The request's bounds supply the
1206 /// page range, so
1207 /// [`UpdatesRequest::until`](crate::request::UpdatesRequest::until) is
1208 /// required here — the paged read is inherently bounded.
1209 ///
1210 /// # Errors
1211 /// Returns [`Error::InvalidRequest`] if the request has no end offset, or
1212 /// another [`Error`] if authentication or the RPC fails.
1213 pub async fn updates_page_with(
1214 &self,
1215 request: &crate::request::UpdatesRequest,
1216 max_page_size: i32,
1217 page_token: Option<Vec<u8>>,
1218 ) -> Result<(Vec<pb::GetUpdateResponse>, Option<Vec<u8>>)> {
1219 request.validate()?;
1220 let (begin_exclusive, end_inclusive) = request.bounds();
1221 let Some(end_inclusive) = end_inclusive else {
1222 return Err(Error::InvalidRequest(
1223 "updates_page_with requires a bounded request: set UpdatesRequest::until"
1224 .to_string(),
1225 ));
1226 };
1227 telemetry::instrument("updates_page", TRANSPORT_GRPC, async move {
1228 self.with_retry(|| {
1229 let page_request = pb::GetUpdatesPageRequest {
1230 begin_offset_exclusive: Some(begin_exclusive),
1231 end_offset_inclusive: Some(end_inclusive),
1232 max_page_size: Some(max_page_size),
1233 update_format: Some(request.update_format()),
1234 descending_order: request.is_descending(),
1235 page_token: page_token.clone(),
1236 };
1237 async move {
1238 let mut client =
1239 service!(self, pb::update_service_client::UpdateServiceClient::new);
1240 let response = client.get_updates_page(page_request).await?.into_inner();
1241 Ok((response.updates, response.next_page_token))
1242 }
1243 })
1244 .await
1245 })
1246 .await
1247 }
1248
1249 /// Stream the Active Contract Set for `parties` as of `active_at_offset`
1250 /// (typically the current ledger end). Yields the active contracts,
1251 /// wildcard-filtered to the given parties.
1252 ///
1253 /// # Errors
1254 /// Returns an [`Error`] if authentication or opening the stream fails.
1255 pub async fn active_contracts(
1256 &self,
1257 parties: Vec<String>,
1258 active_at_offset: i64,
1259 ) -> Result<impl Stream<Item = Result<pb::ActiveContract>> + Send + use<>> {
1260 self.active_contracts_with(crate::request::ActiveContractsRequest::new(
1261 parties,
1262 active_at_offset,
1263 ))
1264 .await
1265 }
1266
1267 /// Like [`Self::active_contracts`], with the full request surface: an
1268 /// [`ActiveContractsRequest`](crate::request::ActiveContractsRequest)
1269 /// additionally filters by template or interface, includes created-event
1270 /// blobs, and drops record labels.
1271 ///
1272 /// # Errors
1273 /// Returns an [`Error`] if authentication or opening the stream fails.
1274 pub async fn active_contracts_with(
1275 &self,
1276 request: crate::request::ActiveContractsRequest,
1277 ) -> Result<impl Stream<Item = Result<pb::ActiveContract>> + Send + use<>> {
1278 let stream = self.acs_entries_with(request).await?;
1279 Ok(stream.filter_map(|item| match item {
1280 Ok(entry) => entry.into_active().map(Ok),
1281 Err(err) => Some(Err(err)),
1282 }))
1283 }
1284
1285 /// Stream the Active Contract Set for `parties` **losslessly**: every entry
1286 /// the participant sends, active or incomplete (see [`AcsEntry`]).
1287 ///
1288 /// # Errors
1289 /// Returns an [`Error`] if authentication or opening the stream fails.
1290 pub async fn acs_entries(
1291 &self,
1292 parties: Vec<String>,
1293 active_at_offset: i64,
1294 ) -> Result<impl Stream<Item = Result<AcsEntry>> + Send + use<>> {
1295 self.acs_entries_with(crate::request::ActiveContractsRequest::new(
1296 parties,
1297 active_at_offset,
1298 ))
1299 .await
1300 }
1301
1302 /// Like [`Self::acs_entries`], with the full request surface of an
1303 /// [`ActiveContractsRequest`](crate::request::ActiveContractsRequest).
1304 ///
1305 /// # Errors
1306 /// Returns an [`Error`] if authentication or opening the stream fails.
1307 pub async fn acs_entries_with(
1308 &self,
1309 request: crate::request::ActiveContractsRequest,
1310 ) -> Result<impl Stream<Item = Result<AcsEntry>> + Send + use<>> {
1311 request.validate()?;
1312 telemetry::instrument("active_contracts", TRANSPORT_GRPC, async move {
1313 let mut client = service!(self, pb::state_service_client::StateServiceClient::new);
1314 let stream = client
1315 .get_active_contracts(pb::GetActiveContractsRequest {
1316 active_at_offset: request.active_at_offset,
1317 event_format: Some(request.event_format()),
1318 stream_continuation_token: None,
1319 })
1320 .await?
1321 .into_inner();
1322
1323 let stream = stream.filter_map(|item| match item {
1324 Ok(response) => acs_entry(response.contract_entry).map(Ok),
1325 Err(status) => Some(Err(Error::from(status))),
1326 });
1327 Ok(telemetry::instrument_stream(
1328 "active_contracts",
1329 TRANSPORT_GRPC,
1330 stream,
1331 ))
1332 })
1333 .await
1334 }
1335
1336 /// Like [`Self::active_contracts`], but **resumable**: reads the ACS
1337 /// snapshot page-by-page (continuation tokens), retrying a failed page on
1338 /// retriable errors from the last token instead of restarting the snapshot
1339 /// from zero. `max_page_size` bounds each page RPC.
1340 pub fn active_contracts_resumable(
1341 &self,
1342 parties: Vec<String>,
1343 active_at_offset: i64,
1344 max_page_size: i32,
1345 ) -> impl Stream<Item = Result<pb::ActiveContract>> + Send + use<> {
1346 self.active_contracts_resumable_with(
1347 crate::request::ActiveContractsRequest::new(parties, active_at_offset),
1348 max_page_size,
1349 )
1350 }
1351
1352 /// Like [`Self::active_contracts_resumable`], with the full request
1353 /// surface of an
1354 /// [`ActiveContractsRequest`](crate::request::ActiveContractsRequest).
1355 pub fn active_contracts_resumable_with(
1356 &self,
1357 request: crate::request::ActiveContractsRequest,
1358 max_page_size: i32,
1359 ) -> impl Stream<Item = Result<pb::ActiveContract>> + Send + use<> {
1360 self.acs_entries_resumable_with(request, max_page_size)
1361 .filter_map(|item| match item {
1362 Ok(entry) => entry.into_active().map(Ok),
1363 Err(err) => Some(Err(err)),
1364 })
1365 }
1366
1367 /// Like [`Self::acs_entries`], but **resumable** and page-based: reads the
1368 /// snapshot page by page and retries a failed page from the last
1369 /// continuation token instead of restarting from zero. Every entry the
1370 /// participant sent is yielded (see [`AcsEntry`]).
1371 pub fn acs_entries_resumable(
1372 &self,
1373 parties: Vec<String>,
1374 active_at_offset: i64,
1375 max_page_size: i32,
1376 ) -> impl Stream<Item = Result<AcsEntry>> + Send + use<> {
1377 self.acs_entries_resumable_with(
1378 crate::request::ActiveContractsRequest::new(parties, active_at_offset),
1379 max_page_size,
1380 )
1381 }
1382
1383 /// Like [`Self::acs_entries_resumable`], with the full request surface of
1384 /// an [`ActiveContractsRequest`](crate::request::ActiveContractsRequest).
1385 pub fn acs_entries_resumable_with(
1386 &self,
1387 request: crate::request::ActiveContractsRequest,
1388 max_page_size: i32,
1389 ) -> impl Stream<Item = Result<AcsEntry>> + Send + use<> {
1390 let client = self.clone();
1391 let (max_reconnects, backoff_unit) = client.reconnect_policy();
1392 async_stream::stream! {
1393 let mut page_token: Option<Vec<u8>> = None;
1394 let mut reconnects = 0u32;
1395 loop {
1396 match client
1397 .acs_page_with(
1398 &request,
1399 max_page_size,
1400 page_token.clone(),
1401 )
1402 .await
1403 {
1404 Ok((entries, next)) => {
1405 reconnects = 0;
1406 for entry in entries {
1407 yield Ok(entry);
1408 }
1409 match next {
1410 Some(next) => page_token = Some(next),
1411 None => return, // snapshot fully delivered
1412 }
1413 }
1414 Err(err) if err.is_retriable() => {
1415 reconnects += 1;
1416 if reconnects > max_reconnects {
1417 // The participant's own failure, for the reason the
1418 // update stream keeps it: the status and its
1419 // classification are what the caller acts on.
1420 tracing::warn!(
1421 max_reconnects,
1422 "acs page failed to resume; reporting the failure that caused it"
1423 );
1424 yield Err(err);
1425 return;
1426 }
1427 tokio::time::sleep(backoff_unit * reconnects).await;
1428 }
1429 Err(err) => {
1430 yield Err(err);
1431 return;
1432 }
1433 }
1434 }
1435 }
1436 }
1437
1438 /// The reconnect policy for resumable streams: `(max_reconnects,
1439 /// backoff_unit)`. Derived from the client's [`RetryConfig`] when one is
1440 /// configured (attempts → reconnect budget, initial backoff → step), else
1441 /// the defaults (5 reconnects, 250ms step).
1442 ///
1443 /// [`RetryConfig`]: canton_core::RetryConfig
1444 fn reconnect_policy(&self) -> (u32, Duration) {
1445 match self.config.retry() {
1446 Some(retry) => (retry.max_attempts, retry.initial_backoff),
1447 None => (5, Duration::from_millis(250)),
1448 }
1449 }
1450
1451 /// Stream ledger updates — transactions and reassignments — for `parties`,
1452 /// starting after `begin_offset` (exclusive). Offset checkpoints are
1453 /// filtered out. Reassignments are surfaced as their own case (each
1454 /// carrying the distinct `Unassigned`/`Assigned` events). Topology events
1455 /// are **not** included; ask for them with
1456 /// [`UpdatesRequest::with_topology_events`](crate::request::UpdatesRequest::with_topology_events)
1457 /// on [`Self::updates_with`].
1458 ///
1459 /// # Errors
1460 /// Returns an [`Error`] if authentication or opening the stream fails.
1461 pub async fn updates(
1462 &self,
1463 parties: Vec<String>,
1464 begin_offset: i64,
1465 ) -> Result<impl Stream<Item = Result<pb::get_updates_response::Update>> + Send + use<>> {
1466 self.updates_with(crate::request::UpdatesRequest::new(parties, begin_offset))
1467 .await
1468 }
1469
1470 /// Like [`Self::updates`], with the full request surface: an
1471 /// [`UpdatesRequest`](crate::request::UpdatesRequest) additionally bounds
1472 /// the stream at an end offset (`until` — the catch-up/sync form, after
1473 /// which the stream terminates), filters by template or interface, selects
1474 /// the ACS-delta shape, includes created-event blobs or topology events,
1475 /// and drops reassignments or record labels.
1476 ///
1477 /// # Errors
1478 /// Returns an [`Error`] if authentication or opening the stream fails.
1479 pub async fn updates_with(
1480 &self,
1481 request: crate::request::UpdatesRequest,
1482 ) -> Result<impl Stream<Item = Result<pb::get_updates_response::Update>> + Send + use<>> {
1483 let stream = self.updates_including_checkpoints(request).await?;
1484 Ok(stream.filter(|item| {
1485 !matches!(
1486 item,
1487 Ok(pb::get_updates_response::Update::OffsetCheckpoint(_))
1488 )
1489 }))
1490 }
1491
1492 /// Like [`Self::updates_with`], as the SDK's own
1493 /// [`Update`](crate::update::Update): the type
1494 /// [`JsonClient::updates_typed_with`](crate::JsonClient::updates_typed_with)
1495 /// returns too, so code that reads updates depends on neither the transport
1496 /// nor the vendored protos (ADR-0010 §4). Checkpoints are dropped, as
1497 /// [`Self::updates_with`] drops them.
1498 ///
1499 /// # Errors
1500 /// Returns an [`Error`] if authentication or opening the stream fails; the
1501 /// stream yields one for a malformed update.
1502 pub async fn updates_typed_with(
1503 &self,
1504 request: crate::request::UpdatesRequest,
1505 ) -> Result<impl Stream<Item = Result<crate::update::Update>> + Send + use<>> {
1506 let stream = self.updates_with(request).await?;
1507 Ok(stream.map(|item| item.and_then(crate::update::Update::from_grpc)))
1508 }
1509
1510 /// The update stream with `OffsetCheckpoint` frames left in.
1511 ///
1512 /// A checkpoint is not an update anyone subscribed for, which is why
1513 /// [`Self::updates_with`] drops it. It is, however, the participant naming
1514 /// an offset that is safe to restart from — and it arrives on a quiet
1515 /// stream, where no transaction has moved the position for a while. The
1516 /// resumable path needs exactly that, so it reads this and filters for
1517 /// itself.
1518 async fn updates_including_checkpoints(
1519 &self,
1520 request: crate::request::UpdatesRequest,
1521 ) -> Result<impl Stream<Item = Result<pb::get_updates_response::Update>> + Send + use<>> {
1522 request.validate()?;
1523 let (_, end_inclusive) = request.bounds();
1524 if request.is_descending() && end_inclusive.is_none() {
1525 return Err(Error::InvalidRequest(
1526 "descending order requires a bounded request: set UpdatesRequest::until"
1527 .to_string(),
1528 ));
1529 }
1530 telemetry::instrument("updates", TRANSPORT_GRPC, async move {
1531 let mut client = service!(self, pb::update_service_client::UpdateServiceClient::new);
1532 let stream = client.get_updates(request.into_grpc()).await?.into_inner();
1533
1534 let stream = stream.filter_map(|item| match item {
1535 Ok(response) => response.update.map(Ok),
1536 Err(status) => Some(Err(Error::from(status))),
1537 });
1538 // Instrumented for the life of the stream, not just its opening:
1539 // a subscription that fails an hour in is the failure that matters.
1540 Ok(telemetry::instrument_stream(
1541 "updates",
1542 TRANSPORT_GRPC,
1543 stream,
1544 ))
1545 })
1546 .await
1547 }
1548
1549 /// Like [`Self::updates`], but **resumable**: on a retriable stream error it
1550 /// reconnects from the last offset it *observed* (rather than restarting
1551 /// from `begin_offset` or losing position), with a short backoff and a
1552 /// bounded number of consecutive reconnects (see the client's
1553 /// [`RetryConfig`](canton_core::RetryConfig)).
1554 ///
1555 /// Observed includes the participant's `OffsetCheckpoint` frames, which are
1556 /// filtered out of what this yields but are the only thing that advances the
1557 /// resume point on a stream where nothing is happening. Once the reconnect
1558 /// budget is spent the stream yields **the failure that caused it** — the
1559 /// participant's status, details and retriable classification intact —
1560 /// rather than an error of the SDK's own.
1561 ///
1562 /// A resume point the participant has since pruned is not retried: the
1563 /// stream yields the refusal, and
1564 /// [`Error::pruned_earliest_offset`](canton_core::Error::pruned_earliest_offset)
1565 /// names where readable data begins. Moving forward to it loses the pruned
1566 /// history, so that is the caller's call, never the stream's.
1567 ///
1568 /// # Example
1569 /// ```no_run
1570 /// # async fn run(client: canton_ledger::CantonClient, party: String)
1571 /// # -> canton_ledger::Result<()> {
1572 /// use tokio_stream::StreamExt as _;
1573 ///
1574 /// let stream = client.updates_resumable(vec![party], 0);
1575 /// tokio::pin!(stream);
1576 /// while let Some(update) = stream.next().await {
1577 /// println!("update: {:?}", update?);
1578 /// }
1579 /// # Ok(()) }
1580 /// ```
1581 pub fn updates_resumable(
1582 &self,
1583 parties: Vec<String>,
1584 begin_offset: i64,
1585 ) -> impl Stream<Item = Result<pb::get_updates_response::Update>> + Send + use<> {
1586 self.updates_resumable_with(crate::request::UpdatesRequest::new(parties, begin_offset))
1587 }
1588
1589 /// Like [`Self::updates_resumable_with`], as the SDK's own
1590 /// [`Update`](crate::update::Update).
1591 pub fn updates_resumable_typed_with(
1592 &self,
1593 request: crate::request::UpdatesRequest,
1594 ) -> impl Stream<Item = Result<crate::update::Update>> + Send + use<> {
1595 self.updates_resumable_with(request)
1596 .map(|item| item.and_then(crate::update::Update::from_grpc))
1597 }
1598
1599 /// Like [`Self::updates_resumable`], with the full request surface of an
1600 /// [`UpdatesRequest`](crate::request::UpdatesRequest). A bounded request
1601 /// (`until`) makes this a *resilient catch-up read*: reconnects resume
1602 /// from the last observed offset, and the stream ends once the participant
1603 /// closes it at the end offset.
1604 pub fn updates_resumable_with(
1605 &self,
1606 request: crate::request::UpdatesRequest,
1607 ) -> impl Stream<Item = Result<pb::get_updates_response::Update>> + Send + use<> {
1608 let client = self.clone();
1609 let (max_reconnects, backoff_unit) = client.reconnect_policy();
1610 async_stream::stream! {
1611 if let Err(err) = request.validate() {
1612 yield Err(err);
1613 return;
1614 }
1615 // Resume tracking assumes ascending offsets; a descending request
1616 // would silently re-read on every reconnect, so refuse it.
1617 if request.is_descending() {
1618 yield Err(Error::InvalidRequest(
1619 "the resumable stream requires ascending order; use updates_with \
1620 (bounded) for descending reads".to_string(),
1621 ));
1622 return;
1623 }
1624 let mut offset = request.begin_exclusive;
1625 let mut reconnects = 0u32;
1626 loop {
1627 // What made this reconnect necessary. Carried out of the inner
1628 // loop so that giving up reports the participant's own failure
1629 // rather than a fresh error of ours: the status, its structured
1630 // details, the correlation id and the retriable classification
1631 // are what an application branches on, and the moment the
1632 // stream gives up is when it needs them most.
1633 let cause = match client
1634 .updates_including_checkpoints(request.resume_after(offset))
1635 .await
1636 {
1637 Ok(stream) => {
1638 tokio::pin!(stream);
1639 loop {
1640 match stream.next().await {
1641 Some(Ok(update)) => {
1642 // Checkpoints are included here: on a quiet
1643 // stream a checkpoint is the only thing that
1644 // moves the resume point, and after pruning
1645 // the older offset may not be servable at all.
1646 offset = update_offset(&update);
1647 reconnects = 0;
1648 if matches!(
1649 update,
1650 pb::get_updates_response::Update::OffsetCheckpoint(_)
1651 ) {
1652 continue;
1653 }
1654 yield Ok(update);
1655 }
1656 Some(Err(err)) if err.is_retriable() => break err,
1657 Some(Err(err)) => {
1658 yield Err(err);
1659 return;
1660 }
1661 None => return, // server closed the stream cleanly
1662 }
1663 }
1664 }
1665 Err(err) if err.is_retriable() => err,
1666 Err(err) => {
1667 yield Err(err);
1668 return;
1669 }
1670 };
1671
1672 reconnects += 1;
1673 if reconnects > max_reconnects {
1674 tracing::warn!(
1675 max_reconnects,
1676 offset,
1677 "update stream gave up resuming; reporting the failure that caused it"
1678 );
1679 yield Err(cause);
1680 return;
1681 }
1682 tokio::time::sleep(backoff_unit * reconnects).await;
1683 }
1684 }
1685 }
1686}
1687
1688/// How far a participant has pruned, as `latest_pruned_offsets` reports it on
1689/// either transport.
1690#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1691#[non_exhaustive]
1692pub struct PrunedOffsets {
1693 /// Updates and completions at or below this offset are gone; a read must
1694 /// start after it.
1695 pub participant_pruned_up_to_inclusive: i64,
1696 /// Divulged contracts are pruned up to this offset, which may lag the
1697 /// participant's own pruning point.
1698 pub all_divulged_contracts_pruned_up_to_inclusive: i64,
1699}
1700
1701/// A point read's "not there": `Ok(None)` rather than an error, because asking
1702/// whether an offset or an id holds an update the caller can see is a question
1703/// with two ordinary answers. Every other failure stays an error.
1704pub(crate) fn found<T>(result: Result<T>) -> Result<Option<T>> {
1705 match result {
1706 Ok(value) => Ok(Some(value)),
1707 Err(error) if is_update_not_found(&error) => Ok(None),
1708 Err(error) => Err(error),
1709 }
1710}
1711
1712fn is_update_not_found(error: &Error) -> bool {
1713 match error.error_info() {
1714 Some(info) => info.reason == "UPDATE_NOT_FOUND",
1715 None => error.code() == Some(tonic::Code::NotFound),
1716 }
1717}
1718
1719/// A point read's response carries its update in a oneof that a well-formed
1720/// response always sets.
1721fn present_update(response: pb::GetUpdateResponse) -> Result<pb::get_update_response::Update> {
1722 response.update.ok_or_else(|| {
1723 Error::UnexpectedResponse(
1724 "the participant returned an update response with no update".to_string(),
1725 )
1726 })
1727}
1728
1729#[cfg(test)]
1730mod tests {
1731 use super::CantonClient;
1732 use crate::Config;
1733 use canton_core::Error;
1734
1735 // Compile-time guarantee that the client is Send + Sync, so consumers can
1736 // share it across tasks and `tokio::spawn` work that holds it. (The streaming
1737 // methods carry `+ Send` in their signatures, which the compiler enforces at
1738 // this crate's build time.)
1739 #[test]
1740 fn client_is_send_and_sync() {
1741 fn assert_send_sync<T: Send + Sync>() {}
1742 assert_send_sync::<CantonClient>();
1743 }
1744
1745 #[tokio::test]
1746 async fn connect_lazy_accepts_a_valid_endpoint() {
1747 // connect_lazy builds a lazy Channel, which needs a Tokio runtime.
1748 assert!(CantonClient::connect_lazy(Config::new("http://localhost:3901")).is_ok());
1749 }
1750
1751 #[tokio::test]
1752 async fn connect_lazy_rejects_a_malformed_endpoint() {
1753 for bad in ["", "not a uri", "http://[bad"] {
1754 let result = CantonClient::connect_lazy(Config::new(bad));
1755 assert!(
1756 matches!(result, Err(Error::InvalidRequest(_))),
1757 "endpoint {bad:?} should be rejected as InvalidRequest, got {result:?}"
1758 );
1759 }
1760 }
1761
1762 // The paged read is inherently bounded, so a request without an end offset
1763 // must be refused before any RPC is attempted.
1764 // Every one of these is refused before a connection is even attempted: the
1765 // client is pointed at a port nothing listens on, so anything that reaches
1766 // the wire fails differently.
1767 #[tokio::test]
1768 async fn requests_the_participant_would_refuse_are_refused_here() {
1769 use crate::request::{ActiveContractsRequest, CompletionsRequest, UpdatesRequest};
1770
1771 let Ok(client) = CantonClient::connect_lazy(Config::new("http://localhost:1")) else {
1772 panic!("lazy connect accepts a valid endpoint");
1773 };
1774 let party = || vec!["alice".to_string()];
1775 let invalid = |result: &Result<_, Error>| matches!(result, Err(Error::InvalidRequest(_)));
1776
1777 // Negative offsets.
1778 assert!(invalid(
1779 &client
1780 .updates_with(UpdatesRequest::new(party(), -1))
1781 .await
1782 .map(|_| ())
1783 ));
1784 assert!(invalid(
1785 &client
1786 .acs_entries_with(ActiveContractsRequest::new(party(), -5))
1787 .await
1788 .map(|_| ())
1789 ));
1790 assert!(invalid(
1791 &client
1792 .completions_with(CompletionsRequest::new(party(), -1))
1793 .await
1794 .map(|_| ())
1795 ));
1796
1797 // A range that ends before it begins.
1798 assert!(invalid(
1799 &client
1800 .updates_with(UpdatesRequest::new(party(), 100).until(50))
1801 .await
1802 .map(|_| ())
1803 ));
1804
1805 // No parties: a subscription that looks alive and can never yield.
1806 assert!(invalid(
1807 &client
1808 .updates_with(UpdatesRequest::new(vec![], 0))
1809 .await
1810 .map(|_| ())
1811 ));
1812 }
1813
1814 #[tokio::test]
1815 async fn submissions_that_cannot_succeed_are_refused_here() {
1816 use crate::command::Submit;
1817
1818 let Ok(client) = CantonClient::connect_lazy(Config::new("http://localhost:1")) else {
1819 panic!("lazy connect accepts a valid endpoint");
1820 };
1821
1822 // No commands: a round trip to be told nothing was asked for.
1823 let empty = client.submission(Submit::new("alice"));
1824 assert!(matches!(
1825 empty.submit().await,
1826 Err(Error::InvalidRequest(_))
1827 ));
1828
1829 // The two minimum-ledger-time forms are mutually exclusive.
1830 let both = client.submission(
1831 Submit::new("alice")
1832 .add_command(crate::command::create(
1833 crate::command::identifier("pkg", "M", "T"),
1834 crate::command::record(vec![]),
1835 ))
1836 .with_min_ledger_time_rel(std::time::Duration::from_secs(1))
1837 .with_min_ledger_time_abs(prost_types::Timestamp::default()),
1838 );
1839 assert!(matches!(both.submit().await, Err(Error::InvalidRequest(_))));
1840 }
1841
1842 #[tokio::test]
1843 async fn updates_page_with_requires_a_bounded_request() {
1844 let Ok(client) = CantonClient::connect_lazy(Config::new("http://localhost:1")) else {
1845 panic!("lazy connect accepts a valid endpoint");
1846 };
1847 let unbounded = crate::request::UpdatesRequest::new(vec!["p".to_string()], 0);
1848 let result = client.updates_page_with(&unbounded, 10, None).await;
1849 assert!(
1850 matches!(result, Err(Error::InvalidRequest(_))),
1851 "expected InvalidRequest, got {result:?}"
1852 );
1853 }
1854
1855 // Descending order needs an end offset (streams), and never composes with
1856 // resume tracking — both must fail fast, before any RPC.
1857 #[tokio::test]
1858 async fn descending_requires_bounds_and_is_refused_on_resumable() {
1859 use tokio_stream::StreamExt as _;
1860
1861 let Ok(client) = CantonClient::connect_lazy(Config::new("http://localhost:1")) else {
1862 panic!("lazy connect accepts a valid endpoint");
1863 };
1864 let descending_unbounded =
1865 crate::request::UpdatesRequest::new(vec!["p".to_string()], 0).descending();
1866
1867 let result = client.updates_with(descending_unbounded.clone()).await;
1868 assert!(matches!(result.err(), Some(Error::InvalidRequest(_))));
1869
1870 let stream = client.updates_resumable_with(descending_unbounded.until(10));
1871 tokio::pin!(stream);
1872 let Some(first) = stream.next().await else {
1873 panic!("the resumable stream should yield the rejection");
1874 };
1875 assert!(matches!(first, Err(Error::InvalidRequest(_))));
1876 }
1877
1878 // A reassignment carries its `Unassigned` (source) and `Assigned` (target)
1879 // events as distinct cases rather than collapsing them into one "reassign"
1880 // event — the multi-synchronizer faithfulness the update stream promises.
1881 // (LocalNet is single-synchronizer, so this is verified structurally.)
1882 #[test]
1883 fn reassignment_preserves_the_unassigned_assigned_split() {
1884 use super::pb;
1885 use pb::get_updates_response::Update;
1886 use pb::reassignment_event::Event;
1887
1888 let update = Update::Reassignment(pb::Reassignment {
1889 update_id: "u1".to_string(),
1890 events: vec![
1891 pb::ReassignmentEvent {
1892 event: Some(Event::Unassigned(pb::UnassignedEvent::default())),
1893 },
1894 pb::ReassignmentEvent {
1895 event: Some(Event::Assigned(pb::AssignedEvent::default())),
1896 },
1897 ],
1898 ..Default::default()
1899 });
1900
1901 let Update::Reassignment(reassignment) = update else {
1902 panic!("expected a reassignment update");
1903 };
1904 assert_eq!(reassignment.events.len(), 2, "both legs are surfaced");
1905 assert!(matches!(
1906 reassignment.events[0].event,
1907 Some(Event::Unassigned(_))
1908 ));
1909 assert!(matches!(
1910 reassignment.events[1].event,
1911 Some(Event::Assigned(_))
1912 ));
1913 }
1914}