Skip to main content

miden_client/rpc/domain/
note.rs

1use alloc::vec::Vec;
2
3use miden_protocol::block::{BlockHeader, BlockNumber};
4use miden_protocol::crypto::merkle::{MerklePath, SparseMerklePath};
5use miden_protocol::note::{
6    Note,
7    NoteAttachment,
8    NoteDetails,
9    NoteHeader,
10    NoteId,
11    NoteInclusionProof,
12    NoteMetadata,
13    NoteScript,
14    NoteTag,
15    NoteType,
16};
17use miden_protocol::{MastForest, MastNodeId, Word};
18use miden_tx::utils::Deserializable;
19
20use super::{MissingFieldHelper, RpcConversionError};
21use crate::rpc::{RpcError, generated as proto};
22
23impl From<NoteId> for proto::note::NoteId {
24    fn from(value: NoteId) -> Self {
25        proto::note::NoteId { id: Some(value.into()) }
26    }
27}
28
29impl TryFrom<proto::note::NoteId> for NoteId {
30    type Error = RpcConversionError;
31
32    fn try_from(value: proto::note::NoteId) -> Result<Self, Self::Error> {
33        let word =
34            Word::try_from(value.id.ok_or(proto::note::NoteId::missing_field(stringify!(id)))?)?;
35        Ok(Self::from_raw(word))
36    }
37}
38
39impl TryFrom<proto::note::NoteMetadata> for NoteMetadata {
40    type Error = RpcConversionError;
41
42    fn try_from(value: proto::note::NoteMetadata) -> Result<Self, Self::Error> {
43        let sender = value
44            .sender
45            .ok_or_else(|| proto::note::NoteMetadata::missing_field(stringify!(sender)))?
46            .try_into()?;
47        let note_type =
48            NoteType::try_from(u64::try_from(value.note_type).expect("invalid note type"))?;
49        let tag = NoteTag::new(value.tag);
50
51        // Deserialize attachment if present
52        let attachment = if value.attachment.is_empty() {
53            NoteAttachment::default()
54        } else {
55            NoteAttachment::read_from_bytes(&value.attachment)
56                .map_err(RpcConversionError::DeserializationError)?
57        };
58
59        Ok(NoteMetadata::new(sender, note_type).with_tag(tag).with_attachment(attachment))
60    }
61}
62
63impl From<NoteMetadata> for proto::note::NoteMetadata {
64    fn from(value: NoteMetadata) -> Self {
65        use miden_tx::utils::Serializable;
66        proto::note::NoteMetadata {
67            sender: Some(value.sender().into()),
68            note_type: value.note_type() as i32,
69            tag: value.tag().as_u32(),
70            attachment: value.attachment().to_bytes(),
71        }
72    }
73}
74
75impl TryFrom<proto::note::NoteInclusionInBlockProof> for NoteInclusionProof {
76    type Error = RpcConversionError;
77
78    fn try_from(value: proto::note::NoteInclusionInBlockProof) -> Result<Self, Self::Error> {
79        Ok(NoteInclusionProof::new(
80            value.block_num.into(),
81            u16::try_from(value.note_index_in_block)
82                .map_err(|_| RpcConversionError::InvalidField("NoteIndexInBlock".into()))?,
83            value
84                .inclusion_path
85                .ok_or_else(|| {
86                    proto::note::NoteInclusionInBlockProof::missing_field(stringify!(
87                        inclusion_path
88                    ))
89                })?
90                .try_into()?,
91        )?)
92    }
93}
94
95// SYNC NOTE
96// ================================================================================================
97
98/// Represents a `roto::rpc_store::SyncNotesResponse` with fields converted into domain types.
99#[derive(Debug)]
100pub struct NoteSyncInfo {
101    /// Number of the latest block in the chain.
102    pub chain_tip: BlockNumber,
103    /// Block header of the block with the first note matching the specified criteria.
104    pub block_header: BlockHeader,
105    /// Proof for block header's MMR with respect to the chain tip.
106    ///
107    /// More specifically, the full proof consists of `forest`, `position` and `path` components.
108    /// This value constitutes the `path`. The other two components can be obtained as follows:
109    ///    - `position` is simply `response.block_header.block_num`.
110    ///    - `forest` is the same as `response.chain_tip + 1`.
111    pub mmr_path: MerklePath,
112    /// List of all notes together with the Merkle paths from `response.block_header.note_root`.
113    pub notes: Vec<CommittedNote>,
114}
115
116impl TryFrom<proto::rpc::SyncNotesResponse> for NoteSyncInfo {
117    type Error = RpcError;
118
119    fn try_from(value: proto::rpc::SyncNotesResponse) -> Result<Self, Self::Error> {
120        let chain_tip = value
121            .pagination_info
122            .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(pagination_info)))?
123            .chain_tip;
124
125        // Validate and convert block header
126        let block_header = value
127            .block_header
128            .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(block_header)))?
129            .try_into()?;
130
131        let mmr_path = value
132            .mmr_path
133            .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(mmr_path)))?
134            .try_into()?;
135
136        // Validate and convert account note inclusions into an (AccountId, Word) tuple
137        let mut notes = vec![];
138        for note in value.notes {
139            let note_id: NoteId = note
140                .note_id
141                .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(notes.note_id)))?
142                .try_into()?;
143
144            let inclusion_path = note
145                .inclusion_path
146                .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(
147                    notes.inclusion_path
148                )))?
149                .try_into()?;
150
151            let metadata = note
152                .metadata
153                .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(notes.metadata)))?
154                .try_into()?;
155
156            let committed_note = CommittedNote::new(
157                note_id,
158                u16::try_from(note.note_index_in_block).map_err(|_| {
159                    RpcConversionError::InvalidField(
160                        "note_index_in_block value out of u16 range".into(),
161                    )
162                })?,
163                inclusion_path,
164                metadata,
165            );
166
167            notes.push(committed_note);
168        }
169
170        Ok(NoteSyncInfo {
171            chain_tip: chain_tip.into(),
172            block_header,
173            mmr_path,
174            notes,
175        })
176    }
177}
178
179// COMMITTED NOTE
180// ================================================================================================
181
182/// Represents a committed note, returned as part of a `SyncStateResponse`.
183#[derive(Debug, Clone)]
184pub struct CommittedNote {
185    /// Note ID of the committed note.
186    note_id: NoteId,
187    /// Note index for the note merkle tree.
188    note_index: u16,
189    /// Merkle path for the note merkle tree up to the block's note root.
190    inclusion_path: SparseMerklePath,
191    /// Note metadata.
192    metadata: NoteMetadata,
193}
194
195impl CommittedNote {
196    pub fn new(
197        note_id: NoteId,
198        note_index: u16,
199        inclusion_path: SparseMerklePath,
200        metadata: NoteMetadata,
201    ) -> Self {
202        Self {
203            note_id,
204            note_index,
205            inclusion_path,
206            metadata,
207        }
208    }
209
210    pub fn note_id(&self) -> &NoteId {
211        &self.note_id
212    }
213
214    pub fn note_index(&self) -> u16 {
215        self.note_index
216    }
217
218    pub fn inclusion_path(&self) -> &SparseMerklePath {
219        &self.inclusion_path
220    }
221
222    pub fn metadata(&self) -> NoteMetadata {
223        self.metadata.clone()
224    }
225}
226
227// FETCHED NOTE
228// ================================================================================================
229
230/// Describes the possible responses from the `GetNotesById` endpoint for a single note.
231#[allow(clippy::large_enum_variant)]
232pub enum FetchedNote {
233    /// Details for a private note only include its [`NoteHeader`] and [`NoteInclusionProof`].
234    /// Other details needed to consume the note are expected to be stored locally, off-chain.
235    Private(NoteHeader, NoteInclusionProof),
236    /// Contains the full [`Note`] object alongside its [`NoteInclusionProof`].
237    Public(Note, NoteInclusionProof),
238}
239
240impl FetchedNote {
241    /// Returns the note's inclusion details.
242    pub fn inclusion_proof(&self) -> &NoteInclusionProof {
243        match self {
244            FetchedNote::Private(_, inclusion_proof) | FetchedNote::Public(_, inclusion_proof) => {
245                inclusion_proof
246            },
247        }
248    }
249
250    /// Returns the note's metadata.
251    pub fn metadata(&self) -> &NoteMetadata {
252        match self {
253            FetchedNote::Private(header, _) => header.metadata(),
254            FetchedNote::Public(note, _) => note.metadata(),
255        }
256    }
257
258    /// Returns the note's ID.
259    pub fn id(&self) -> NoteId {
260        match self {
261            FetchedNote::Private(header, _) => header.id(),
262            FetchedNote::Public(note, _) => note.id(),
263        }
264    }
265}
266
267impl TryFrom<proto::note::CommittedNote> for FetchedNote {
268    type Error = RpcConversionError;
269
270    fn try_from(value: proto::note::CommittedNote) -> Result<Self, Self::Error> {
271        let inclusion_proof = value.inclusion_proof.ok_or_else(|| {
272            proto::note::CommittedNote::missing_field(stringify!(inclusion_proof))
273        })?;
274
275        let note_id: NoteId = inclusion_proof
276            .note_id
277            .ok_or_else(|| {
278                proto::note::CommittedNote::missing_field(stringify!(inclusion_proof.note_id))
279            })?
280            .try_into()?;
281
282        let inclusion_proof = NoteInclusionProof::try_from(inclusion_proof)?;
283
284        let note = value
285            .note
286            .ok_or_else(|| proto::note::CommittedNote::missing_field(stringify!(note)))?;
287
288        let metadata = note
289            .metadata
290            .ok_or_else(|| proto::note::CommittedNote::missing_field(stringify!(note.metadata)))?
291            .try_into()?;
292
293        if let Some(detail_bytes) = note.details {
294            let details = NoteDetails::read_from_bytes(&detail_bytes)?;
295            let (assets, recipient) = details.into_parts();
296
297            Ok(FetchedNote::Public(Note::new(assets, metadata, recipient), inclusion_proof))
298        } else {
299            let note_header = NoteHeader::new(note_id, metadata);
300            Ok(FetchedNote::Private(note_header, inclusion_proof))
301        }
302    }
303}
304
305// NOTE SCRIPT
306// ================================================================================================
307
308impl TryFrom<proto::note::NoteScript> for NoteScript {
309    type Error = RpcConversionError;
310
311    fn try_from(note_script: proto::note::NoteScript) -> Result<Self, Self::Error> {
312        let mast_forest = MastForest::read_from_bytes(&note_script.mast)?;
313        let entrypoint = MastNodeId::from_u32_safe(note_script.entrypoint, &mast_forest)?;
314        Ok(NoteScript::from_parts(alloc::sync::Arc::new(mast_forest), entrypoint))
315    }
316}