miden_client/rpc/domain/
note.rs

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