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::collections::BTreeMap;
8use alloc::sync::Arc;
9use alloc::vec::Vec;
10
11use futures::Stream;
12use miden_lib::utils::Serializable;
13use miden_objects::address::Address;
14use miden_objects::note::{Note, NoteDetails, NoteHeader, NoteId, NoteInclusionProof, NoteTag};
15use miden_tx::auth::TransactionAuthenticator;
16use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, SliceReader};
17
18pub use self::errors::NoteTransportError;
19use crate::store::{InputNoteRecord, Store};
20use crate::{Client, ClientError};
21
22pub const NOTE_TRANSPORT_DEFAULT_ENDPOINT: &str = "http://localhost:57292";
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();
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        let update = self.fetch_note_transport_update(cursor, note_tags).await?;
87
88        self.store.apply_note_transport_update(update).await?;
89
90        Ok(())
91    }
92
93    /// Fetches all notes for tracked note tags.
94    ///
95    /// Similar to [`Client::fetch_private_notes`] however does not employ pagination,
96    /// fetching all notes stored in the note transport network for the tracked tags.
97    /// Please prefer using [`Client::fetch_private_notes`] to avoid downloading repeated notes.
98    pub async fn fetch_all_private_notes(&mut self) -> Result<(), ClientError> {
99        let note_tags = self.store.get_unique_note_tags().await?;
100
101        let update =
102            self.fetch_note_transport_update(NoteTransportCursor::init(), note_tags).await?;
103
104        self.store.apply_note_transport_update(update).await?;
105
106        Ok(())
107    }
108
109    /// Fetch notes from the note transport network for provided note tags
110    ///
111    /// A [`NoteTransportUpdate`] is created containing the downloaded notes.
112    /// Pagination is employed, where only notes after the provided cursor are requested.
113    pub(crate) async fn fetch_note_transport_update<I>(
114        &mut self,
115        cursor: NoteTransportCursor,
116        tags: I,
117    ) -> Result<NoteTransportUpdate, ClientError>
118    where
119        I: IntoIterator<Item = NoteTag>,
120    {
121        let mut tnotes = BTreeMap::new();
122        // Fetch notes
123        let (note_infos, rcursor) = self
124            .get_note_transport_api()?
125            .fetch_notes(&tags.into_iter().collect::<Vec<_>>(), cursor)
126            .await?;
127        for note_info in &note_infos {
128            // e2ee impl hint:
129            // for key in self.store.decryption_keys() try
130            // key.decrypt(details_bytes_encrypted)
131            let tnote = rejoin_note(&note_info.header, &note_info.details_bytes)?;
132            tnotes.insert(tnote.id(), tnote);
133        }
134
135        // Retrieve inclusion proofs from the Miden node
136        let ids = tnotes.keys().copied().collect::<Vec<_>>();
137        let nnotes = self.rpc_api.get_notes_by_id(&ids).await?;
138
139        // Create `NoteInputRecord`s of the transport-fetched notes and the node-retrieved proofs
140        let mut note_updates = Vec::with_capacity(tnotes.len());
141        for nnote in nnotes {
142            if let Some(tnote) = tnotes.get(&nnote.id()) {
143                // Try to combine the transport-fetched note with the node-retrieved proof
144                let opt_input_note_record = self
145                    .combine_transport_note_and_proof(
146                        tnote.clone(),
147                        nnote.id(),
148                        nnote.inclusion_proof().clone(),
149                    )
150                    .await;
151                if let Some(input_note_record) = opt_input_note_record {
152                    tnotes.remove(&nnote.id());
153                    note_updates.push(input_note_record);
154                }
155            }
156        }
157
158        // Keep the remaining transport notes which inclusion proofs were not retrieved
159        for (_id, tnote) in tnotes {
160            let input_note_record = InputNoteRecord::from(tnote);
161            note_updates.push(input_note_record);
162        }
163
164        let update = NoteTransportUpdate { note_updates, cursor: rcursor };
165
166        Ok(update)
167    }
168
169    /// Combines a transport-fetched note with a node-retrieved proof into a note record
170    async fn combine_transport_note_and_proof(
171        &self,
172        tnote: Note,
173        note_id: NoteId,
174        inclusion_proof: NoteInclusionProof,
175    ) -> Option<InputNoteRecord> {
176        // Update existing note, if it exists
177        let previous_note = self.get_input_note(note_id).await.ok()?;
178        // Build note record from the transport-fetch note and node-retrieved proof
179        self.import_note_record_by_proof(previous_note, tnote, inclusion_proof)
180            .await
181            .ok()?
182    }
183}
184
185/// Populates the note transport cursor setting with 0, if it is not setup
186pub(crate) async fn init_note_transport_cursor(store: Arc<dyn Store>) -> Result<(), ClientError> {
187    let setting = NOTE_TRANSPORT_CURSOR_STORE_SETTING;
188    if store.get_setting(setting.into()).await?.is_none() {
189        let initial_cursor = 0u64.to_be_bytes().to_vec();
190        store.set_setting(setting.into(), initial_cursor).await?;
191    }
192    Ok(())
193}
194
195/// Note transport cursor
196///
197/// Pagination integer used to reduce the number of fetched notes from the note transport network,
198/// avoiding duplicate downloads.
199#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord)]
200pub struct NoteTransportCursor(u64);
201
202/// Note Transport update
203pub struct NoteTransportUpdate {
204    /// Pagination cursor for next fetch
205    pub cursor: NoteTransportCursor,
206    /// Fetched notes
207    pub note_updates: Vec<InputNoteRecord>,
208}
209
210impl NoteTransportCursor {
211    pub fn new(value: u64) -> Self {
212        Self(value)
213    }
214
215    pub fn init() -> Self {
216        Self::new(0)
217    }
218
219    pub fn value(&self) -> u64 {
220        self.0
221    }
222}
223
224impl From<u64> for NoteTransportCursor {
225    fn from(value: u64) -> Self {
226        Self::new(value)
227    }
228}
229
230/// The main transport client trait for sending and receiving encrypted notes
231#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
232#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
233pub trait NoteTransportClient: Send + Sync {
234    /// Send a note with optionally encrypted details
235    async fn send_note(
236        &self,
237        header: NoteHeader,
238        details: Vec<u8>,
239    ) -> Result<(), NoteTransportError>;
240
241    /// Fetch notes for given tags
242    ///
243    /// Downloads notes for given tags.
244    /// Returns notes labelled after the provided cursor (pagination), and an updated cursor.
245    async fn fetch_notes(
246        &self,
247        tag: &[NoteTag],
248        cursor: NoteTransportCursor,
249    ) -> Result<(Vec<NoteInfo>, NoteTransportCursor), NoteTransportError>;
250
251    /// Stream notes for a given tag
252    async fn stream_notes(
253        &self,
254        tag: NoteTag,
255        cursor: NoteTransportCursor,
256    ) -> Result<Box<dyn NoteStream>, NoteTransportError>;
257}
258
259/// Stream trait for note streaming
260pub trait NoteStream:
261    Stream<Item = Result<Vec<NoteInfo>, NoteTransportError>> + Send + Unpin
262{
263}
264
265/// Information about a note fetched from the note transport network
266#[derive(Debug, Clone)]
267pub struct NoteInfo {
268    /// Note header
269    pub header: NoteHeader,
270    /// Note details, can be encrypted
271    pub details_bytes: Vec<u8>,
272}
273
274// SERIALIZATION
275// ================================================================================================
276
277impl Serializable for NoteInfo {
278    fn write_into<W: ByteWriter>(&self, target: &mut W) {
279        self.header.write_into(target);
280        self.details_bytes.write_into(target);
281    }
282}
283
284impl Deserializable for NoteInfo {
285    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
286        let header = NoteHeader::read_from(source)?;
287        let details_bytes = Vec::<u8>::read_from(source)?;
288        Ok(NoteInfo { header, details_bytes })
289    }
290}
291
292impl Serializable for NoteTransportCursor {
293    fn write_into<W: ByteWriter>(&self, target: &mut W) {
294        self.0.write_into(target);
295    }
296}
297
298impl Deserializable for NoteTransportCursor {
299    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
300        let value = u64::read_from(source)?;
301        Ok(Self::new(value))
302    }
303}
304
305fn rejoin_note(header: &NoteHeader, details_bytes: &[u8]) -> Result<Note, DeserializationError> {
306    let mut reader = SliceReader::new(details_bytes);
307    let details = NoteDetails::read_from(&mut reader)?;
308    let metadata = *header.metadata();
309    Ok(Note::new(details.assets().clone(), metadata, details.recipient().clone()))
310}