Skip to main content

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}