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