iota_grpc_client/
client.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::time::Duration;
6
7use iota_grpc_types::v1::{
8    ledger_service::ledger_service_client::LedgerServiceClient,
9    move_package_service::move_package_service_client::MovePackageServiceClient,
10    state_service::state_service_client::StateServiceClient,
11    transaction_execution_service::transaction_execution_service_client::TransactionExecutionServiceClient,
12};
13use tonic::codec::CompressionEncoding;
14
15use crate::{api::Result, interceptors::HeadersInterceptor};
16
17type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
18
19pub type InterceptedChannel =
20    tonic::service::interceptor::InterceptedService<tonic::transport::Channel, HeadersInterceptor>;
21
22/// gRPC client factory for IOTA gRPC operations.
23#[derive(Clone)]
24pub struct Client {
25    /// Target URI of the gRPC server
26    uri: http::Uri,
27    /// Shared gRPC channel for all service clients
28    channel: tonic::transport::Channel,
29    /// Headers interceptor for adding custom headers to requests
30    headers: HeadersInterceptor,
31    /// Maximum decoding message size for responses
32    max_decoding_message_size: Option<usize>,
33}
34
35impl Client {
36    /// Connect to a gRPC server and create a new Client instance.
37    #[allow(clippy::result_large_err)]
38    pub async fn connect<T>(uri: T) -> Result<Self>
39    where
40        T: TryInto<http::Uri>,
41        T::Error: Into<BoxError>,
42    {
43        let uri = uri
44            .try_into()
45            .map_err(Into::into)
46            .map_err(tonic::Status::from_error)?;
47
48        let mut endpoint = tonic::transport::Endpoint::from(uri.clone());
49        if uri.scheme() == Some(&http::uri::Scheme::HTTPS) {
50            #[cfg(not(feature = "tls-ring"))]
51            return Err(tonic::Status::failed_precondition(
52                "the `tls-ring` feature must be enabled for HTTPS",
53            )
54            .into());
55
56            #[cfg(not(any(feature = "tls-native-roots", feature = "tls-webpki-roots")))]
57            return Err(tonic::Status::failed_precondition(
58                "the `tls-native-roots` or `tls-webpki-roots` feature must be enabled for HTTPS",
59            )
60            .into());
61
62            #[cfg(all(
63                feature = "tls-ring",
64                any(feature = "tls-native-roots", feature = "tls-webpki-roots")
65            ))]
66            {
67                endpoint = endpoint
68                    .tls_config(
69                        tonic::transport::channel::ClientTlsConfig::new().with_enabled_roots(),
70                    )
71                    .map_err(Into::into)
72                    .map_err(tonic::Status::from_error)?;
73            }
74        }
75
76        let channel = endpoint
77            .connect_timeout(Duration::from_secs(5))
78            .http2_keep_alive_interval(Duration::from_secs(5))
79            .connect_lazy();
80
81        Ok(Self {
82            uri,
83            channel,
84            headers: Default::default(),
85            max_decoding_message_size: None,
86        })
87    }
88
89    pub fn uri(&self) -> &http::Uri {
90        &self.uri
91    }
92
93    /// Get a reference to the underlying channel.
94    ///
95    /// This can be useful for creating additional service clients that aren't
96    /// yet integrated into Client.
97    pub fn channel(&self) -> &tonic::transport::Channel {
98        &self.channel
99    }
100
101    pub fn headers(&self) -> &HeadersInterceptor {
102        &self.headers
103    }
104
105    pub fn max_decoding_message_size(&self) -> Option<usize> {
106        self.max_decoding_message_size
107    }
108
109    pub fn with_headers(mut self, headers: HeadersInterceptor) -> Self {
110        self.headers = headers;
111        self
112    }
113
114    pub fn with_max_decoding_message_size(mut self, limit: usize) -> Self {
115        self.max_decoding_message_size = Some(limit);
116        self
117    }
118
119    /// Get a ledger service client.
120    pub fn ledger_service_client(&self) -> LedgerServiceClient<InterceptedChannel> {
121        self.configure_client(LedgerServiceClient::with_interceptor(
122            self.channel.clone(),
123            self.headers.clone(),
124        ))
125    }
126
127    /// Get a transaction execution service client.
128    pub fn execution_service_client(
129        &self,
130    ) -> TransactionExecutionServiceClient<InterceptedChannel> {
131        self.configure_client(TransactionExecutionServiceClient::with_interceptor(
132            self.channel.clone(),
133            self.headers.clone(),
134        ))
135    }
136
137    /// Get a state service client.
138    pub fn state_service_client(&self) -> StateServiceClient<InterceptedChannel> {
139        self.configure_client(StateServiceClient::with_interceptor(
140            self.channel.clone(),
141            self.headers.clone(),
142        ))
143    }
144
145    /// Get a move package service client.
146    pub fn move_package_service_client(&self) -> MovePackageServiceClient<InterceptedChannel> {
147        self.configure_client(MovePackageServiceClient::with_interceptor(
148            self.channel.clone(),
149            self.headers.clone(),
150        ))
151    }
152
153    /// Apply common client configuration (compression, message size limits).
154    fn configure_client<C: GrpcClientConfig>(&self, client: C) -> C {
155        let client = client.accept_compressed(CompressionEncoding::Zstd);
156        if let Some(limit) = self.max_decoding_message_size {
157            client.max_decoding_message_size(limit)
158        } else {
159            client
160        }
161    }
162}
163
164/// Trait for common gRPC client configuration methods.
165///
166/// This trait abstracts over the common configuration methods shared by
167/// tonic-generated service clients, allowing `configure_client` to work
168/// generically.
169trait GrpcClientConfig: Sized {
170    fn accept_compressed(self, encoding: CompressionEncoding) -> Self;
171    fn max_decoding_message_size(self, limit: usize) -> Self;
172}
173
174/// Implement `GrpcClientConfig` for tonic-generated service clients.
175macro_rules! impl_grpc_client_config {
176    ($($client:ty),* $(,)?) => {
177        $(
178            impl GrpcClientConfig for $client {
179                fn accept_compressed(self, encoding: CompressionEncoding) -> Self {
180                    self.accept_compressed(encoding)
181                }
182                fn max_decoding_message_size(self, limit: usize) -> Self {
183                    self.max_decoding_message_size(limit)
184                }
185            }
186        )*
187    };
188}
189
190impl_grpc_client_config!(
191    LedgerServiceClient<InterceptedChannel>,
192    TransactionExecutionServiceClient<InterceptedChannel>,
193    StateServiceClient<InterceptedChannel>,
194    MovePackageServiceClient<InterceptedChannel>,
195);