miden_client/note_transport/
grpc.rs1use 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#[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#[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#[derive(Clone)]
88struct ConnectedClient {
89 client: MidenNoteTransportClient<Service>,
90 streaming_client: MidenNoteTransportClient<Service>,
91 health_client: HealthClient<Service>,
92}
93
94pub struct GrpcNoteTransportClient {
98 inner: RwLock<Option<ConnectedClient>>,
99 endpoint: String,
100 timeout_ms: u64,
101}
102
103impl GrpcNoteTransportClient {
104 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 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 async fn api(&self) -> Result<MidenNoteTransportClient<Service>, NoteTransportError> {
127 Ok(self.ensure_connected().await?.client)
128 }
129
130 async fn streaming_api(&self) -> Result<MidenNoteTransportClient<Service>, NoteTransportError> {
132 Ok(self.ensure_connected().await?.streaming_client)
133 }
134
135 async fn health_api(&self) -> Result<HealthClient<Service>, NoteTransportError> {
137 Ok(self.ensure_connected().await?.health_client)
138 }
139
140 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 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 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 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 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 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 pub async fn health_check(&mut self) -> Result<(), NoteTransportError> {
252 let request = tonic::Request::new(HealthCheckRequest {
253 service: String::new(), });
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
313pub struct NoteStreamAdapter {
315 inner: Streaming<StreamNotesUpdate>,
316}
317
318impl NoteStreamAdapter {
319 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 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 {}