Skip to main content

miden_client/rpc/domain/
note.rs

1use alloc::collections::BTreeMap;
2use alloc::vec::Vec;
3
4use miden_protocol::account::AccountId;
5use miden_protocol::block::BlockHeader;
6use miden_protocol::crypto::merkle::MerklePath;
7use miden_protocol::note::{
8    Note,
9    NoteAttachmentHeader,
10    NoteAttachmentScheme,
11    NoteAttachments,
12    NoteDetails,
13    NoteDetailsCommitment,
14    NoteHeader,
15    NoteId,
16    NoteInclusionProof,
17    NoteMetadata,
18    NoteScript,
19    NoteTag,
20    NoteType,
21    PartialNoteMetadata,
22};
23use miden_protocol::{MastForest, MastNodeId, Word};
24use miden_tx::utils::serde::Deserializable;
25
26use super::{MissingFieldHelper, RpcConversionError};
27use crate::rpc::{RpcError, generated as proto};
28
29impl From<NoteId> for proto::note::NoteId {
30    fn from(value: NoteId) -> Self {
31        proto::note::NoteId { id: Some(value.into()) }
32    }
33}
34
35impl TryFrom<proto::note::NoteId> for NoteId {
36    type Error = RpcConversionError;
37
38    fn try_from(value: proto::note::NoteId) -> Result<Self, Self::Error> {
39        let word =
40            Word::try_from(value.id.ok_or(proto::note::NoteId::missing_field(stringify!(id)))?)?;
41        Ok(Self::from_raw(word))
42    }
43}
44
45fn note_type_from_proto(raw: i32) -> Result<NoteType, RpcConversionError> {
46    let proto_note_type = proto::note::NoteType::try_from(raw)
47        .map_err(|_| RpcConversionError::InvalidField(alloc::format!("note_type={raw}")))?;
48    match proto_note_type {
49        proto::note::NoteType::Public => Ok(NoteType::Public),
50        proto::note::NoteType::Private => Ok(NoteType::Private),
51        proto::note::NoteType::Unspecified => {
52            Err(RpcConversionError::InvalidField("note_type=NOTE_TYPE_UNSPECIFIED".into()))
53        },
54    }
55}
56
57fn note_type_to_proto(note_type: NoteType) -> i32 {
58    let proto_note_type = match note_type {
59        NoteType::Public => proto::note::NoteType::Public,
60        NoteType::Private => proto::note::NoteType::Private,
61    };
62    proto_note_type as i32
63}
64
65/// Decodes the `attachment_schemes` slice from a proto `NoteMetadata` into the fixed-size header
66/// array expected by [`NoteMetadata::from_parts`]. Trailing absent slots may be omitted on the
67/// wire; we pad with absent headers to reach the protocol's `NoteAttachments::MAX_COUNT`.
68fn attachment_headers_from_proto(
69    schemes: &[u32],
70) -> Result<[NoteAttachmentHeader; NoteAttachments::MAX_COUNT], RpcConversionError> {
71    if schemes.len() > NoteAttachments::MAX_COUNT {
72        return Err(RpcConversionError::InvalidField(alloc::format!(
73            "attachment_schemes length {} exceeds NoteAttachments::MAX_COUNT",
74            schemes.len(),
75        )));
76    }
77    let mut headers = [NoteAttachmentHeader::absent(); NoteAttachments::MAX_COUNT];
78    for (slot, raw) in schemes.iter().enumerate() {
79        if *raw == 0 {
80            continue;
81        }
82        let raw_u16 = u16::try_from(*raw).map_err(|_| {
83            RpcConversionError::InvalidField(alloc::format!(
84                "attachment_schemes[{slot}]={raw} does not fit in u16",
85            ))
86        })?;
87        let scheme = NoteAttachmentScheme::new(raw_u16).map_err(|err| {
88            RpcConversionError::InvalidField(alloc::format!("attachment_schemes[{slot}]: {err}"))
89        })?;
90        headers[slot] = NoteAttachmentHeader::new(scheme);
91    }
92    Ok(headers)
93}
94
95fn attachment_schemes_to_proto(
96    headers: &[NoteAttachmentHeader; NoteAttachments::MAX_COUNT],
97) -> Vec<u32> {
98    // Encode each header as the scheme value, with `0` meaning absent. Trailing absent slots
99    // are stripped to match the wire convention.
100    let mut encoded: Vec<u32> = headers
101        .iter()
102        .map(|h| h.scheme().map_or(0, |s| u32::from(s.as_u16())))
103        .collect();
104    while matches!(encoded.last(), Some(0)) {
105        encoded.pop();
106    }
107    encoded
108}
109
110impl TryFrom<proto::note::NoteMetadata> for NoteMetadata {
111    type Error = RpcConversionError;
112
113    fn try_from(value: proto::note::NoteMetadata) -> Result<Self, Self::Error> {
114        let partial_metadata: PartialNoteMetadata = (&value).try_into()?;
115        let attachment_headers = attachment_headers_from_proto(&value.attachment_schemes)?;
116        let attachments_commitment = value
117            .attachments_commitment
118            .ok_or_else(|| {
119                proto::note::NoteMetadata::missing_field(stringify!(attachments_commitment))
120            })?
121            .try_into()?;
122
123        Ok(NoteMetadata::from_parts(
124            partial_metadata,
125            attachment_headers,
126            attachments_commitment,
127        ))
128    }
129}
130
131impl TryFrom<&proto::note::NoteMetadata> for PartialNoteMetadata {
132    type Error = RpcConversionError;
133
134    fn try_from(value: &proto::note::NoteMetadata) -> Result<Self, Self::Error> {
135        let sender = value
136            .sender
137            .clone()
138            .ok_or_else(|| proto::note::NoteMetadata::missing_field(stringify!(sender)))?
139            .try_into()?;
140        let note_type = note_type_from_proto(value.note_type)?;
141        let tag = NoteTag::new(value.tag);
142
143        Ok(PartialNoteMetadata::new(sender, note_type).with_tag(tag))
144    }
145}
146
147impl From<NoteMetadata> for proto::note::NoteMetadata {
148    fn from(value: NoteMetadata) -> Self {
149        proto::note::NoteMetadata {
150            sender: Some(value.sender().into()),
151            note_type: note_type_to_proto(value.note_type()),
152            tag: value.tag().as_u32(),
153            attachment_schemes: attachment_schemes_to_proto(value.attachment_headers()),
154            attachments_commitment: Some(value.attachments_commitment().into()),
155        }
156    }
157}
158
159impl TryFrom<proto::note::NoteHeader> for NoteHeader {
160    type Error = RpcConversionError;
161
162    fn try_from(value: proto::note::NoteHeader) -> Result<Self, Self::Error> {
163        let details_commitment_word: Word = value
164            .details_commitment
165            .ok_or(proto::note::NoteHeader::missing_field(stringify!(details_commitment)))?
166            .try_into()?;
167        let metadata = value
168            .metadata
169            .ok_or(proto::note::NoteHeader::missing_field(stringify!(metadata)))?
170            .try_into()?;
171        Ok(NoteHeader::new(
172            NoteDetailsCommitment::from_raw(details_commitment_word),
173            metadata,
174        ))
175    }
176}
177
178impl TryFrom<proto::note::NoteInclusionInBlockProof> for NoteInclusionProof {
179    type Error = RpcConversionError;
180
181    fn try_from(value: proto::note::NoteInclusionInBlockProof) -> Result<Self, Self::Error> {
182        Ok(NoteInclusionProof::new(
183            value.block_num.into(),
184            u16::try_from(value.note_index_in_block)
185                .map_err(|_| RpcConversionError::InvalidField("NoteIndexInBlock".into()))?,
186            value
187                .inclusion_path
188                .ok_or_else(|| {
189                    proto::note::NoteInclusionInBlockProof::missing_field(stringify!(
190                        inclusion_path
191                    ))
192                })?
193                .try_into()?,
194        )?)
195    }
196}
197
198// SYNC NOTE
199// ================================================================================================
200
201/// Represents a single block's worth of note sync data from the `SyncNotesResponse`.
202#[derive(Debug, Clone)]
203pub struct NoteSyncBlock {
204    /// Block header containing the matching notes.
205    pub block_header: BlockHeader,
206    /// MMR path for verifying the block's inclusion in the MMR at `block_to`.
207    pub mmr_path: MerklePath,
208    /// Notes matching the requested tags in this block, keyed by note ID.
209    pub notes: BTreeMap<NoteId, CommittedNote>,
210}
211
212/// Content resolved for a single note during
213/// [`NodeRpcClient::sync_notes_with_details`](crate::rpc::NodeRpcClient::sync_notes_with_details)
214/// as response from `GetNotesById`.
215#[allow(clippy::large_enum_variant)]
216pub enum SyncedNoteDetails {
217    /// A public note's full body.
218    Public(Note),
219    /// A private note's attachment content, if it carries any. Private notes expose no on-chain
220    /// body; only their attachments are resolved.
221    Private(Option<NoteAttachments>),
222}
223
224impl TryFrom<proto::rpc::sync_notes_response::NoteSyncBlock> for NoteSyncBlock {
225    type Error = RpcError;
226
227    fn try_from(
228        block: proto::rpc::sync_notes_response::NoteSyncBlock,
229    ) -> Result<Self, Self::Error> {
230        let block_header = block
231            .block_header
232            .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(blocks.block_header)))?
233            .try_into()?;
234
235        let mmr_path = block
236            .mmr_path
237            .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(blocks.mmr_path)))?
238            .try_into()?;
239
240        let notes: BTreeMap<NoteId, CommittedNote> = block
241            .notes
242            .into_iter()
243            .map(|n| {
244                let note = CommittedNote::try_from(n)?;
245                Ok((*note.note_id(), note))
246            })
247            .collect::<Result<_, RpcConversionError>>()?;
248
249        Ok(NoteSyncBlock { block_header, mmr_path, notes })
250    }
251}
252
253// COMMITTED NOTE
254// ================================================================================================
255
256/// Represents a committed note, returned as part of a `SyncNotesResponse`.
257#[derive(Debug, Clone)]
258pub struct CommittedNote {
259    /// Note ID of the committed note.
260    note_id: NoteId,
261    /// Note metadata. Sync responses always carry the full [`NoteMetadata`] (header fields plus
262    /// attachment scheme markers and the attachments commitment); attachment **content** is
263    /// fetched separately via `GetNotesById`.
264    metadata: NoteMetadata,
265    /// Inclusion proof for the note in the block.
266    inclusion_proof: NoteInclusionProof,
267}
268
269impl CommittedNote {
270    pub fn new(
271        note_id: NoteId,
272        metadata: NoteMetadata,
273        inclusion_proof: NoteInclusionProof,
274    ) -> Self {
275        Self { note_id, metadata, inclusion_proof }
276    }
277
278    pub fn note_id(&self) -> &NoteId {
279        &self.note_id
280    }
281
282    pub fn note_type(&self) -> NoteType {
283        self.metadata.note_type()
284    }
285
286    pub fn tag(&self) -> NoteTag {
287        self.metadata.tag()
288    }
289
290    pub fn sender(&self) -> AccountId {
291        self.metadata.sender()
292    }
293
294    /// Returns the full note metadata.
295    pub fn metadata(&self) -> &NoteMetadata {
296        &self.metadata
297    }
298
299    pub fn inclusion_proof(&self) -> &NoteInclusionProof {
300        &self.inclusion_proof
301    }
302}
303
304impl TryFrom<proto::note::NoteSyncRecord> for CommittedNote {
305    type Error = RpcConversionError;
306
307    fn try_from(note: proto::note::NoteSyncRecord) -> Result<Self, Self::Error> {
308        let proto_metadata = note
309            .metadata
310            .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(notes.metadata)))?;
311        let metadata: NoteMetadata = proto_metadata.try_into()?;
312
313        let proto_inclusion_proof = note.inclusion_proof.ok_or(
314            proto::rpc::SyncNotesResponse::missing_field(stringify!(notes.inclusion_proof)),
315        )?;
316
317        let note_id: NoteId = proto_inclusion_proof
318            .note_id
319            .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(
320                notes.inclusion_proof.note_id
321            )))?
322            .try_into()?;
323
324        let inclusion_proof: NoteInclusionProof = proto_inclusion_proof.try_into()?;
325
326        Ok(CommittedNote::new(note_id, metadata, inclusion_proof))
327    }
328}
329
330// FETCHED NOTE
331// ================================================================================================
332
333/// Describes the possible responses from the `GetNotesById` endpoint for a single note.
334#[allow(clippy::large_enum_variant)]
335pub enum FetchedNote {
336    /// Details for a private note include its ID, metadata, attachments and inclusion proof. Other
337    /// details needed to consume the note are expected to be stored locally, off-chain.
338    ///
339    /// Attachments are a public extension of the note and are stored on-chain even for private
340    /// notes, so the node returns them here; they are needed to reconstruct the correct note ID.
341    Private(NoteId, NoteMetadata, NoteAttachments, NoteInclusionProof),
342    /// Contains the full [`Note`] object alongside its [`NoteInclusionProof`].
343    Public(Note, NoteInclusionProof),
344}
345
346impl FetchedNote {
347    /// Returns the note's inclusion details.
348    pub fn inclusion_proof(&self) -> &NoteInclusionProof {
349        match self {
350            FetchedNote::Private(_, _, _, inclusion_proof)
351            | FetchedNote::Public(_, inclusion_proof) => inclusion_proof,
352        }
353    }
354
355    /// Returns the note's metadata.
356    pub fn metadata(&self) -> &NoteMetadata {
357        match self {
358            FetchedNote::Private(_, metadata, ..) => metadata,
359            FetchedNote::Public(note, _) => note.metadata(),
360        }
361    }
362
363    /// Returns the note's attachments.
364    pub fn attachments(&self) -> &NoteAttachments {
365        match self {
366            FetchedNote::Private(_, _, attachments, _) => attachments,
367            FetchedNote::Public(note, _) => note.attachments(),
368        }
369    }
370
371    /// Returns the note's ID.
372    pub fn id(&self) -> NoteId {
373        match self {
374            FetchedNote::Private(note_id, ..) => *note_id,
375            FetchedNote::Public(note, _) => note.id(),
376        }
377    }
378}
379
380impl TryFrom<proto::note::CommittedNote> for FetchedNote {
381    type Error = RpcConversionError;
382
383    fn try_from(value: proto::note::CommittedNote) -> Result<Self, Self::Error> {
384        let inclusion_proof = value.inclusion_proof.ok_or_else(|| {
385            proto::note::CommittedNote::missing_field(stringify!(inclusion_proof))
386        })?;
387
388        let note_id: NoteId = inclusion_proof
389            .note_id
390            .ok_or_else(|| {
391                proto::note::CommittedNote::missing_field(stringify!(inclusion_proof.note_id))
392            })?
393            .try_into()?;
394
395        let inclusion_proof = NoteInclusionProof::try_from(inclusion_proof)?;
396
397        let note = value
398            .note
399            .ok_or_else(|| proto::note::CommittedNote::missing_field(stringify!(note)))?;
400
401        let proto_metadata = note
402            .metadata
403            .ok_or_else(|| proto::note::CommittedNote::missing_field(stringify!(note.metadata)))?;
404        let metadata: NoteMetadata = proto_metadata.clone().try_into()?;
405        let partial_metadata: PartialNoteMetadata = (&proto_metadata).try_into()?;
406
407        let attachments = if note.attachments.is_empty() {
408            NoteAttachments::empty()
409        } else {
410            NoteAttachments::read_from_bytes(&note.attachments)?
411        };
412
413        if let Some(detail_bytes) = note.details {
414            let details = NoteDetails::read_from_bytes(&detail_bytes)?;
415            let (assets, recipient) = details.into_parts();
416
417            Ok(FetchedNote::Public(
418                Note::with_attachments(assets, partial_metadata, recipient, attachments),
419                inclusion_proof,
420            ))
421        } else {
422            Ok(FetchedNote::Private(note_id, metadata, attachments, inclusion_proof))
423        }
424    }
425}
426
427// NOTE SCRIPT
428// ================================================================================================
429
430impl TryFrom<proto::note::NoteScript> for NoteScript {
431    type Error = RpcConversionError;
432
433    fn try_from(note_script: proto::note::NoteScript) -> Result<Self, Self::Error> {
434        let mast_forest = MastForest::read_from_bytes(&note_script.mast)?;
435        let entrypoint = MastNodeId::from_u32_safe(note_script.entrypoint, &mast_forest)?;
436        Ok(NoteScript::from_parts(alloc::sync::Arc::new(mast_forest), entrypoint))
437    }
438}