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