Skip to main content

miden_client/note_transport/
mod.rs

1pub mod errors;
2pub mod generated;
3#[cfg(feature = "tonic")]
4pub mod grpc;
5
6use alloc::boxed::Box;
7use alloc::sync::Arc;
8use alloc::vec::Vec;
9
10use futures::Stream;
11use miden_protocol::address::Address;
12use miden_protocol::block::BlockNumber;
13use miden_protocol::note::{Note, NoteDetails, NoteFile, NoteHeader, NoteTag};
14use miden_protocol::utils::Serializable;
15use miden_tx::auth::TransactionAuthenticator;
16use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, SliceReader};
17
18pub use self::errors::NoteTransportError;
19use crate::store::Store;
20use crate::{Client, ClientError};
21
22pub const NOTE_TRANSPORT_DEFAULT_ENDPOINT: &str = "https://transport.miden.io";
23pub const NOTE_TRANSPORT_CURSOR_STORE_SETTING: &str = "note_transport_cursor";
24
25/// Client note transport methods.
26impl<AUTH> Client<AUTH> {
27    /// Check if note transport connection is configured
28    pub fn is_note_transport_enabled(&self) -> bool {
29        self.note_transport_api.is_some()
30    }
31
32    /// Returns the Note Transport client
33    ///
34    /// Errors if the note transport is not configured.
35    pub(crate) fn get_note_transport_api(
36        &self,
37    ) -> Result<Arc<dyn NoteTransportClient>, NoteTransportError> {
38        self.note_transport_api.clone().ok_or(NoteTransportError::Disabled)
39    }
40
41    /// Send a note through the note transport network.
42    ///
43    /// The note will be end-to-end encrypted (unimplemented, currently plaintext)
44    /// using the provided recipient's `address` details.
45    /// The recipient will be able to retrieve this note through the note's [`NoteTag`].
46    pub async fn send_private_note(
47        &mut self,
48        note: Note,
49        _address: &Address,
50    ) -> Result<(), ClientError> {
51        let api = self.get_note_transport_api()?;
52
53        let header = note.header().clone();
54        let details = NoteDetails::from(note);
55        let details_bytes = details.to_bytes();
56        // e2ee impl hint:
57        // address.key().encrypt(details_bytes)
58        api.send_note(header, details_bytes).await?;
59
60        Ok(())
61    }
62}
63
64impl<AUTH> Client<AUTH>
65where
66    AUTH: TransactionAuthenticator + Sync + 'static,
67{
68    /// Fetch notes for tracked note tags.
69    ///
70    /// The client will query the configured note transport node for all tracked note tags.
71    /// To list tracked tags please use [`Client::get_note_tags`]. To add a new note tag please use
72    /// [`Client::add_note_tag`].
73    /// Only notes directed at your addresses will be stored and readable given the use of
74    /// end-to-end encryption (unimplemented).
75    /// Fetched notes will be stored into the client's store.
76    ///
77    /// An internal pagination mechanism is employed to reduce the number of downloaded notes.
78    /// To fetch the full history of private notes for the tracked tags, use
79    /// [`Client::fetch_all_private_notes`].
80    pub async fn fetch_private_notes(&mut self) -> Result<(), ClientError> {
81        // Unique tags
82        let note_tags = self.store.get_unique_note_tags().await?;
83        // Get global cursor
84        let cursor = self.store.get_note_transport_cursor().await?;
85
86        self.fetch_transport_notes(cursor, note_tags).await?;
87
88        Ok(())
89    }
90
91    /// Fetches all notes for tracked note tags.
92    ///
93    /// Similar to [`Client::fetch_private_notes`] however does not employ pagination,
94    /// fetching all notes stored in the note transport network for the tracked tags.
95    /// Please prefer using [`Client::fetch_private_notes`] to avoid downloading repeated notes.
96    pub async fn fetch_all_private_notes(&mut self) -> Result<(), ClientError> {
97        let note_tags = self.store.get_unique_note_tags().await?;
98
99        self.fetch_transport_notes(NoteTransportCursor::init(), note_tags).await?;
100
101        Ok(())
102    }
103
104    /// Fetch notes from the note transport network for provided note tags
105    ///
106    /// Pagination is employed, where only notes after the provided cursor are requested.
107    /// Downloaded notes are imported.
108    pub(crate) async fn fetch_transport_notes<I>(
109        &mut self,
110        cursor: NoteTransportCursor,
111        tags: I,
112    ) -> Result<(), ClientError>
113    where
114        I: IntoIterator<Item = NoteTag>,
115    {
116        // Number of blocks to look back from sync height when scanning for committed notes.
117        // Handles the race where a note is committed on-chain just before the NTL delivers
118        // its data — without this, check_expected_notes would scan from sync_height forward
119        // and miss the already-committed note.
120        const NOTE_LOOKBACK_BLOCKS: u32 = 20;
121
122        let mut notes = Vec::new();
123        // Fetch notes
124        let (note_infos, rcursor) = self
125            .get_note_transport_api()?
126            .fetch_notes(&tags.into_iter().collect::<Vec<_>>(), cursor)
127            .await?;
128        for note_info in &note_infos {
129            // e2ee impl hint:
130            // for key in self.store.decryption_keys() try
131            // key.decrypt(details_bytes_encrypted)
132            let note = rejoin_note(&note_info.header, &note_info.details_bytes)?;
133            notes.push(note);
134        }
135
136        let sync_height = self.get_sync_height().await?;
137        let after_block_num =
138            BlockNumber::from(sync_height.as_u32().saturating_sub(NOTE_LOOKBACK_BLOCKS));
139
140        // Import fetched notes
141        let mut note_requests = Vec::with_capacity(notes.len());
142        for note in notes {
143            let tag = note.metadata().tag();
144            let note_file = NoteFile::NoteDetails {
145                details: note.into(),
146                after_block_num,
147                tag: Some(tag),
148            };
149            note_requests.push(note_file);
150        }
151        self.import_notes(&note_requests).await?;
152
153        // Update cursor (pagination)
154        self.store.update_note_transport_cursor(rcursor).await?;
155
156        Ok(())
157    }
158}
159
160/// Populates the note transport cursor setting with 0, if it is not setup
161pub(crate) async fn init_note_transport_cursor(store: Arc<dyn Store>) -> Result<(), ClientError> {
162    let setting = NOTE_TRANSPORT_CURSOR_STORE_SETTING;
163    if store.get_setting(setting.into()).await?.is_none() {
164        let initial_cursor = 0u64.to_be_bytes().to_vec();
165        store.set_setting(setting.into(), initial_cursor).await?;
166    }
167    Ok(())
168}
169
170/// Note transport cursor
171///
172/// Pagination integer used to reduce the number of fetched notes from the note transport network,
173/// avoiding duplicate downloads.
174#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)]
175pub struct NoteTransportCursor(u64);
176
177/// Note Transport update
178pub struct NoteTransportUpdate {
179    /// Pagination cursor for next fetch
180    pub cursor: NoteTransportCursor,
181    /// Fetched notes
182    pub notes: Vec<Note>,
183}
184
185impl NoteTransportCursor {
186    pub fn new(value: u64) -> Self {
187        Self(value)
188    }
189
190    pub fn init() -> Self {
191        Self::new(0)
192    }
193
194    pub fn value(&self) -> u64 {
195        self.0
196    }
197}
198
199impl From<u64> for NoteTransportCursor {
200    fn from(value: u64) -> Self {
201        Self::new(value)
202    }
203}
204
205/// The main transport client trait for sending and receiving encrypted notes
206#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
207#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
208pub trait NoteTransportClient: Send + Sync {
209    /// Send a note with optionally encrypted details
210    async fn send_note(
211        &self,
212        header: NoteHeader,
213        details: Vec<u8>,
214    ) -> Result<(), NoteTransportError>;
215
216    /// Fetch notes for given tags
217    ///
218    /// Downloads notes for given tags.
219    /// Returns notes labelled after the provided cursor (pagination), and an updated cursor.
220    async fn fetch_notes(
221        &self,
222        tag: &[NoteTag],
223        cursor: NoteTransportCursor,
224    ) -> Result<(Vec<NoteInfo>, NoteTransportCursor), NoteTransportError>;
225
226    /// Stream notes for a given tag
227    async fn stream_notes(
228        &self,
229        tag: NoteTag,
230        cursor: NoteTransportCursor,
231    ) -> Result<Box<dyn NoteStream>, NoteTransportError>;
232}
233
234/// Stream trait for note streaming
235pub trait NoteStream:
236    Stream<Item = Result<Vec<NoteInfo>, NoteTransportError>> + Send + Unpin
237{
238}
239
240/// Information about a note fetched from the note transport network
241#[derive(Debug, Clone)]
242pub struct NoteInfo {
243    /// Note header
244    pub header: NoteHeader,
245    /// Note details, can be encrypted
246    pub details_bytes: Vec<u8>,
247}
248
249// SERIALIZATION
250// ================================================================================================
251
252impl Serializable for NoteInfo {
253    fn write_into<W: ByteWriter>(&self, target: &mut W) {
254        self.header.write_into(target);
255        self.details_bytes.write_into(target);
256    }
257}
258
259impl Deserializable for NoteInfo {
260    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
261        let header = NoteHeader::read_from(source)?;
262        let details_bytes = Vec::<u8>::read_from(source)?;
263        Ok(NoteInfo { header, details_bytes })
264    }
265}
266
267impl Serializable for NoteTransportCursor {
268    fn write_into<W: ByteWriter>(&self, target: &mut W) {
269        self.0.write_into(target);
270    }
271}
272
273impl Deserializable for NoteTransportCursor {
274    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
275        let value = u64::read_from(source)?;
276        Ok(Self::new(value))
277    }
278}
279
280fn rejoin_note(header: &NoteHeader, details_bytes: &[u8]) -> Result<Note, DeserializationError> {
281    let mut reader = SliceReader::new(details_bytes);
282    let details = NoteDetails::read_from(&mut reader)?;
283    let metadata = header.metadata().clone();
284    Ok(Note::new(details.assets().clone(), metadata, details.recipient().clone()))
285}