Skip to main content

miden_node_proto/clients/
mod.rs

1//! gRPC client builder utilities for Miden node.
2//!
3//! This module provides a unified type-safe [`Builder`] for creating various gRPC clients with
4//! explicit configuration decisions for TLS, timeout, and metadata.
5//!
6//! # Examples
7//!
8//! ```rust
9//! # use miden_node_proto::clients::{Builder, WantsTls, StoreNtxBuilderClient};
10//! # use url::Url;
11//!
12//! # async fn example() -> anyhow::Result<()> {
13//! // Create a store client with OTEL and TLS
14//! let url = Url::parse("https://example.com:8080")?;
15//! let client: StoreNtxBuilderClient = Builder::new(url)
16//!     .with_tls()?                   // or `.without_tls()`
17//!     .without_timeout()             // or `.with_timeout(Duration::from_secs(10))`
18//!     .without_metadata_version()    // or `.with_metadata_version("1.0".into())`
19//!     .without_metadata_genesis()    // or `.with_metadata_genesis(genesis)`
20//!     .with_otel_context_injection() // or `.without_otel_context_injection()`
21//!     .connect::<StoreNtxBuilderClient>()
22//!     .await?;
23//! # Ok(())
24//! # }
25//! ```
26
27use std::marker::PhantomData;
28use std::ops::{Deref, DerefMut};
29use std::str::FromStr;
30use std::time::Duration;
31
32use anyhow::{Context, Result};
33use http::header::ACCEPT;
34use miden_node_utils::tracing::grpc::OtelInterceptor;
35use tonic::metadata::AsciiMetadataValue;
36use tonic::service::interceptor::InterceptedService;
37use tonic::transport::{Channel, ClientTlsConfig, Endpoint};
38use tonic::{Request, Status};
39use url::Url;
40
41use crate::generated;
42
43#[derive(Clone)]
44pub struct Interceptor {
45    otel: Option<OtelInterceptor>,
46    accept: AsciiMetadataValue,
47}
48
49impl Default for Interceptor {
50    fn default() -> Self {
51        Self {
52            otel: None,
53            accept: AsciiMetadataValue::from_static(Self::MEDIA_TYPE),
54        }
55    }
56}
57
58impl Interceptor {
59    const MEDIA_TYPE: &str = "application/vnd.miden";
60    const VERSION: &str = "version";
61    const GENESIS: &str = "genesis";
62
63    fn new(enable_otel: bool, version: Option<&str>, genesis: Option<&str>) -> Self {
64        if let Some(version) = version
65            && !version.is_ascii()
66        {
67            panic!("version contains non-ascii values: {version}");
68        }
69
70        if let Some(genesis) = genesis
71            && !genesis.is_ascii()
72        {
73            panic!("genesis contains non-ascii values: {genesis}");
74        }
75
76        let accept = match (version, genesis) {
77            (None, None) => Self::MEDIA_TYPE.to_string(),
78            (None, Some(genesis)) => format!("{}; {}={genesis}", Self::MEDIA_TYPE, Self::GENESIS),
79            (Some(version), None) => format!("{}; {}={version}", Self::MEDIA_TYPE, Self::VERSION),
80            (Some(version), Some(genesis)) => format!(
81                "{}; {}={version}, {}={genesis}",
82                Self::MEDIA_TYPE,
83                Self::VERSION,
84                Self::GENESIS
85            ),
86        };
87        Self {
88            otel: enable_otel.then_some(OtelInterceptor),
89            // SAFETY: we checked that all values are ascii at the top of the function.
90            accept: AsciiMetadataValue::from_str(&accept).unwrap(),
91        }
92    }
93}
94
95impl tonic::service::Interceptor for Interceptor {
96    fn call(&mut self, mut request: tonic::Request<()>) -> Result<Request<()>, Status> {
97        if let Some(mut otel) = self.otel {
98            request = otel.call(request)?;
99        }
100
101        request.metadata_mut().insert(ACCEPT.as_str(), self.accept.clone());
102
103        Ok(request)
104    }
105}
106
107// TYPE ALIASES TO AID LEGIBILITY
108// ================================================================================================
109
110type InterceptedChannel = InterceptedService<Channel, Interceptor>;
111type GeneratedRpcClient = generated::rpc::api_client::ApiClient<InterceptedChannel>;
112type GeneratedBlockProducerClient =
113    generated::block_producer::api_client::ApiClient<InterceptedChannel>;
114type GeneratedStoreClientForNtxBuilder =
115    generated::store::ntx_builder_client::NtxBuilderClient<InterceptedChannel>;
116type GeneratedStoreClientForBlockProducer =
117    generated::store::block_producer_client::BlockProducerClient<InterceptedChannel>;
118type GeneratedStoreClientForRpc = generated::store::rpc_client::RpcClient<InterceptedChannel>;
119type GeneratedProxyStatusClient =
120    generated::remote_prover::proxy_status_api_client::ProxyStatusApiClient<InterceptedChannel>;
121type GeneratedProverClient = generated::remote_prover::api_client::ApiClient<InterceptedChannel>;
122type GeneratedValidatorClient = generated::validator::api_client::ApiClient<InterceptedChannel>;
123type GeneratedNtxBuilderClient = generated::ntx_builder::api_client::ApiClient<InterceptedChannel>;
124
125// gRPC CLIENTS
126// ================================================================================================
127
128#[derive(Debug, Clone)]
129pub struct RpcClient(GeneratedRpcClient);
130#[derive(Debug, Clone)]
131pub struct BlockProducerClient(GeneratedBlockProducerClient);
132#[derive(Debug, Clone)]
133pub struct StoreNtxBuilderClient(GeneratedStoreClientForNtxBuilder);
134#[derive(Debug, Clone)]
135pub struct StoreBlockProducerClient(GeneratedStoreClientForBlockProducer);
136#[derive(Debug, Clone)]
137pub struct StoreRpcClient(GeneratedStoreClientForRpc);
138#[derive(Debug, Clone)]
139pub struct RemoteProverProxyStatusClient(GeneratedProxyStatusClient);
140#[derive(Debug, Clone)]
141pub struct RemoteProverClient(GeneratedProverClient);
142#[derive(Debug, Clone)]
143pub struct ValidatorClient(GeneratedValidatorClient);
144#[derive(Debug, Clone)]
145pub struct NtxBuilderClient(GeneratedNtxBuilderClient);
146
147impl DerefMut for RpcClient {
148    fn deref_mut(&mut self) -> &mut Self::Target {
149        &mut self.0
150    }
151}
152
153impl Deref for RpcClient {
154    type Target = GeneratedRpcClient;
155
156    fn deref(&self) -> &Self::Target {
157        &self.0
158    }
159}
160
161impl DerefMut for BlockProducerClient {
162    fn deref_mut(&mut self) -> &mut Self::Target {
163        &mut self.0
164    }
165}
166
167impl Deref for BlockProducerClient {
168    type Target = GeneratedBlockProducerClient;
169
170    fn deref(&self) -> &Self::Target {
171        &self.0
172    }
173}
174
175impl DerefMut for StoreNtxBuilderClient {
176    fn deref_mut(&mut self) -> &mut Self::Target {
177        &mut self.0
178    }
179}
180
181impl Deref for StoreNtxBuilderClient {
182    type Target = GeneratedStoreClientForNtxBuilder;
183
184    fn deref(&self) -> &Self::Target {
185        &self.0
186    }
187}
188
189impl DerefMut for StoreBlockProducerClient {
190    fn deref_mut(&mut self) -> &mut Self::Target {
191        &mut self.0
192    }
193}
194
195impl Deref for StoreBlockProducerClient {
196    type Target = GeneratedStoreClientForBlockProducer;
197
198    fn deref(&self) -> &Self::Target {
199        &self.0
200    }
201}
202
203impl DerefMut for StoreRpcClient {
204    fn deref_mut(&mut self) -> &mut Self::Target {
205        &mut self.0
206    }
207}
208
209impl Deref for StoreRpcClient {
210    type Target = GeneratedStoreClientForRpc;
211
212    fn deref(&self) -> &Self::Target {
213        &self.0
214    }
215}
216
217impl DerefMut for RemoteProverProxyStatusClient {
218    fn deref_mut(&mut self) -> &mut Self::Target {
219        &mut self.0
220    }
221}
222
223impl Deref for RemoteProverProxyStatusClient {
224    type Target = GeneratedProxyStatusClient;
225
226    fn deref(&self) -> &Self::Target {
227        &self.0
228    }
229}
230
231impl DerefMut for RemoteProverClient {
232    fn deref_mut(&mut self) -> &mut Self::Target {
233        &mut self.0
234    }
235}
236
237impl Deref for RemoteProverClient {
238    type Target = GeneratedProverClient;
239
240    fn deref(&self) -> &Self::Target {
241        &self.0
242    }
243}
244
245impl DerefMut for ValidatorClient {
246    fn deref_mut(&mut self) -> &mut Self::Target {
247        &mut self.0
248    }
249}
250
251impl Deref for ValidatorClient {
252    type Target = GeneratedValidatorClient;
253
254    fn deref(&self) -> &Self::Target {
255        &self.0
256    }
257}
258
259impl DerefMut for NtxBuilderClient {
260    fn deref_mut(&mut self) -> &mut Self::Target {
261        &mut self.0
262    }
263}
264
265impl Deref for NtxBuilderClient {
266    type Target = GeneratedNtxBuilderClient;
267
268    fn deref(&self) -> &Self::Target {
269        &self.0
270    }
271}
272
273// GRPC CLIENT BUILDER TRAIT
274// ================================================================================================
275
276/// Trait for building gRPC clients from a common [`Builder`] configuration.
277pub trait GrpcClient {
278    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self;
279}
280
281impl GrpcClient for RpcClient {
282    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
283        Self(GeneratedRpcClient::new(InterceptedService::new(channel, interceptor)))
284    }
285}
286
287impl GrpcClient for BlockProducerClient {
288    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
289        Self(GeneratedBlockProducerClient::new(InterceptedService::new(channel, interceptor)))
290    }
291}
292
293impl GrpcClient for StoreNtxBuilderClient {
294    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
295        Self(GeneratedStoreClientForNtxBuilder::new(InterceptedService::new(
296            channel,
297            interceptor,
298        )))
299    }
300}
301
302impl GrpcClient for StoreBlockProducerClient {
303    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
304        Self(GeneratedStoreClientForBlockProducer::new(InterceptedService::new(
305            channel,
306            interceptor,
307        )))
308    }
309}
310
311impl GrpcClient for StoreRpcClient {
312    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
313        Self(GeneratedStoreClientForRpc::new(InterceptedService::new(channel, interceptor)))
314    }
315}
316
317impl GrpcClient for RemoteProverProxyStatusClient {
318    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
319        Self(GeneratedProxyStatusClient::new(InterceptedService::new(channel, interceptor)))
320    }
321}
322
323impl GrpcClient for RemoteProverClient {
324    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
325        Self(GeneratedProverClient::new(InterceptedService::new(channel, interceptor)))
326    }
327}
328
329impl GrpcClient for ValidatorClient {
330    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
331        Self(GeneratedValidatorClient::new(InterceptedService::new(channel, interceptor)))
332    }
333}
334
335impl GrpcClient for NtxBuilderClient {
336    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
337        Self(GeneratedNtxBuilderClient::new(InterceptedService::new(channel, interceptor)))
338    }
339}
340
341// STRICT TYPE-SAFE BUILDER (NO DEFAULTS)
342// ================================================================================================
343
344/// A type-safe builder that forces the caller to make an explicit decision for each
345/// configuration item (TLS, timeout, metadata version, metadata genesis) before connecting.
346///
347/// This builder replaces the previous defaulted builder. Callers must explicitly choose TLS,
348/// timeout, and metadata options before connecting.
349///
350/// Usage example:
351///
352/// ```rust
353/// # use miden_node_proto::clients::{Builder, WantsTls, RpcClient};
354/// # use url::Url;
355/// # use std::time::Duration;
356///
357/// # async fn example() -> anyhow::Result<()> {
358/// let url = Url::parse("https://rpc.example.com:8080")?;
359/// let client: RpcClient = Builder::new(url)
360///     .with_tls()?                          // or `.without_tls()`
361///     .with_timeout(Duration::from_secs(5)) // or `.without_timeout()`
362///     .with_metadata_version("1.0".into())  // or `.without_metadata_version()`
363///     .without_metadata_genesis()           // or `.with_metadata_genesis(genesis)`
364///     .with_otel_context_injection()        // or `.without_otel_context_injection()`
365///     .connect::<RpcClient>()
366///     .await?;
367/// # Ok(())
368/// # }
369/// ```
370#[derive(Clone, Debug)]
371pub struct Builder<State> {
372    endpoint: Endpoint,
373    metadata_version: Option<String>,
374    metadata_genesis: Option<String>,
375    enable_otel: bool,
376    _state: PhantomData<State>,
377}
378
379#[derive(Copy, Clone, Debug)]
380pub struct WantsTls;
381#[derive(Copy, Clone, Debug)]
382pub struct WantsTimeout;
383#[derive(Copy, Clone, Debug)]
384pub struct WantsVersion;
385#[derive(Copy, Clone, Debug)]
386pub struct WantsGenesis;
387#[derive(Copy, Clone, Debug)]
388pub struct WantsOTel;
389#[derive(Copy, Clone, Debug)]
390pub struct WantsConnection;
391
392impl<State> Builder<State> {
393    /// Convenience function to cast the state type and carry internal configuration forward.
394    fn next_state<Next>(self) -> Builder<Next> {
395        Builder {
396            endpoint: self.endpoint,
397            metadata_version: self.metadata_version,
398            metadata_genesis: self.metadata_genesis,
399            enable_otel: self.enable_otel,
400            _state: PhantomData::<Next>,
401        }
402    }
403}
404
405impl Builder<WantsTls> {
406    /// Create a new strict builder from a gRPC endpoint URL such as
407    /// `http://localhost:8080` or `https://api.example.com:443`.
408    pub fn new(url: Url) -> Builder<WantsTls> {
409        let endpoint = Endpoint::from_shared(String::from(url))
410            .expect("Url type always results in valid endpoint");
411
412        Builder {
413            endpoint,
414            metadata_version: None,
415            metadata_genesis: None,
416            enable_otel: false,
417            _state: PhantomData,
418        }
419    }
420
421    /// Explicitly disable TLS.
422    pub fn without_tls(self) -> Builder<WantsTimeout> {
423        self.next_state()
424    }
425
426    /// Explicitly enable TLS.
427    pub fn with_tls(mut self) -> Result<Builder<WantsTimeout>> {
428        self.endpoint = self
429            .endpoint
430            .tls_config(ClientTlsConfig::new().with_native_roots())
431            .context("Failed to configure TLS")?;
432
433        Ok(self.next_state())
434    }
435}
436
437impl Builder<WantsTimeout> {
438    /// Explicitly disable request timeout.
439    pub fn without_timeout(self) -> Builder<WantsVersion> {
440        self.next_state()
441    }
442
443    /// Explicitly configure a request timeout.
444    pub fn with_timeout(mut self, duration: Duration) -> Builder<WantsVersion> {
445        self.endpoint = self.endpoint.timeout(duration);
446        self.next_state()
447    }
448}
449
450impl Builder<WantsVersion> {
451    /// Do not include version in request metadata.
452    pub fn without_metadata_version(mut self) -> Builder<WantsGenesis> {
453        self.metadata_version = None;
454        self.next_state()
455    }
456
457    /// Include a specific version string in request metadata.
458    pub fn with_metadata_version(mut self, version: String) -> Builder<WantsGenesis> {
459        self.metadata_version = Some(version);
460        self.next_state()
461    }
462}
463
464impl Builder<WantsGenesis> {
465    /// Do not include genesis commitment in request metadata.
466    pub fn without_metadata_genesis(mut self) -> Builder<WantsOTel> {
467        self.metadata_genesis = None;
468        self.next_state()
469    }
470
471    /// Include a specific genesis commitment string in request metadata.
472    pub fn with_metadata_genesis(mut self, genesis: String) -> Builder<WantsOTel> {
473        self.metadata_genesis = Some(genesis);
474        self.next_state()
475    }
476}
477
478impl Builder<WantsOTel> {
479    /// Enables OpenTelemetry context propagation via gRPC.
480    ///
481    /// This is used to by OpenTelemetry to connect traces across network boundaries. The server on
482    /// the other end must be configured to receive and use the injected trace context.
483    pub fn with_otel_context_injection(mut self) -> Builder<WantsConnection> {
484        self.enable_otel = true;
485        self.next_state()
486    }
487
488    /// Disables OpenTelemetry context propagation. This should be disabled when interfacing with
489    /// external third party gRPC servers.
490    pub fn without_otel_context_injection(mut self) -> Builder<WantsConnection> {
491        self.enable_otel = false;
492        self.next_state()
493    }
494}
495
496impl Builder<WantsConnection> {
497    /// Establish an eager connection and return a fully configured client.
498    pub async fn connect<T>(self) -> Result<T>
499    where
500        T: GrpcClient,
501    {
502        let channel = self.endpoint.connect().await?;
503        Ok(self.connect_with_channel::<T>(channel))
504    }
505
506    /// Establish a lazy connection and return a client that will connect on first use.
507    pub fn connect_lazy<T>(self) -> T
508    where
509        T: GrpcClient,
510    {
511        let channel = self.endpoint.connect_lazy();
512        self.connect_with_channel::<T>(channel)
513    }
514
515    fn connect_with_channel<T>(self, channel: Channel) -> T
516    where
517        T: GrpcClient,
518    {
519        let interceptor = Interceptor::new(
520            self.enable_otel,
521            self.metadata_version.as_deref(),
522            self.metadata_genesis.as_deref(),
523        );
524        T::with_interceptor(channel, interceptor)
525    }
526}