Skip to main content

miden_client/note_transport/
grpc.rs

1//! gRPC-based note transport client.
2//!
3//! On native targets, the connection is established lazily on the first request using a
4//! TLS-enabled `tonic` channel. On WASM, a `tonic_web_wasm_client` is created on demand.
5
6use alloc::boxed::Box;
7use alloc::string::String;
8use alloc::vec::Vec;
9use core::pin::Pin;
10use core::task::{Context, Poll};
11
12use futures::Stream;
13use miden_protocol::block::BlockNumber;
14use miden_protocol::note::{NoteHeader, NoteTag};
15use miden_protocol::utils::serde::{Deserializable, Serializable};
16use miden_tx::utils::sync::RwLock;
17use tonic::{Request, Streaming};
18use tonic_health::pb::HealthCheckRequest;
19use tonic_health::pb::health_client::HealthClient;
20#[cfg(target_arch = "wasm32")]
21use {core::time::Duration, tonic_web_wasm_client::options::FetchOptions};
22#[cfg(not(target_arch = "wasm32"))]
23use {
24    std::time::Duration,
25    tonic::transport::{Channel, ClientTlsConfig},
26};
27
28use super::generated::miden_note_transport::miden_note_transport_client::MidenNoteTransportClient;
29use super::generated::miden_note_transport::{
30    FetchNotesRequest,
31    SendNoteRequest,
32    StreamNotesRequest,
33    StreamNotesUpdate,
34    TransportNote,
35};
36use super::{NoteInfo, NoteStream, NoteTransportCursor, NoteTransportError};
37
38#[cfg(not(target_arch = "wasm32"))]
39type Service = Channel;
40#[cfg(target_arch = "wasm32")]
41type Service = tonic_web_wasm_client::Client;
42
43/// Establishes a connection to the note transport service with the configured channel timeout.
44#[cfg(not(target_arch = "wasm32"))]
45async fn connect_channel(
46    endpoint: &str,
47    timeout_ms: u64,
48) -> Result<ConnectedClient, NoteTransportError> {
49    let endpoint = tonic::transport::Endpoint::try_from(String::from(endpoint))
50        .map_err(|e| NoteTransportError::Connection(Box::new(e)))?
51        .timeout(Duration::from_millis(timeout_ms));
52    let tls = ClientTlsConfig::new().with_native_roots();
53    let channel = endpoint
54        .tls_config(tls)
55        .map_err(|e| NoteTransportError::Connection(Box::new(e)))?
56        .connect()
57        .await
58        .map_err(|e| NoteTransportError::Connection(Box::new(e)))?;
59    Ok(ConnectedClient {
60        client: MidenNoteTransportClient::new(channel.clone()),
61        streaming_client: MidenNoteTransportClient::new(channel.clone()),
62        health_client: HealthClient::new(channel),
63    })
64}
65
66/// Establishes note transport clients with timed unary requests and untimed streams.
67///
68/// Fetch timeouts include response bodies and would otherwise terminate long-lived streams.
69#[cfg(target_arch = "wasm32")]
70#[allow(clippy::unused_async)]
71async fn connect_channel(
72    endpoint: &str,
73    timeout_ms: u64,
74) -> Result<ConnectedClient, NoteTransportError> {
75    let fetch_options = FetchOptions::new().timeout(Duration::from_millis(timeout_ms));
76    let wasm_client =
77        tonic_web_wasm_client::Client::new_with_options(String::from(endpoint), fetch_options);
78    let streaming_wasm_client = tonic_web_wasm_client::Client::new(String::from(endpoint));
79    Ok(ConnectedClient {
80        client: MidenNoteTransportClient::new(wasm_client.clone()),
81        streaming_client: MidenNoteTransportClient::new(streaming_wasm_client),
82        health_client: HealthClient::new(wasm_client),
83    })
84}
85
86/// Inner state holding the connected gRPC clients.
87#[derive(Clone)]
88struct ConnectedClient {
89    client: MidenNoteTransportClient<Service>,
90    streaming_client: MidenNoteTransportClient<Service>,
91    health_client: HealthClient<Service>,
92}
93
94/// gRPC client for the note transport network.
95///
96/// The connection is established lazily on first use.
97pub struct GrpcNoteTransportClient {
98    inner: RwLock<Option<ConnectedClient>>,
99    endpoint: String,
100    timeout_ms: u64,
101}
102
103impl GrpcNoteTransportClient {
104    /// Creates a new [`GrpcNoteTransportClient`] without establishing a connection.
105    /// The connection will be established lazily on the first request.
106    pub fn new(endpoint: String, timeout_ms: u64) -> Self {
107        Self {
108            inner: RwLock::new(None),
109            endpoint,
110            timeout_ms,
111        }
112    }
113
114    /// Ensures the client is connected and returns the connected state.
115    async fn ensure_connected(&self) -> Result<ConnectedClient, NoteTransportError> {
116        if let Some(connected) = self.inner.read().as_ref() {
117            return Ok(connected.clone());
118        }
119
120        let connected = connect_channel(&self.endpoint, self.timeout_ms).await?;
121        *self.inner.write() = Some(connected.clone());
122        Ok(connected)
123    }
124
125    /// Get a clone of the main client, connecting if needed.
126    async fn api(&self) -> Result<MidenNoteTransportClient<Service>, NoteTransportError> {
127        Ok(self.ensure_connected().await?.client)
128    }
129
130    /// Gets a clone of the streaming client, connecting if needed.
131    async fn streaming_api(&self) -> Result<MidenNoteTransportClient<Service>, NoteTransportError> {
132        Ok(self.ensure_connected().await?.streaming_client)
133    }
134
135    /// Get a clone of the health client, connecting if needed.
136    async fn health_api(&self) -> Result<HealthClient<Service>, NoteTransportError> {
137        Ok(self.ensure_connected().await?.health_client)
138    }
139
140    /// Pushes a note to the note transport network.
141    ///
142    /// While the note header goes in plaintext, the provided note details can be encrypted.
143    pub async fn send_note(
144        &self,
145        header: NoteHeader,
146        details: Vec<u8>,
147    ) -> Result<(), NoteTransportError> {
148        self.send_note_inner(header, details, None).await
149    }
150
151    /// Pushes a note to the note transport network, relaying a block hint for the recipient.
152    ///
153    /// `block_hint` is forwarded to the server (as the `TransportNote`'s `after_block_num`) as the
154    /// block from which the recipient should start scanning for the note's commitment.
155    pub async fn send_note_with_block_hint(
156        &self,
157        header: NoteHeader,
158        details: Vec<u8>,
159        block_hint: BlockNumber,
160    ) -> Result<(), NoteTransportError> {
161        self.send_note_inner(header, details, Some(block_hint.as_u32())).await
162    }
163
164    /// Sends a note, passing the optional block hint straight through to the wire `TransportNote`.
165    async fn send_note_inner(
166        &self,
167        header: NoteHeader,
168        details: Vec<u8>,
169        after_block_num: Option<u32>,
170    ) -> Result<(), NoteTransportError> {
171        let request = SendNoteRequest {
172            note: Some(TransportNote {
173                header: header.to_bytes(),
174                details,
175                after_block_num,
176            }),
177        };
178
179        self.api()
180            .await?
181            .send_note(Request::new(request))
182            .await
183            .map_err(|e| NoteTransportError::Network(format!("Send note failed: {e:?}")))?;
184
185        Ok(())
186    }
187
188    /// Downloads notes for given tags from the note transport network.
189    ///
190    /// Returns notes labeled after the provided cursor (pagination), and an updated cursor.
191    pub async fn fetch_notes(
192        &self,
193        tags: &[NoteTag],
194        cursor: NoteTransportCursor,
195    ) -> Result<(Vec<NoteInfo>, NoteTransportCursor), NoteTransportError> {
196        let tags_int = tags.iter().map(NoteTag::as_u32).collect();
197        let request = FetchNotesRequest { tags: tags_int, cursor: cursor.value() };
198
199        let response = self
200            .api()
201            .await?
202            .fetch_notes(Request::new(request))
203            .await
204            .map_err(|e| NoteTransportError::Network(format!("Fetch notes failed: {e:?}")))?;
205
206        let response = response.into_inner();
207
208        // Convert protobuf notes to internal format and track the most recent received timestamp
209        let mut notes = Vec::new();
210
211        for pnote in response.notes {
212            let header = NoteHeader::read_from_bytes(&pnote.header)?;
213
214            notes.push(NoteInfo {
215                header,
216                details_bytes: pnote.details,
217                block_hint: pnote.after_block_num.map(BlockNumber::from),
218            });
219        }
220
221        Ok((notes, response.cursor.into()))
222    }
223
224    /// Stream notes from the note transport network.
225    ///
226    /// Subscribes to a given tag.
227    /// New notes are received periodically.
228    pub async fn stream_notes(
229        &self,
230        tag: NoteTag,
231        cursor: NoteTransportCursor,
232    ) -> Result<NoteStreamAdapter, NoteTransportError> {
233        let request = StreamNotesRequest {
234            tag: tag.as_u32(),
235            cursor: cursor.value(),
236        };
237
238        let response = self
239            .streaming_api()
240            .await?
241            .stream_notes(request)
242            .await
243            .map_err(|e| NoteTransportError::Network(format!("Stream notes failed: {e:?}")))?;
244        Ok(NoteStreamAdapter::new(response.into_inner()))
245    }
246
247    /// gRPC-standardized server health-check.
248    ///
249    /// Checks if the note transport node and respective gRPC services are serving requests.
250    /// Currently the grPC server operates only one service labelled `MidenNoteTransport`.
251    pub async fn health_check(&mut self) -> Result<(), NoteTransportError> {
252        let request = tonic::Request::new(HealthCheckRequest {
253            service: String::new(), // empty string -> whole server
254        });
255
256        let response = self
257            .health_api()
258            .await?
259            .check(request)
260            .await
261            .map_err(|e| NoteTransportError::Network(format!("Health check failed: {e}")))?
262            .into_inner();
263
264        let serving = matches!(
265            response.status(),
266            tonic_health::pb::health_check_response::ServingStatus::Serving
267        );
268
269        serving
270            .then_some(())
271            .ok_or_else(|| NoteTransportError::Network("Service is not serving".into()))
272    }
273}
274
275#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
276#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
277impl super::NoteTransportClient for GrpcNoteTransportClient {
278    async fn send_note(
279        &self,
280        header: NoteHeader,
281        details: Vec<u8>,
282    ) -> Result<(), NoteTransportError> {
283        self.send_note(header, details).await
284    }
285
286    async fn send_note_with_block_hint(
287        &self,
288        header: NoteHeader,
289        details: Vec<u8>,
290        block_hint: BlockNumber,
291    ) -> Result<(), NoteTransportError> {
292        self.send_note_with_block_hint(header, details, block_hint).await
293    }
294
295    async fn fetch_notes(
296        &self,
297        tags: &[NoteTag],
298        cursor: NoteTransportCursor,
299    ) -> Result<(Vec<NoteInfo>, NoteTransportCursor), NoteTransportError> {
300        self.fetch_notes(tags, cursor).await
301    }
302
303    async fn stream_notes(
304        &self,
305        tag: NoteTag,
306        cursor: NoteTransportCursor,
307    ) -> Result<Box<dyn NoteStream>, NoteTransportError> {
308        let stream = self.stream_notes(tag, cursor).await?;
309        Ok(Box::new(stream))
310    }
311}
312
313/// Convert from `tonic::Streaming<StreamNotesUpdate>` to [`NoteStream`]
314pub struct NoteStreamAdapter {
315    inner: Streaming<StreamNotesUpdate>,
316}
317
318impl NoteStreamAdapter {
319    /// Create a new [`NoteStreamAdapter`]
320    pub fn new(stream: Streaming<StreamNotesUpdate>) -> Self {
321        Self { inner: stream }
322    }
323}
324
325impl Stream for NoteStreamAdapter {
326    type Item = Result<Vec<NoteInfo>, NoteTransportError>;
327
328    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
329        match Pin::new(&mut self.inner).poll_next(cx) {
330            Poll::Ready(Some(Ok(update))) => {
331                // Convert StreamNotesUpdate to Vec<NoteInfo>
332                let mut notes = Vec::new();
333                for pnote in update.notes {
334                    let header = NoteHeader::read_from_bytes(&pnote.header)?;
335
336                    notes.push(NoteInfo {
337                        header,
338                        details_bytes: pnote.details,
339                        block_hint: pnote.after_block_num.map(BlockNumber::from),
340                    });
341                }
342                Poll::Ready(Some(Ok(notes)))
343            },
344            Poll::Ready(Some(Err(status))) => Poll::Ready(Some(Err(NoteTransportError::Network(
345                format!("tonic status: {status}"),
346            )))),
347            Poll::Ready(None) => Poll::Ready(None),
348            Poll::Pending => Poll::Pending,
349        }
350    }
351}
352
353impl NoteStream for NoteStreamAdapter {}