sui_rpc/client/mod.rs
1use std::sync::Arc;
2use std::time::Duration;
3use tap::Pipe;
4use tonic::body::Body;
5use tonic::codec::CompressionEncoding;
6use tonic::transport::channel::ClientTlsConfig;
7use tower::Layer;
8use tower::Service;
9use tower::ServiceBuilder;
10use tower::util::BoxLayer;
11use tower::util::BoxService;
12
13mod response_ext;
14pub use response_ext::ResponseExt;
15
16mod interceptors;
17pub use interceptors::HeadersInterceptor;
18
19mod watchdog;
20pub use watchdog::BodyIdleTimeout;
21use watchdog::DEFAULT_BODY_IDLE_TIMEOUT;
22use watchdog::WatchdogLayer;
23
24mod unsent_request_retry;
25use unsent_request_retry::UnsentRequestRetry;
26
27mod staking_rewards;
28pub use staking_rewards::DelegatedStake;
29
30mod coin_selection;
31mod ledger_streams;
32mod lists;
33pub use ledger_streams::CheckpointStreamFrame;
34pub use ledger_streams::CheckpointStreamRequest;
35pub use ledger_streams::CheckpointStreamStart;
36pub use ledger_streams::Delivery;
37pub use ledger_streams::EventStreamFrame;
38pub use ledger_streams::EventStreamRequest;
39pub use ledger_streams::EventStreamStart;
40pub use ledger_streams::LedgerStreamConfig;
41pub use ledger_streams::LedgerStreamEvent;
42pub use ledger_streams::LedgerStreamFamily;
43pub use ledger_streams::LedgerStreamOperation;
44pub use ledger_streams::LedgerStreamStage;
45pub use ledger_streams::ListConfig;
46pub use ledger_streams::ListEvent;
47pub use ledger_streams::TransactionStreamFrame;
48pub use ledger_streams::TransactionStreamRequest;
49pub use ledger_streams::TransactionStreamStart;
50
51mod transaction_execution;
52pub use transaction_execution::ExecuteAndWaitError;
53
54use crate::proto::sui::rpc::v2::ledger_service_client::LedgerServiceClient;
55use crate::proto::sui::rpc::v2::move_package_service_client::MovePackageServiceClient;
56use crate::proto::sui::rpc::v2::signature_verification_service_client::SignatureVerificationServiceClient;
57use crate::proto::sui::rpc::v2::state_service_client::StateServiceClient;
58use crate::proto::sui::rpc::v2::subscription_service_client::SubscriptionServiceClient;
59use crate::proto::sui::rpc::v2::transaction_execution_service_client::TransactionExecutionServiceClient;
60#[cfg(feature = "unstable")]
61use crate::proto::sui::rpc::v2alpha::proof_service_client::ProofServiceClient;
62
63type Result<T, E = tonic::Status> = std::result::Result<T, E>;
64type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
65type BoxedChannel = BoxService<http::Request<Body>, http::Response<Body>, tonic::Status>;
66
67type RequestLayer = BoxLayer<
68 BoxService<http::Request<Body>, http::Response<Body>, BoxError>,
69 http::Request<Body>,
70 http::Response<Body>,
71 BoxError,
72>;
73
74const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
75const DEFAULT_TCP_KEEPALIVE_IDLE: Duration = Duration::from_secs(15);
76const DEFAULT_TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(5);
77const DEFAULT_TCP_KEEPALIVE_RETRIES: u32 = 3;
78const DEFAULT_HTTP2_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(5);
79const DEFAULT_HTTP2_KEEP_ALIVE_TIMEOUT: Duration = Duration::from_secs(20);
80const DEFAULT_NUM_CONNECTIONS: usize = 1;
81
82// RPCs made through a `Client` (and all of its clones) are multiplexed over
83// its HTTP/2 connections, so each connection-level receive window is shared
84// by every in-flight response on that connection. A streaming response that the
85// application holds without polling pins up to a full stream window of that
86// shared budget; once the connection window is exhausted, every RPC on the
87// channel hangs indefinitely while TCP and HTTP/2 keepalives stay healthy.
88// hyper's client defaults (2 MiB stream / 5 MiB connection) let ~3 stalled
89// streams starve the connection. Keep the stream window at hyper's default
90// but raise the connection window so ~32 concurrently stalled streams are
91// needed instead.
92const DEFAULT_HTTP2_STREAM_WINDOW_SIZE: u32 = 2 * 1024 * 1024;
93const DEFAULT_HTTP2_CONNECTION_WINDOW_SIZE: u32 = 64 * 1024 * 1024;
94
95/// A gRPC client for the Sui fullnode RPC interface.
96///
97/// RPCs made through a client and its clones are multiplexed over a single
98/// HTTP/2 connection by default; see
99/// [`with_num_connections`](Client::with_num_connections) to spread them over
100/// several.
101///
102/// # Ledger streams
103///
104/// `Client` provides two styles of stream operations for checkpoints, transactions, and events:
105///
106/// - `list_*` ([`list_checkpoints`](Client::list_checkpoints), [`list_transactions`](Client::list_transactions), [`list_events`](Client::list_events)):
107/// Finite pagination streams yielding raw response pages until reaching an end bound or the ledger tip.
108/// - `stream_*` ([`stream_checkpoints`](Client::stream_checkpoints), [`stream_transactions`](Client::stream_transactions), [`stream_events`](Client::stream_events)):
109/// Resumable, infinite streams starting from any position (`Tip`, `Checkpoint`, or `Resume` cursor)
110/// that automatically handle backfill replay, live subscription or polling, and transient error retries.
111///
112/// Stream read masks must include `sequence_number` for checkpoints; `checkpoint` and
113/// `transaction_index` for transactions; and `checkpoint`, `transaction_index`, and `event_index`
114/// for events (`"*"` satisfies all three).
115///
116/// Process a payload before persisting its restart position. To resume after a checkpoint, pass
117/// `cursor + 1` to [`CheckpointStreamStart::Checkpoint`]. To resume transactions or events, pass
118/// `frame.cursor` to [`TransactionStreamStart::Resume`] or [`EventStreamStart::Resume`].
119///
120/// ## Example
121///
122/// ```no_run
123/// # use futures::StreamExt;
124/// # use sui_rpc::field::{FieldMask, FieldMaskUtil};
125/// # use sui_rpc::proto::sui::rpc::v2::ExecutedTransaction;
126/// # use sui_rpc::Client;
127/// # use sui_rpc::client::{TransactionStreamRequest, TransactionStreamStart};
128/// #
129/// # async fn process_transaction(_: &ExecutedTransaction) {}
130/// # async fn persist_position(_: &[u8]) {}
131/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
132/// let client = Client::new(Client::MAINNET_FULLNODE)?;
133/// let read_mask = FieldMask::from_paths(["checkpoint", "transaction_index", "digest"]);
134/// let request = TransactionStreamRequest::new().with_read_mask(read_mask.clone());
135///
136/// let mut stream = Box::pin(client.stream_transactions(request));
137/// let persisted = loop {
138/// let Some(frame) = stream.next().await else {
139/// return Ok(());
140/// };
141/// let frame = frame?;
142///
143/// if let Some(transaction) = &frame.transaction {
144/// process_transaction(transaction).await;
145/// }
146/// let persisted = frame.cursor.clone();
147/// persist_position(&persisted).await;
148/// break persisted;
149/// };
150/// drop(stream);
151///
152/// let restart_request = TransactionStreamRequest::new()
153/// .with_read_mask(read_mask)
154/// .with_start(TransactionStreamStart::Resume(persisted));
155/// let mut resumed = Box::pin(client.stream_transactions(restart_request));
156/// let _ = resumed.next().await;
157/// # Ok(())
158/// # }
159/// ```
160///
161/// # Timeouts and deadlines
162///
163/// No default bounds the total duration of a call. Two opt-in bounds are
164/// available:
165///
166/// - A per-call deadline set with [`tonic::Request::set_timeout`]. This
167/// attaches the standard `grpc-timeout` header, so a server that supports
168/// it enforces the deadline too, and the client enforces it locally end to
169/// end: tonic bounds the wait for response headers, and the client's
170/// watchdog (see [`with_body_idle_timeout`](Client::with_body_idle_timeout))
171/// bounds the response body against the same deadline.
172/// The ledger-stream `list_*` and `stream_*` methods do not accept
173/// `tonic::Request`. One logical operation may issue several RPCs, so per-RPC
174/// authentication, common headers, tracing, and timeouts belong on the
175/// client: static headers on [`Client::with_headers`], dynamic per-RPC auth
176/// or tracing on [`Client::request_layer`]. Configuring a [`Clone`]d client
177/// affects only that clone and shares the underlying connection, so a single
178/// stream gets dedicated headers by receiving a configured clone.
179/// - A client-wide response-headers timeout set with
180/// [`with_response_headers_timeout`](Client::with_response_headers_timeout).
181/// This is enforced locally only and its timer stops once response headers
182/// arrive, so it bounds every unary call (whose headers are not sent until
183/// the handler completes) without cutting off long-lived streams.
184///
185/// Independent of any deadline, the watchdog resets RPCs whose response body
186/// makes no progress for 30 seconds (configurable), so a call on a stalled
187/// connection fails with `DeadlineExceeded` instead of hanging forever.
188///
189/// # Undelivered requests
190///
191/// Requests the server never processed are resent automatically, following
192/// gRPC's transparent retry (gRFC A6) as grpc-go and grpc-java implement it:
193/// a request cancelled before it left the client is retried up to three
194/// times, and a request the server rejected unprocessed -- a stream above the
195/// last stream id of a NO_ERROR GOAWAY, as sent when a server or load
196/// balancer drains a connection, or a stream reset with REFUSED_STREAM -- is
197/// retried once. A request the server may have processed is never resent.
198/// A per-call deadline ([`tonic::Request::set_timeout`]) covers all attempts
199/// together; the response-headers timeout applies to each attempt.
200#[derive(Clone)]
201pub struct Client {
202 channel: tonic::transport::Channel,
203
204 // Everything other than the channel is only consulted when building a
205 // per-service client or reconfiguring, so it lives behind an `Arc` to
206 // keep `Client` itself small; it is cloned by value into futures
207 // throughout the SDK. The `Endpoint` alone is over 500 bytes.
208 config: Arc<ClientConfig>,
209}
210
211#[derive(Clone)]
212struct ClientConfig {
213 uri: http::Uri,
214 endpoint: tonic::transport::Endpoint,
215 headers: HeadersInterceptor,
216 max_decoding_message_size: Option<usize>,
217 body_idle_timeout: Option<Duration>,
218 num_connections: usize,
219
220 /// Layer to apply to all RPC requests
221 request_layer: Option<RequestLayer>,
222}
223
224/// Open `num_connections` lazy connections to `endpoint`, all sharing its
225/// configuration, and balance requests across them.
226///
227/// Each connection is inserted under its own key, because the balancer
228/// identifies backends by key and the endpoint is the same for all of them.
229/// The change sender is dropped once the set is populated: the set is fixed,
230/// and the balancer retains it after discovery ends.
231fn build_channel(
232 endpoint: &tonic::transport::Endpoint,
233 num_connections: usize,
234) -> tonic::transport::Channel {
235 if num_connections <= 1 {
236 return endpoint.connect_lazy();
237 }
238
239 let (channel, changes) = tonic::transport::Channel::balance_channel::<usize>(num_connections);
240 for key in 0..num_connections {
241 changes
242 .try_send(tonic::transport::channel::Change::Insert(
243 key,
244 endpoint.clone(),
245 ))
246 .expect("change channel has capacity for every connection");
247 }
248
249 channel
250}
251
252impl Client {
253 /// URL for the public-good, Sui Foundation provided fullnodes for mainnet.
254 pub const MAINNET_FULLNODE: &str = "https://fullnode.mainnet.sui.io";
255
256 /// URL for the public-good, Sui Foundation provided fullnodes for testnet.
257 pub const TESTNET_FULLNODE: &str = "https://fullnode.testnet.sui.io";
258
259 /// URL for the public-good, Sui Foundation provided fullnodes for devnet.
260 pub const DEVNET_FULLNODE: &str = "https://fullnode.devnet.sui.io";
261
262 /// URL for the public-good, Sui Foundation provided archive for mainnet.
263 pub const MAINNET_ARCHIVE: &str = "https://archive.mainnet.sui.io";
264
265 /// URL for the public-good, Sui Foundation provided archive for testnet.
266 pub const TESTNET_ARCHIVE: &str = "https://archive.testnet.sui.io";
267
268 /// Build a client from a fully custom [`tonic::transport::Endpoint`].
269 ///
270 /// This bypasses every transport default that [`Client::new`] applies,
271 /// including the HTTP/2 flow-control windows that protect a connection
272 /// from starvation by stalled streaming responses. Prefer
273 /// [`Client::new`] plus the `with_*` configuration methods unless an
274 /// endpoint setting is needed that the client does not expose. The
275 /// idle-body watchdog (see [`Client::with_body_idle_timeout`]) is part of
276 /// the client rather than the endpoint and stays enabled.
277 ///
278 /// In particular, do not rely on
279 /// [`http2_adaptive_window`](tonic::transport::Endpoint::http2_adaptive_window)
280 /// as a substitute for large static windows: with adaptive windowing,
281 /// hyper starts the connection window at the 64 KiB HTTP/2 spec default
282 /// until bandwidth-delay probing ramps up, so a single stalled stream can
283 /// starve the whole connection.
284 pub fn from_endpoint(endpoint: &tonic::transport::Endpoint) -> Self {
285 let uri = endpoint.uri().clone();
286 let channel = build_channel(endpoint, DEFAULT_NUM_CONNECTIONS);
287 Self {
288 channel,
289 config: Arc::new(ClientConfig {
290 uri,
291 endpoint: endpoint.clone(),
292 headers: Default::default(),
293 max_decoding_message_size: None,
294 body_idle_timeout: Some(DEFAULT_BODY_IDLE_TIMEOUT),
295 num_connections: DEFAULT_NUM_CONNECTIONS,
296 request_layer: None,
297 }),
298 }
299 }
300
301 #[allow(clippy::result_large_err)]
302 pub fn new<T>(uri: T) -> Result<Self>
303 where
304 T: TryInto<http::Uri>,
305 T::Error: Into<BoxError>,
306 {
307 let uri = uri
308 .try_into()
309 .map_err(Into::into)
310 .map_err(status_from_error)?;
311 let mut endpoint = tonic::transport::Endpoint::from(uri.clone());
312 if uri.scheme() == Some(&http::uri::Scheme::HTTPS) {
313 endpoint = endpoint
314 .tls_config(ClientTlsConfig::new().with_enabled_roots())
315 .map_err(Into::into)
316 .map_err(status_from_error)?;
317 }
318
319 let endpoint = endpoint
320 .connect_timeout(DEFAULT_CONNECT_TIMEOUT)
321 .tcp_keepalive(Some(DEFAULT_TCP_KEEPALIVE_IDLE))
322 .tcp_keepalive_interval(Some(DEFAULT_TCP_KEEPALIVE_INTERVAL))
323 .tcp_keepalive_retries(Some(DEFAULT_TCP_KEEPALIVE_RETRIES))
324 .http2_keep_alive_interval(DEFAULT_HTTP2_KEEP_ALIVE_INTERVAL)
325 .keep_alive_timeout(DEFAULT_HTTP2_KEEP_ALIVE_TIMEOUT)
326 .initial_stream_window_size(DEFAULT_HTTP2_STREAM_WINDOW_SIZE)
327 .initial_connection_window_size(DEFAULT_HTTP2_CONNECTION_WINDOW_SIZE);
328 let channel = build_channel(&endpoint, DEFAULT_NUM_CONNECTIONS);
329
330 Ok(Self {
331 channel,
332 config: Arc::new(ClientConfig {
333 uri,
334 endpoint,
335 headers: Default::default(),
336 max_decoding_message_size: None,
337 body_idle_timeout: Some(DEFAULT_BODY_IDLE_TIMEOUT),
338 num_connections: DEFAULT_NUM_CONNECTIONS,
339 request_layer: None,
340 }),
341 })
342 }
343
344 /// Set the idle timeout for the client's response-body watchdog.
345 /// Defaults to 30 seconds.
346 ///
347 /// The watchdog bounds the time between response-body progress events: if
348 /// a whole idle period passes without a frame of the response being
349 /// delivered to the caller -- because the connection is starved or dead,
350 /// or because the caller has parked a streaming response without polling
351 /// it -- the watchdog resets the stream, releasing the HTTP/2
352 /// flow-control window it had pinned, and the call observes a
353 /// [`DeadlineExceeded`](tonic::Code::DeadlineExceeded) status on its next
354 /// poll. This is what turns "an RPC on a starved connection hangs
355 /// forever" into a bounded failure, and what keeps an abandoned stream
356 /// from starving the shared connection in the first place.
357 ///
358 /// Streams that are legitimately quiet for longer than the timeout (the
359 /// fullnode's checkpoint subscription is not: it emits watermarks every
360 /// few seconds) should raise or disable the watchdog for that call with a
361 /// [`BodyIdleTimeout`] request extension.
362 pub fn with_body_idle_timeout(mut self, timeout: Duration) -> Self {
363 Arc::make_mut(&mut self.config).body_idle_timeout = Some(timeout);
364 self
365 }
366
367 /// Disable the client's response-body watchdog (see
368 /// [`with_body_idle_timeout`](Self::with_body_idle_timeout)).
369 ///
370 /// Without it, an RPC whose response can no longer make progress hangs
371 /// indefinitely; only disable the watchdog when every call is bounded by
372 /// the caller. It can be re-enabled for individual requests with a
373 /// [`BodyIdleTimeout`] request extension.
374 pub fn without_body_idle_timeout(mut self) -> Self {
375 Arc::make_mut(&mut self.config).body_idle_timeout = None;
376 self
377 }
378
379 /// Set a timeout for the response-headers phase of every RPC made
380 /// through this client. Disabled by default.
381 ///
382 /// The timer covers a request from dispatch on the connection until
383 /// response headers arrive and is dropped once they do, so a client-wide
384 /// value does not cut off long-lived streaming responses. Because a
385 /// server does not send response headers for a unary call until the
386 /// handler completes, this effectively bounds the total duration of
387 /// unary calls; the body that follows is bounded by the idle-body
388 /// watchdog (see [`with_body_idle_timeout`](Self::with_body_idle_timeout))
389 /// and, when set, the per-call deadline. Connection establishment is
390 /// bounded separately by the connect timeout.
391 ///
392 /// This timeout is enforced locally only; it is not communicated to the
393 /// server. When a per-call deadline ([`tonic::Request::set_timeout`]) is
394 /// also set, the shorter of the two bounds the headers phase locally, so
395 /// a per-call deadline can tighten this bound but never extend it --
396 /// size the timeout for the slowest expected RPC. Expiry surfaces as
397 /// [`DeadlineExceeded`](tonic::Code::DeadlineExceeded).
398 ///
399 /// This rebuilds the underlying channel, so it must be called before the
400 /// client is used or cloned; earlier clones keep the previous
401 /// configuration.
402 pub fn with_response_headers_timeout(mut self, timeout: Duration) -> Self {
403 let config = Arc::make_mut(&mut self.config);
404 config.endpoint = config.endpoint.clone().timeout(timeout);
405 self.channel = build_channel(&config.endpoint, config.num_connections);
406 self
407 }
408
409 /// Set how many HTTP/2 connections the client opens to the endpoint.
410 /// Defaults to 1; a count of 0 is treated as 1.
411 ///
412 /// Requests are distributed across the connections rather than sharing
413 /// one connection's flow-control window and driver task. A single
414 /// connection's throughput is bounded by that window and by the one task
415 /// driving its multiplexed streams, so workloads that keep many streams
416 /// in flight at once (bulk or streaming reads) can be limited by it well
417 /// before the server is. Every connection carries the same endpoint
418 /// configuration, so the window sizes described in
419 /// [`with_initial_connection_window_size`](Self::with_initial_connection_window_size)
420 /// apply to each one, and the client's total window budget scales with
421 /// the count.
422 ///
423 /// Placement is effectively random per request, not a strict rotation, so
424 /// connections carry equal load only on average. Requests held open
425 /// concurrently, such as long-lived streams, can land unevenly.
426 ///
427 /// Connections are established lazily and reconnect independently, and
428 /// one that cannot be established fails only the requests routed to it.
429 ///
430 /// This rebuilds the underlying channel, so it must be called before the
431 /// client is used or cloned; earlier clones keep the previous
432 /// configuration.
433 pub fn with_num_connections(mut self, num_connections: usize) -> Self {
434 let config = Arc::make_mut(&mut self.config);
435 config.num_connections = num_connections.max(1);
436 self.channel = build_channel(&config.endpoint, config.num_connections);
437 self
438 }
439
440 /// Set the HTTP/2 per-stream receive window, in bytes.
441 ///
442 /// This bounds how much unread response data a single RPC can buffer
443 /// before the server must stop sending on that stream. It also bounds how
444 /// much of the shared connection window (see
445 /// [`with_initial_connection_window_size`](Self::with_initial_connection_window_size))
446 /// one stalled stream can pin. Defaults to 2 MiB.
447 ///
448 /// This rebuilds the underlying channel, so it must be called before the
449 /// client is used or cloned; earlier clones keep the previous
450 /// configuration.
451 pub fn with_initial_stream_window_size(mut self, size: u32) -> Self {
452 let config = Arc::make_mut(&mut self.config);
453 config.endpoint = config.endpoint.clone().initial_stream_window_size(size);
454 self.channel = build_channel(&config.endpoint, config.num_connections);
455 self
456 }
457
458 /// Set the HTTP/2 connection-level receive window, in bytes.
459 ///
460 /// This window is shared by every RPC multiplexed over one of the
461 /// client's HTTP/2 connections, including those issued by clones of the
462 /// client. Response data that the application has not yet read counts
463 /// against it, so it determines how many concurrently stalled streaming
464 /// responses it takes to starve a connection and hang every other RPC on
465 /// it. Defaults to 64 MiB (~32 stalled streams at the default 2 MiB
466 /// stream window). The window applies per connection, so a client
467 /// configured with
468 /// [`with_num_connections`](Self::with_num_connections) has this much on
469 /// each.
470 ///
471 /// This rebuilds the underlying channel, so it must be called before the
472 /// client is used or cloned; earlier clones keep the previous
473 /// configuration.
474 pub fn with_initial_connection_window_size(mut self, size: u32) -> Self {
475 let config = Arc::make_mut(&mut self.config);
476 config.endpoint = config.endpoint.clone().initial_connection_window_size(size);
477 self.channel = build_channel(&config.endpoint, config.num_connections);
478 self
479 }
480
481 pub fn with_headers(mut self, headers: HeadersInterceptor) -> Self {
482 Arc::make_mut(&mut self.config).headers = headers;
483 self
484 }
485
486 /// Provide an optional [`Layer`] that will be used to wrap all RPC
487 /// requests.
488 ///
489 /// This could be helpful in providing global metrics and logging
490 /// for all outbound requests.
491 ///
492 /// The layer's service may return any response body that implements
493 /// [`http_body::Body<Data = bytes::Bytes>`] and any error type that
494 /// implements `Into<Box<dyn Error + Send + Sync>>`. Both are mapped
495 /// to the internal types automatically.
496 ///
497 /// # Example
498 ///
499 /// Add a layer that logs each request URI:
500 ///
501 /// ```
502 /// # let _rt = tokio::runtime::Builder::new_current_thread()
503 /// # .build()
504 /// # .unwrap();
505 /// # let _guard = _rt.enter();
506 /// use sui_rpc::Client;
507 /// use tower::ServiceBuilder;
508 ///
509 /// let client = Client::new(Client::MAINNET_FULLNODE)
510 /// .unwrap()
511 /// .request_layer(ServiceBuilder::new().map_request(|req: http::Request<_>| {
512 /// println!("request to {}", req.uri());
513 /// req
514 /// }));
515 /// ```
516 pub fn request_layer<L, ResBody, E>(mut self, layer: L) -> Self
517 where
518 L: Layer<BoxService<http::Request<Body>, http::Response<Body>, BoxError>>
519 + Send
520 + Sync
521 + 'static,
522 L::Service: Service<http::Request<Body>, Response = http::Response<ResBody>, Error = E>
523 + Send
524 + 'static,
525 <L::Service as Service<http::Request<Body>>>::Future: Send + 'static,
526 ResBody: http_body::Body<Data = bytes::Bytes> + Send + 'static,
527 ResBody::Error: Into<BoxError>,
528 E: Into<BoxError> + Send + 'static,
529 {
530 let layer = BoxLayer::new(
531 ServiceBuilder::new()
532 .map_response(|resp: http::Response<ResBody>| resp.map(Body::new))
533 .map_err(Into::<BoxError>::into)
534 .layer(layer),
535 );
536 Arc::make_mut(&mut self.config).request_layer = Some(layer);
537 self
538 }
539
540 pub fn with_max_decoding_message_size(mut self, limit: usize) -> Self {
541 Arc::make_mut(&mut self.config).max_decoding_message_size = Some(limit);
542 self
543 }
544
545 pub fn uri(&self) -> &http::Uri {
546 &self.config.uri
547 }
548
549 fn channel(&self) -> BoxedChannel {
550 let headers = self.config.headers.clone();
551
552 // Build the base service with headers applied at the HTTP level and the
553 // transport error mapped to BoxError for compatibility with user layers.
554 let base = BoxService::new(
555 ServiceBuilder::new()
556 .map_request(move |mut req: http::Request<Body>| {
557 // Inserted before the configured headers so a caller can override it.
558 req.headers_mut().insert(
559 crate::headers::X_SUI_CLIENT_PROTOCOL_VERSION,
560 http::HeaderValue::from(crate::headers::MAX_PROTOCOL_VERSION),
561 );
562 if !headers.headers().is_empty() {
563 req.headers_mut()
564 .extend(headers.headers().clone().into_headers());
565 }
566 req
567 })
568 .service(UnsentRequestRetry::new(self.channel.clone())),
569 );
570
571 // Guard every response body with the idle-body watchdog, beneath any
572 // user layers so their view of the response goes through the
573 // watchdog's bridge.
574 let base = BoxService::new(WatchdogLayer::new(self.config.body_idle_timeout).layer(base));
575
576 // Apply the user's outbound request layer if present.
577 let layered = if let Some(layer) = &self.config.request_layer {
578 layer.layer(base)
579 } else {
580 base
581 };
582
583 // Map the final error to tonic::Status (a concrete type) so that
584 // downstream users of the tonic-generated clients don't run into
585 // lifetime-inference issues with async_trait and Box<dyn Error>.
586 BoxService::new(
587 ServiceBuilder::new()
588 .map_err(status_from_error)
589 .service(layered),
590 )
591 }
592
593 pub fn ledger_client(&mut self) -> LedgerServiceClient<BoxedChannel> {
594 LedgerServiceClient::new(self.channel())
595 .accept_compressed(CompressionEncoding::Zstd)
596 .pipe(|client| {
597 if let Some(limit) = self.config.max_decoding_message_size {
598 client.max_decoding_message_size(limit)
599 } else {
600 client
601 }
602 })
603 }
604
605 pub fn state_client(&mut self) -> StateServiceClient<BoxedChannel> {
606 StateServiceClient::new(self.channel())
607 .accept_compressed(CompressionEncoding::Zstd)
608 .pipe(|client| {
609 if let Some(limit) = self.config.max_decoding_message_size {
610 client.max_decoding_message_size(limit)
611 } else {
612 client
613 }
614 })
615 }
616
617 pub fn execution_client(&mut self) -> TransactionExecutionServiceClient<BoxedChannel> {
618 TransactionExecutionServiceClient::new(self.channel())
619 .accept_compressed(CompressionEncoding::Zstd)
620 .pipe(|client| {
621 if let Some(limit) = self.config.max_decoding_message_size {
622 client.max_decoding_message_size(limit)
623 } else {
624 client
625 }
626 })
627 }
628
629 pub fn package_client(&mut self) -> MovePackageServiceClient<BoxedChannel> {
630 MovePackageServiceClient::new(self.channel())
631 .accept_compressed(CompressionEncoding::Zstd)
632 .pipe(|client| {
633 if let Some(limit) = self.config.max_decoding_message_size {
634 client.max_decoding_message_size(limit)
635 } else {
636 client
637 }
638 })
639 }
640
641 pub fn signature_verification_client(
642 &mut self,
643 ) -> SignatureVerificationServiceClient<BoxedChannel> {
644 SignatureVerificationServiceClient::new(self.channel())
645 .accept_compressed(CompressionEncoding::Zstd)
646 .pipe(|client| {
647 if let Some(limit) = self.config.max_decoding_message_size {
648 client.max_decoding_message_size(limit)
649 } else {
650 client
651 }
652 })
653 }
654
655 pub fn subscription_client(&mut self) -> SubscriptionServiceClient<BoxedChannel> {
656 SubscriptionServiceClient::new(self.channel())
657 .accept_compressed(CompressionEncoding::Zstd)
658 .pipe(|client| {
659 if let Some(limit) = self.config.max_decoding_message_size {
660 client.max_decoding_message_size(limit)
661 } else {
662 client
663 }
664 })
665 }
666
667 /// Returns a client for the unstable alpha `ProofService`, which serves
668 /// Object Checkpoint State (OCS) inclusion proofs.
669 #[cfg(feature = "unstable")]
670 #[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
671 pub fn proof_client(&mut self) -> ProofServiceClient<BoxedChannel> {
672 ProofServiceClient::new(self.channel())
673 .accept_compressed(CompressionEncoding::Zstd)
674 .pipe(|client| {
675 if let Some(limit) = self.config.max_decoding_message_size {
676 client.max_decoding_message_size(limit)
677 } else {
678 client
679 }
680 })
681 }
682}
683
684/// Map a transport error to a [`tonic::Status`].
685///
686/// tonic surfaces an expired headers-phase timeout (the client's
687/// response-headers timeout, or a `grpc-timeout` deadline expiring before
688/// response headers arrive) as `Cancelled`. The gRPC code for an expired
689/// deadline is `DeadlineExceeded`, and the watchdog already uses it for
690/// body-phase expiry, so normalize before delegating to tonic's own mapping.
691fn status_from_error(error: BoxError) -> tonic::Status {
692 let mut source: Option<&(dyn std::error::Error + 'static)> = Some(error.as_ref());
693 while let Some(err) = source {
694 // An embedded `Status` takes precedence, as in tonic's own mapping.
695 if err.is::<tonic::Status>() {
696 break;
697 }
698 if err.is::<tonic::TimeoutExpired>() {
699 return tonic::Status::deadline_exceeded(
700 "timeout expired before response headers were received",
701 );
702 }
703 source = err.source();
704 }
705 tonic::Status::from_error(error)
706}