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>;
123
124// gRPC CLIENTS
125// ================================================================================================
126
127#[derive(Debug, Clone)]
128pub struct RpcClient(GeneratedRpcClient);
129#[derive(Debug, Clone)]
130pub struct BlockProducerClient(GeneratedBlockProducerClient);
131#[derive(Debug, Clone)]
132pub struct StoreNtxBuilderClient(GeneratedStoreClientForNtxBuilder);
133#[derive(Debug, Clone)]
134pub struct StoreBlockProducerClient(GeneratedStoreClientForBlockProducer);
135#[derive(Debug, Clone)]
136pub struct StoreRpcClient(GeneratedStoreClientForRpc);
137#[derive(Debug, Clone)]
138pub struct RemoteProverProxyStatusClient(GeneratedProxyStatusClient);
139#[derive(Debug, Clone)]
140pub struct RemoteProverClient(GeneratedProverClient);
141#[derive(Debug, Clone)]
142pub struct ValidatorClient(GeneratedValidatorClient);
143
144impl DerefMut for RpcClient {
145    fn deref_mut(&mut self) -> &mut Self::Target {
146        &mut self.0
147    }
148}
149
150impl Deref for RpcClient {
151    type Target = GeneratedRpcClient;
152
153    fn deref(&self) -> &Self::Target {
154        &self.0
155    }
156}
157
158impl DerefMut for BlockProducerClient {
159    fn deref_mut(&mut self) -> &mut Self::Target {
160        &mut self.0
161    }
162}
163
164impl Deref for BlockProducerClient {
165    type Target = GeneratedBlockProducerClient;
166
167    fn deref(&self) -> &Self::Target {
168        &self.0
169    }
170}
171
172impl DerefMut for StoreNtxBuilderClient {
173    fn deref_mut(&mut self) -> &mut Self::Target {
174        &mut self.0
175    }
176}
177
178impl Deref for StoreNtxBuilderClient {
179    type Target = GeneratedStoreClientForNtxBuilder;
180
181    fn deref(&self) -> &Self::Target {
182        &self.0
183    }
184}
185
186impl DerefMut for StoreBlockProducerClient {
187    fn deref_mut(&mut self) -> &mut Self::Target {
188        &mut self.0
189    }
190}
191
192impl Deref for StoreBlockProducerClient {
193    type Target = GeneratedStoreClientForBlockProducer;
194
195    fn deref(&self) -> &Self::Target {
196        &self.0
197    }
198}
199
200impl DerefMut for StoreRpcClient {
201    fn deref_mut(&mut self) -> &mut Self::Target {
202        &mut self.0
203    }
204}
205
206impl Deref for StoreRpcClient {
207    type Target = GeneratedStoreClientForRpc;
208
209    fn deref(&self) -> &Self::Target {
210        &self.0
211    }
212}
213
214impl DerefMut for RemoteProverProxyStatusClient {
215    fn deref_mut(&mut self) -> &mut Self::Target {
216        &mut self.0
217    }
218}
219
220impl Deref for RemoteProverProxyStatusClient {
221    type Target = GeneratedProxyStatusClient;
222
223    fn deref(&self) -> &Self::Target {
224        &self.0
225    }
226}
227
228impl DerefMut for RemoteProverClient {
229    fn deref_mut(&mut self) -> &mut Self::Target {
230        &mut self.0
231    }
232}
233
234impl Deref for RemoteProverClient {
235    type Target = GeneratedProverClient;
236
237    fn deref(&self) -> &Self::Target {
238        &self.0
239    }
240}
241
242impl DerefMut for ValidatorClient {
243    fn deref_mut(&mut self) -> &mut Self::Target {
244        &mut self.0
245    }
246}
247
248impl Deref for ValidatorClient {
249    type Target = GeneratedValidatorClient;
250
251    fn deref(&self) -> &Self::Target {
252        &self.0
253    }
254}
255
256// GRPC CLIENT BUILDER TRAIT
257// ================================================================================================
258
259/// Trait for building gRPC clients from a common [`Builder`] configuration.
260pub trait GrpcClient {
261    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self;
262}
263
264impl GrpcClient for RpcClient {
265    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
266        Self(GeneratedRpcClient::new(InterceptedService::new(channel, interceptor)))
267    }
268}
269
270impl GrpcClient for BlockProducerClient {
271    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
272        Self(GeneratedBlockProducerClient::new(InterceptedService::new(channel, interceptor)))
273    }
274}
275
276impl GrpcClient for StoreNtxBuilderClient {
277    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
278        Self(GeneratedStoreClientForNtxBuilder::new(InterceptedService::new(
279            channel,
280            interceptor,
281        )))
282    }
283}
284
285impl GrpcClient for StoreBlockProducerClient {
286    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
287        Self(GeneratedStoreClientForBlockProducer::new(InterceptedService::new(
288            channel,
289            interceptor,
290        )))
291    }
292}
293
294impl GrpcClient for StoreRpcClient {
295    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
296        Self(GeneratedStoreClientForRpc::new(InterceptedService::new(channel, interceptor)))
297    }
298}
299
300impl GrpcClient for RemoteProverProxyStatusClient {
301    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
302        Self(GeneratedProxyStatusClient::new(InterceptedService::new(channel, interceptor)))
303    }
304}
305
306impl GrpcClient for RemoteProverClient {
307    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
308        Self(GeneratedProverClient::new(InterceptedService::new(channel, interceptor)))
309    }
310}
311
312impl GrpcClient for ValidatorClient {
313    fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
314        Self(GeneratedValidatorClient::new(InterceptedService::new(channel, interceptor)))
315    }
316}
317
318// STRICT TYPE-SAFE BUILDER (NO DEFAULTS)
319// ================================================================================================
320
321/// A type-safe builder that forces the caller to make an explicit decision for each
322/// configuration item (TLS, timeout, metadata version, metadata genesis) before connecting.
323///
324/// This builder replaces the previous defaulted builder. Callers must explicitly choose TLS,
325/// timeout, and metadata options before connecting.
326///
327/// Usage example:
328///
329/// ```rust
330/// # use miden_node_proto::clients::{Builder, WantsTls, RpcClient};
331/// # use url::Url;
332/// # use std::time::Duration;
333///
334/// # async fn example() -> anyhow::Result<()> {
335/// let url = Url::parse("https://rpc.example.com:8080")?;
336/// let client: RpcClient = Builder::new(url)
337///     .with_tls()?                          // or `.without_tls()`
338///     .with_timeout(Duration::from_secs(5)) // or `.without_timeout()`
339///     .with_metadata_version("1.0".into())  // or `.without_metadata_version()`
340///     .without_metadata_genesis()           // or `.with_metadata_genesis(genesis)`
341///     .with_otel_context_injection()        // or `.without_otel_context_injection()`
342///     .connect::<RpcClient>()
343///     .await?;
344/// # Ok(())
345/// # }
346/// ```
347#[derive(Clone, Debug)]
348pub struct Builder<State> {
349    endpoint: Endpoint,
350    metadata_version: Option<String>,
351    metadata_genesis: Option<String>,
352    enable_otel: bool,
353    _state: PhantomData<State>,
354}
355
356#[derive(Copy, Clone, Debug)]
357pub struct WantsTls;
358#[derive(Copy, Clone, Debug)]
359pub struct WantsTimeout;
360#[derive(Copy, Clone, Debug)]
361pub struct WantsVersion;
362#[derive(Copy, Clone, Debug)]
363pub struct WantsGenesis;
364#[derive(Copy, Clone, Debug)]
365pub struct WantsOTel;
366#[derive(Copy, Clone, Debug)]
367pub struct WantsConnection;
368
369impl<State> Builder<State> {
370    /// Convenience function to cast the state type and carry internal configuration forward.
371    fn next_state<Next>(self) -> Builder<Next> {
372        Builder {
373            endpoint: self.endpoint,
374            metadata_version: self.metadata_version,
375            metadata_genesis: self.metadata_genesis,
376            enable_otel: self.enable_otel,
377            _state: PhantomData::<Next>,
378        }
379    }
380}
381
382impl Builder<WantsTls> {
383    /// Create a new strict builder from a gRPC endpoint URL such as
384    /// `http://localhost:8080` or `https://api.example.com:443`.
385    pub fn new(url: Url) -> Builder<WantsTls> {
386        let endpoint = Endpoint::from_shared(String::from(url))
387            .expect("Url type always results in valid endpoint");
388
389        Builder {
390            endpoint,
391            metadata_version: None,
392            metadata_genesis: None,
393            enable_otel: false,
394            _state: PhantomData,
395        }
396    }
397
398    /// Explicitly disable TLS.
399    pub fn without_tls(self) -> Builder<WantsTimeout> {
400        self.next_state()
401    }
402
403    /// Explicitly enable TLS.
404    pub fn with_tls(mut self) -> Result<Builder<WantsTimeout>> {
405        self.endpoint = self
406            .endpoint
407            .tls_config(ClientTlsConfig::new().with_native_roots())
408            .context("Failed to configure TLS")?;
409
410        Ok(self.next_state())
411    }
412}
413
414impl Builder<WantsTimeout> {
415    /// Explicitly disable request timeout.
416    pub fn without_timeout(self) -> Builder<WantsVersion> {
417        self.next_state()
418    }
419
420    /// Explicitly configure a request timeout.
421    pub fn with_timeout(mut self, duration: Duration) -> Builder<WantsVersion> {
422        self.endpoint = self.endpoint.timeout(duration);
423        self.next_state()
424    }
425}
426
427impl Builder<WantsVersion> {
428    /// Do not include version in request metadata.
429    pub fn without_metadata_version(mut self) -> Builder<WantsGenesis> {
430        self.metadata_version = None;
431        self.next_state()
432    }
433
434    /// Include a specific version string in request metadata.
435    pub fn with_metadata_version(mut self, version: String) -> Builder<WantsGenesis> {
436        self.metadata_version = Some(version);
437        self.next_state()
438    }
439}
440
441impl Builder<WantsGenesis> {
442    /// Do not include genesis commitment in request metadata.
443    pub fn without_metadata_genesis(mut self) -> Builder<WantsOTel> {
444        self.metadata_genesis = None;
445        self.next_state()
446    }
447
448    /// Include a specific genesis commitment string in request metadata.
449    pub fn with_metadata_genesis(mut self, genesis: String) -> Builder<WantsOTel> {
450        self.metadata_genesis = Some(genesis);
451        self.next_state()
452    }
453}
454
455impl Builder<WantsOTel> {
456    /// Enables OpenTelemetry context propagation via gRPC.
457    ///
458    /// This is used to by OpenTelemetry to connect traces across network boundaries. The server on
459    /// the other end must be configured to receive and use the injected trace context.
460    pub fn with_otel_context_injection(mut self) -> Builder<WantsConnection> {
461        self.enable_otel = true;
462        self.next_state()
463    }
464
465    /// Disables OpenTelemetry context propagation. This should be disabled when interfacing with
466    /// external third party gRPC servers.
467    pub fn without_otel_context_injection(mut self) -> Builder<WantsConnection> {
468        self.enable_otel = false;
469        self.next_state()
470    }
471}
472
473impl Builder<WantsConnection> {
474    /// Establish an eager connection and return a fully configured client.
475    pub async fn connect<T>(self) -> Result<T>
476    where
477        T: GrpcClient,
478    {
479        let channel = self.endpoint.connect().await?;
480        Ok(self.connect_with_channel::<T>(channel))
481    }
482
483    /// Establish a lazy connection and return a client that will connect on first use.
484    pub fn connect_lazy<T>(self) -> T
485    where
486        T: GrpcClient,
487    {
488        let channel = self.endpoint.connect_lazy();
489        self.connect_with_channel::<T>(channel)
490    }
491
492    fn connect_with_channel<T>(self, channel: Channel) -> T
493    where
494        T: GrpcClient,
495    {
496        let interceptor = Interceptor::new(
497            self.enable_otel,
498            self.metadata_version.as_deref(),
499            self.metadata_genesis.as_deref(),
500        );
501        T::with_interceptor(channel, interceptor)
502    }
503}