miden_node_proto/clients/
mod.rs1use std::marker::PhantomData;
29use std::ops::{Deref, DerefMut};
30use std::str::FromStr;
31use std::time::Duration;
32
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, Error as TransportError};
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 auth_header_value: Option<AsciiMetadataValue>,
48}
49
50impl Default for Interceptor {
51 fn default() -> Self {
52 Self {
53 otel: None,
54 accept: AsciiMetadataValue::from_static(Self::MEDIA_TYPE),
55 auth_header_value: None,
56 }
57 }
58}
59
60impl Interceptor {
61 const MEDIA_TYPE: &str = "application/vnd.miden";
62 const VERSION: &str = "version";
63 const GENESIS: &str = "genesis";
64 const NETWORK_TX_AUTH_HEADER_NAME: &str = "x-miden-network-tx-auth";
65
66 fn new(
67 enable_otel: bool,
68 version: Option<&str>,
69 genesis: Option<&str>,
70 auth_header: Option<AsciiMetadataValue>,
71 ) -> Self {
72 if let Some(version) = version
73 && !version.is_ascii()
74 {
75 panic!("version contains non-ascii values: {version}");
76 }
77
78 if let Some(genesis) = genesis
79 && !genesis.is_ascii()
80 {
81 panic!("genesis contains non-ascii values: {genesis}");
82 }
83
84 let accept = match (version, genesis) {
85 (None, None) => Self::MEDIA_TYPE.to_string(),
86 (None, Some(genesis)) => format!("{}; {}={genesis}", Self::MEDIA_TYPE, Self::GENESIS),
87 (Some(version), None) => format!("{}; {}={version}", Self::MEDIA_TYPE, Self::VERSION),
88 (Some(version), Some(genesis)) => format!(
89 "{}; {}={version}, {}={genesis}",
90 Self::MEDIA_TYPE,
91 Self::VERSION,
92 Self::GENESIS
93 ),
94 };
95 Self {
96 otel: enable_otel.then_some(OtelInterceptor),
97 accept: AsciiMetadataValue::from_str(&accept).unwrap(),
99 auth_header_value: auth_header,
100 }
101 }
102}
103
104impl tonic::service::Interceptor for Interceptor {
105 fn call(&mut self, mut request: tonic::Request<()>) -> Result<Request<()>, Status> {
106 if let Some(mut otel) = self.otel {
107 request = otel.call(request)?;
108 }
109
110 if request.metadata().get(ACCEPT.as_str()).is_none() {
111 request.metadata_mut().insert(ACCEPT.as_str(), self.accept.clone());
112 }
113
114 if let Some(value) = &self.auth_header_value {
115 request.metadata_mut().insert(Self::NETWORK_TX_AUTH_HEADER_NAME, value.clone());
116 }
117
118 Ok(request)
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn interceptor_preserves_existing_accept_metadata() {
128 let original_accept =
129 AsciiMetadataValue::from_static("application/vnd.miden; version=1.2; genesis=0x1234");
130 let mut request = Request::new(());
131 request.metadata_mut().insert(ACCEPT.as_str(), original_accept.clone());
132
133 let mut interceptor = Interceptor::new(false, Some("9.9"), Some("0xabcd"), None);
134 let request = tonic::service::Interceptor::call(&mut interceptor, request)
135 .expect("interceptor should succeed");
136
137 assert_eq!(request.metadata().get(ACCEPT.as_str()), Some(&original_accept));
138 }
139
140 #[test]
141 fn interceptor_inserts_accept_metadata_when_missing() {
142 let mut interceptor = Interceptor::new(false, Some("9.9"), Some("0xabcd"), None);
143
144 let request = tonic::service::Interceptor::call(&mut interceptor, Request::new(()))
145 .expect("interceptor should succeed");
146
147 assert_eq!(
148 request.metadata().get(ACCEPT.as_str()).and_then(|value| value.to_str().ok()),
149 Some("application/vnd.miden; version=9.9, genesis=0xabcd"),
150 );
151 }
152}
153
154type InterceptedChannel = InterceptedService<Channel, Interceptor>;
158type GeneratedRpcClient = generated::rpc::api_client::ApiClient<InterceptedChannel>;
159type GeneratedProxyStatusClient =
160 generated::remote_prover::proxy_status_api_client::ProxyStatusApiClient<InterceptedChannel>;
161type GeneratedProverClient = generated::remote_prover::api_client::ApiClient<InterceptedChannel>;
162type GeneratedValidatorClient = generated::validator::api_client::ApiClient<InterceptedChannel>;
163type GeneratedNtxBuilderClient = generated::ntx_builder::api_client::ApiClient<InterceptedChannel>;
164
165#[derive(Debug, Clone)]
169pub struct RpcClient(GeneratedRpcClient);
170#[derive(Debug, Clone)]
171pub struct RemoteProverProxyStatusClient(GeneratedProxyStatusClient);
172#[derive(Debug, Clone)]
173pub struct RemoteProverClient(GeneratedProverClient);
174#[derive(Debug, Clone)]
175pub struct ValidatorClient(GeneratedValidatorClient);
176#[derive(Debug, Clone)]
177pub struct NtxBuilderClient(GeneratedNtxBuilderClient);
178
179impl DerefMut for RpcClient {
180 fn deref_mut(&mut self) -> &mut Self::Target {
181 &mut self.0
182 }
183}
184
185impl Deref for RpcClient {
186 type Target = GeneratedRpcClient;
187
188 fn deref(&self) -> &Self::Target {
189 &self.0
190 }
191}
192
193impl DerefMut for RemoteProverProxyStatusClient {
194 fn deref_mut(&mut self) -> &mut Self::Target {
195 &mut self.0
196 }
197}
198
199impl Deref for RemoteProverProxyStatusClient {
200 type Target = GeneratedProxyStatusClient;
201
202 fn deref(&self) -> &Self::Target {
203 &self.0
204 }
205}
206
207impl DerefMut for RemoteProverClient {
208 fn deref_mut(&mut self) -> &mut Self::Target {
209 &mut self.0
210 }
211}
212
213impl Deref for RemoteProverClient {
214 type Target = GeneratedProverClient;
215
216 fn deref(&self) -> &Self::Target {
217 &self.0
218 }
219}
220
221impl DerefMut for ValidatorClient {
222 fn deref_mut(&mut self) -> &mut Self::Target {
223 &mut self.0
224 }
225}
226
227impl Deref for ValidatorClient {
228 type Target = GeneratedValidatorClient;
229
230 fn deref(&self) -> &Self::Target {
231 &self.0
232 }
233}
234
235impl DerefMut for NtxBuilderClient {
236 fn deref_mut(&mut self) -> &mut Self::Target {
237 &mut self.0
238 }
239}
240
241impl Deref for NtxBuilderClient {
242 type Target = GeneratedNtxBuilderClient;
243
244 fn deref(&self) -> &Self::Target {
245 &self.0
246 }
247}
248
249pub trait GrpcClient {
254 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self;
255}
256
257impl GrpcClient for RpcClient {
258 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
259 Self(GeneratedRpcClient::new(InterceptedService::new(channel, interceptor)))
260 }
261}
262
263impl GrpcClient for RemoteProverProxyStatusClient {
264 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
265 Self(GeneratedProxyStatusClient::new(InterceptedService::new(channel, interceptor)))
266 }
267}
268
269impl GrpcClient for RemoteProverClient {
270 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
271 Self(GeneratedProverClient::new(InterceptedService::new(channel, interceptor)))
272 }
273}
274
275impl GrpcClient for ValidatorClient {
276 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
277 Self(GeneratedValidatorClient::new(InterceptedService::new(channel, interceptor)))
278 }
279}
280
281impl GrpcClient for NtxBuilderClient {
282 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
283 Self(GeneratedNtxBuilderClient::new(InterceptedService::new(channel, interceptor)))
284 }
285}
286
287#[derive(Clone, Debug)]
318pub struct Builder<State> {
319 endpoint: Endpoint,
320 metadata_version: Option<String>,
321 metadata_genesis: Option<String>,
322 metadata_auth_header_value: Option<AsciiMetadataValue>,
323 enable_otel: bool,
324 _state: PhantomData<State>,
325}
326
327#[derive(Copy, Clone, Debug)]
328pub struct WantsTls;
329#[derive(Copy, Clone, Debug)]
330pub struct WantsTimeout;
331#[derive(Copy, Clone, Debug)]
332pub struct WantsVersion;
333#[derive(Copy, Clone, Debug)]
334pub struct WantsGenesis;
335#[derive(Copy, Clone, Debug)]
336pub struct WantsOTel;
337#[derive(Copy, Clone, Debug)]
338pub struct WantsConnection;
339
340impl<State> Builder<State> {
341 fn next_state<Next>(self) -> Builder<Next> {
343 Builder {
344 endpoint: self.endpoint,
345 metadata_version: self.metadata_version,
346 metadata_genesis: self.metadata_genesis,
347 metadata_auth_header_value: self.metadata_auth_header_value,
348 enable_otel: self.enable_otel,
349 _state: PhantomData::<Next>,
350 }
351 }
352}
353
354impl Builder<WantsTls> {
355 pub fn new(url: Url) -> Builder<WantsTls> {
358 let endpoint = Endpoint::from_shared(String::from(url))
359 .expect("Url type always results in valid endpoint");
360
361 Builder {
362 endpoint,
363 metadata_version: None,
364 metadata_genesis: None,
365 metadata_auth_header_value: None,
366 enable_otel: false,
367 _state: PhantomData,
368 }
369 }
370
371 pub fn without_tls(self) -> Builder<WantsTimeout> {
373 self.next_state()
374 }
375
376 pub fn with_tls(mut self) -> Result<Builder<WantsTimeout>, TransportError> {
378 self.endpoint = self.endpoint.tls_config(ClientTlsConfig::new().with_native_roots())?;
379
380 Ok(self.next_state())
381 }
382}
383
384impl Builder<WantsTimeout> {
385 pub fn without_timeout(self) -> Builder<WantsVersion> {
387 self.next_state()
388 }
389
390 pub fn with_timeout(mut self, duration: Duration) -> Builder<WantsVersion> {
392 self.endpoint = self.endpoint.timeout(duration);
393 self.next_state()
394 }
395}
396
397impl Builder<WantsVersion> {
398 pub fn without_metadata_version(mut self) -> Builder<WantsGenesis> {
400 self.metadata_version = None;
401 self.next_state()
402 }
403
404 pub fn with_metadata_version(mut self, version: String) -> Builder<WantsGenesis> {
406 self.metadata_version = Some(version);
407 self.next_state()
408 }
409}
410
411impl Builder<WantsGenesis> {
412 pub fn without_metadata_genesis(mut self) -> Builder<WantsOTel> {
414 self.metadata_genesis = None;
415 self.next_state()
416 }
417
418 pub fn with_metadata_genesis(mut self, genesis: String) -> Builder<WantsOTel> {
420 self.metadata_genesis = Some(genesis);
421 self.next_state()
422 }
423}
424
425impl Builder<WantsOTel> {
426 #[must_use]
428 pub fn without_auth_header(mut self) -> Self {
429 self.metadata_auth_header_value = None;
430 self
431 }
432
433 #[must_use]
435 pub fn with_auth_header_value(mut self, value: AsciiMetadataValue) -> Self {
436 self.metadata_auth_header_value = Some(value);
437 self
438 }
439
440 pub fn with_otel_context_injection(mut self) -> Builder<WantsConnection> {
445 self.enable_otel = true;
446 self.next_state()
447 }
448
449 pub fn without_otel_context_injection(mut self) -> Builder<WantsConnection> {
452 self.enable_otel = false;
453 self.next_state()
454 }
455}
456
457impl Builder<WantsConnection> {
458 pub async fn connect<T>(self) -> Result<T, TransportError>
460 where
461 T: GrpcClient,
462 {
463 let channel = self.endpoint.connect().await?;
464 Ok(self.connect_with_channel::<T>(channel))
465 }
466
467 pub fn connect_lazy<T>(self) -> T
469 where
470 T: GrpcClient,
471 {
472 let channel = self.endpoint.connect_lazy();
473 self.connect_with_channel::<T>(channel)
474 }
475
476 fn connect_with_channel<T>(self, channel: Channel) -> T
477 where
478 T: GrpcClient,
479 {
480 let interceptor = Interceptor::new(
481 self.enable_otel,
482 self.metadata_version.as_deref(),
483 self.metadata_genesis.as_deref(),
484 self.metadata_auth_header_value,
485 );
486 T::with_interceptor(channel, interceptor)
487 }
488}