miden_client/rpc/domain/
note.rs

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