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 miden_protocol::batch::ProposedBatch;
36use miden_protocol::utils::serde::Serializable;
37use tonic::metadata::AsciiMetadataValue;
38use tonic::service::interceptor::InterceptedService;
39use tonic::transport::{Channel, ClientTlsConfig, Endpoint, Error as TransportError};
40use tonic::{Request, Status};
41use url::Url;
42
43use crate::generated;
44
45#[derive(Clone)]
46pub struct Interceptor {
47 otel: Option<OtelInterceptor>,
48 accept: AsciiMetadataValue,
49 auth_header_value: Option<AsciiMetadataValue>,
50}
51
52impl Default for Interceptor {
53 fn default() -> Self {
54 Self {
55 otel: None,
56 accept: AsciiMetadataValue::from_static(Self::MEDIA_TYPE),
57 auth_header_value: None,
58 }
59 }
60}
61
62impl Interceptor {
63 const MEDIA_TYPE: &str = "application/vnd.miden";
64 const VERSION: &str = "version";
65 const GENESIS: &str = "genesis";
66 const NETWORK_TX_AUTH_HEADER_NAME: &str = "x-miden-network-tx-auth";
67
68 fn new(
69 enable_otel: bool,
70 version: Option<&str>,
71 genesis: Option<&str>,
72 auth_header: Option<AsciiMetadataValue>,
73 ) -> Self {
74 if let Some(version) = version
75 && !version.is_ascii()
76 {
77 panic!("version contains non-ascii values: {version}");
78 }
79
80 if let Some(genesis) = genesis
81 && !genesis.is_ascii()
82 {
83 panic!("genesis contains non-ascii values: {genesis}");
84 }
85
86 let accept = match (version, genesis) {
87 (None, None) => Self::MEDIA_TYPE.to_string(),
88 (None, Some(genesis)) => format!("{}; {}={genesis}", Self::MEDIA_TYPE, Self::GENESIS),
89 (Some(version), None) => format!("{}; {}={version}", Self::MEDIA_TYPE, Self::VERSION),
90 (Some(version), Some(genesis)) => format!(
91 "{}; {}={version}, {}={genesis}",
92 Self::MEDIA_TYPE,
93 Self::VERSION,
94 Self::GENESIS
95 ),
96 };
97 Self {
98 otel: enable_otel.then_some(OtelInterceptor),
99 accept: AsciiMetadataValue::from_str(&accept).unwrap(),
101 auth_header_value: auth_header,
102 }
103 }
104}
105
106impl tonic::service::Interceptor for Interceptor {
107 fn call(&mut self, mut request: tonic::Request<()>) -> Result<Request<()>, Status> {
108 if let Some(mut otel) = self.otel {
109 request = otel.call(request)?;
110 }
111
112 if request.metadata().get(ACCEPT.as_str()).is_none() {
113 request.metadata_mut().insert(ACCEPT.as_str(), self.accept.clone());
114 }
115
116 if let Some(value) = &self.auth_header_value {
117 request.metadata_mut().insert(Self::NETWORK_TX_AUTH_HEADER_NAME, value.clone());
118 }
119
120 Ok(request)
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn interceptor_preserves_existing_accept_metadata() {
130 let original_accept =
131 AsciiMetadataValue::from_static("application/vnd.miden; version=1.2; genesis=0x1234");
132 let mut request = Request::new(());
133 request.metadata_mut().insert(ACCEPT.as_str(), original_accept.clone());
134
135 let mut interceptor = Interceptor::new(false, Some("9.9"), Some("0xabcd"), None);
136 let request = tonic::service::Interceptor::call(&mut interceptor, request)
137 .expect("interceptor should succeed");
138
139 assert_eq!(request.metadata().get(ACCEPT.as_str()), Some(&original_accept));
140 }
141
142 #[test]
143 fn interceptor_inserts_accept_metadata_when_missing() {
144 let mut interceptor = Interceptor::new(false, Some("9.9"), Some("0xabcd"), None);
145
146 let request = tonic::service::Interceptor::call(&mut interceptor, Request::new(()))
147 .expect("interceptor should succeed");
148
149 assert_eq!(
150 request.metadata().get(ACCEPT.as_str()).and_then(|value| value.to_str().ok()),
151 Some("application/vnd.miden; version=9.9, genesis=0xabcd"),
152 );
153 }
154}
155
156type InterceptedChannel = InterceptedService<Channel, Interceptor>;
160type GeneratedRpcClient = generated::rpc::api_client::ApiClient<InterceptedChannel>;
161type GeneratedProxyStatusClient =
162 generated::remote_prover::proxy_status_api_client::ProxyStatusApiClient<InterceptedChannel>;
163type GeneratedProverClient = generated::remote_prover::api_client::ApiClient<InterceptedChannel>;
164type GeneratedValidatorClient = generated::validator::api_client::ApiClient<InterceptedChannel>;
165type GeneratedNtxBuilderClient = generated::ntx_builder::api_client::ApiClient<InterceptedChannel>;
166type GeneratedSequencerClient = generated::sequencer::api_client::ApiClient<InterceptedChannel>;
167
168#[derive(Debug, Clone)]
172pub struct RpcClient(GeneratedRpcClient);
173#[derive(Debug, Clone)]
174pub struct RemoteProverProxyStatusClient(GeneratedProxyStatusClient);
175#[derive(Debug, Clone)]
176pub struct RemoteProverClient(GeneratedProverClient);
177#[derive(Debug, Clone)]
178pub struct ValidatorClient(GeneratedValidatorClient);
179#[derive(Debug, Clone)]
180pub struct NtxBuilderClient(GeneratedNtxBuilderClient);
181#[derive(Debug, Clone)]
182pub struct SequencerClient(GeneratedSequencerClient);
183
184impl DerefMut for RpcClient {
185 fn deref_mut(&mut self) -> &mut Self::Target {
186 &mut self.0
187 }
188}
189
190impl Deref for RpcClient {
191 type Target = GeneratedRpcClient;
192
193 fn deref(&self) -> &Self::Target {
194 &self.0
195 }
196}
197
198impl DerefMut for RemoteProverProxyStatusClient {
199 fn deref_mut(&mut self) -> &mut Self::Target {
200 &mut self.0
201 }
202}
203
204impl Deref for RemoteProverProxyStatusClient {
205 type Target = GeneratedProxyStatusClient;
206
207 fn deref(&self) -> &Self::Target {
208 &self.0
209 }
210}
211
212impl DerefMut for RemoteProverClient {
213 fn deref_mut(&mut self) -> &mut Self::Target {
214 &mut self.0
215 }
216}
217
218impl Deref for RemoteProverClient {
219 type Target = GeneratedProverClient;
220
221 fn deref(&self) -> &Self::Target {
222 &self.0
223 }
224}
225
226impl DerefMut for ValidatorClient {
227 fn deref_mut(&mut self) -> &mut Self::Target {
228 &mut self.0
229 }
230}
231
232impl Deref for ValidatorClient {
233 type Target = GeneratedValidatorClient;
234
235 fn deref(&self) -> &Self::Target {
236 &self.0
237 }
238}
239
240impl DerefMut for NtxBuilderClient {
241 fn deref_mut(&mut self) -> &mut Self::Target {
242 &mut self.0
243 }
244}
245
246impl Deref for NtxBuilderClient {
247 type Target = GeneratedNtxBuilderClient;
248
249 fn deref(&self) -> &Self::Target {
250 &self.0
251 }
252}
253
254impl DerefMut for SequencerClient {
255 fn deref_mut(&mut self) -> &mut Self::Target {
256 &mut self.0
257 }
258}
259
260impl Deref for SequencerClient {
261 type Target = GeneratedSequencerClient;
262
263 fn deref(&self) -> &Self::Target {
264 &self.0
265 }
266}
267
268pub trait GrpcClient {
273 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self;
274}
275
276impl GrpcClient for RpcClient {
277 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
278 Self(GeneratedRpcClient::new(InterceptedService::new(channel, interceptor)))
279 }
280}
281
282impl GrpcClient for RemoteProverProxyStatusClient {
283 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
284 Self(GeneratedProxyStatusClient::new(InterceptedService::new(channel, interceptor)))
285 }
286}
287
288impl GrpcClient for RemoteProverClient {
289 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
290 Self(GeneratedProverClient::new(InterceptedService::new(channel, interceptor)))
291 }
292}
293
294impl GrpcClient for ValidatorClient {
295 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
296 Self(GeneratedValidatorClient::new(InterceptedService::new(channel, interceptor)))
297 }
298}
299
300impl GrpcClient for NtxBuilderClient {
301 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
302 Self(GeneratedNtxBuilderClient::new(InterceptedService::new(channel, interceptor)))
303 }
304}
305
306impl GrpcClient for SequencerClient {
307 fn with_interceptor(channel: Channel, interceptor: Interceptor) -> Self {
308 Self(GeneratedSequencerClient::new(InterceptedService::new(channel, interceptor)))
309 }
310}
311
312#[derive(Clone, Debug)]
343pub struct Builder<State> {
344 endpoint: Endpoint,
345 metadata_version: Option<String>,
346 metadata_genesis: Option<String>,
347 metadata_auth_header_value: Option<AsciiMetadataValue>,
348 enable_otel: bool,
349 _state: PhantomData<State>,
350}
351
352#[derive(Copy, Clone, Debug)]
353pub struct WantsTls;
354#[derive(Copy, Clone, Debug)]
355pub struct WantsTimeout;
356#[derive(Copy, Clone, Debug)]
357pub struct WantsVersion;
358#[derive(Copy, Clone, Debug)]
359pub struct WantsGenesis;
360#[derive(Copy, Clone, Debug)]
361pub struct WantsOTel;
362#[derive(Copy, Clone, Debug)]
363pub struct WantsConnection;
364
365impl<State> Builder<State> {
366 fn next_state<Next>(self) -> Builder<Next> {
368 Builder {
369 endpoint: self.endpoint,
370 metadata_version: self.metadata_version,
371 metadata_genesis: self.metadata_genesis,
372 metadata_auth_header_value: self.metadata_auth_header_value,
373 enable_otel: self.enable_otel,
374 _state: PhantomData::<Next>,
375 }
376 }
377}
378
379const HTTP2_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(20);
381const HTTP2_KEEPALIVE_TIMEOUT: Duration = Duration::from_secs(10);
383const TCP_KEEPALIVE: Duration = Duration::from_secs(30);
385
386impl Builder<WantsTls> {
387 pub fn new(url: Url) -> Builder<WantsTls> {
390 let endpoint = Endpoint::from_shared(String::from(url))
391 .expect("Url type always results in valid endpoint")
392 .http2_keep_alive_interval(HTTP2_KEEPALIVE_INTERVAL)
395 .keep_alive_timeout(HTTP2_KEEPALIVE_TIMEOUT)
396 .keep_alive_while_idle(true)
397 .tcp_keepalive(Some(TCP_KEEPALIVE));
398
399 Builder {
400 endpoint,
401 metadata_version: None,
402 metadata_genesis: None,
403 metadata_auth_header_value: None,
404 enable_otel: false,
405 _state: PhantomData,
406 }
407 }
408
409 pub fn without_tls(self) -> Builder<WantsTimeout> {
411 self.next_state()
412 }
413
414 pub fn with_tls(mut self) -> Result<Builder<WantsTimeout>, TransportError> {
416 self.endpoint = self.endpoint.tls_config(ClientTlsConfig::new().with_native_roots())?;
417
418 Ok(self.next_state())
419 }
420}
421
422impl Builder<WantsTimeout> {
423 pub fn without_timeout(self) -> Builder<WantsVersion> {
425 self.next_state()
426 }
427
428 pub fn with_timeout(mut self, duration: Duration) -> Builder<WantsVersion> {
430 self.endpoint = self.endpoint.timeout(duration);
431 self.next_state()
432 }
433}
434
435impl Builder<WantsVersion> {
436 pub fn without_metadata_version(mut self) -> Builder<WantsGenesis> {
438 self.metadata_version = None;
439 self.next_state()
440 }
441
442 pub fn with_metadata_version(mut self, version: String) -> Builder<WantsGenesis> {
444 self.metadata_version = Some(version);
445 self.next_state()
446 }
447}
448
449impl Builder<WantsGenesis> {
450 pub fn without_metadata_genesis(mut self) -> Builder<WantsOTel> {
452 self.metadata_genesis = None;
453 self.next_state()
454 }
455
456 pub fn with_metadata_genesis(mut self, genesis: String) -> Builder<WantsOTel> {
458 self.metadata_genesis = Some(genesis);
459 self.next_state()
460 }
461}
462
463impl Builder<WantsOTel> {
464 #[must_use]
466 pub fn without_auth_header(mut self) -> Self {
467 self.metadata_auth_header_value = None;
468 self
469 }
470
471 #[must_use]
473 pub fn with_auth_header_value(mut self, value: AsciiMetadataValue) -> Self {
474 self.metadata_auth_header_value = Some(value);
475 self
476 }
477
478 pub fn with_otel_context_injection(mut self) -> Builder<WantsConnection> {
483 self.enable_otel = true;
484 self.next_state()
485 }
486
487 pub fn without_otel_context_injection(mut self) -> Builder<WantsConnection> {
490 self.enable_otel = false;
491 self.next_state()
492 }
493}
494
495impl Builder<WantsConnection> {
496 pub async fn connect<T>(self) -> Result<T, TransportError>
498 where
499 T: GrpcClient,
500 {
501 let channel = self.endpoint.connect().await?;
502 Ok(self.connect_with_channel::<T>(channel))
503 }
504
505 pub fn connect_lazy<T>(self) -> T
507 where
508 T: GrpcClient,
509 {
510 let channel = self.endpoint.connect_lazy();
511 self.connect_with_channel::<T>(channel)
512 }
513
514 fn connect_with_channel<T>(self, channel: Channel) -> T
515 where
516 T: GrpcClient,
517 {
518 let interceptor = Interceptor::new(
519 self.enable_otel,
520 self.metadata_version.as_deref(),
521 self.metadata_genesis.as_deref(),
522 self.metadata_auth_header_value,
523 );
524 T::with_interceptor(channel, interceptor)
525 }
526}
527
528impl ValidatorClient {
529 pub async fn submit_batch(
535 &mut self,
536 proposed_batch: &ProposedBatch,
537 transaction_inputs: &[Vec<u8>],
538 ) -> Result<(), Status> {
539 if proposed_batch.transactions().len() != transaction_inputs.len() {
540 return Err(Status::invalid_argument(
541 "transaction inputs do not match the batch's transactions",
542 ));
543 }
544 for (tx, inputs) in proposed_batch.transactions().iter().zip(transaction_inputs) {
545 let proven_tx = generated::transaction::ProvenTransaction {
546 transaction: tx.to_bytes(),
547 transaction_inputs: Some(inputs.clone()),
548 };
549 self.submit_proven_transaction(proven_tx).await?;
550 }
551 Ok(())
552 }
553}