1use alloc::collections::BTreeMap;
2use alloc::format;
3use alloc::vec::Vec;
4
5use miden_protocol::account::AccountId;
6use miden_protocol::block::{BlockHeader, BlockNumber};
7use miden_protocol::crypto::SequentialCommit;
8use miden_protocol::crypto::merkle::MerklePath;
9use miden_protocol::note::{
10 Note,
11 NoteAttachment,
12 NoteAttachmentHeader,
13 NoteAttachmentScheme,
14 NoteAttachments,
15 NoteDetails,
16 NoteDetailsCommitment,
17 NoteHeader,
18 NoteId,
19 NoteInclusionProof,
20 NoteMetadata,
21 NoteScript,
22 NoteTag,
23 NoteType,
24 PartialNoteMetadata,
25};
26use miden_protocol::{Felt, MastForest, MastNodeId, Word};
27use miden_tx::utils::serde::Deserializable;
28
29use super::{MissingFieldHelper, RpcConversionError};
30use crate::rpc::{RpcError, generated as proto};
31
32impl From<NoteId> for proto::note::NoteId {
33 fn from(value: NoteId) -> Self {
34 proto::note::NoteId { id: Some(value.into()) }
35 }
36}
37
38impl TryFrom<proto::note::NoteId> for NoteId {
39 type Error = RpcConversionError;
40
41 fn try_from(value: proto::note::NoteId) -> Result<Self, Self::Error> {
42 let word =
43 Word::try_from(value.id.ok_or(proto::note::NoteId::missing_field(stringify!(id)))?)?;
44 Ok(Self::from_raw(word))
45 }
46}
47
48fn note_type_from_proto(raw: i32) -> Result<NoteType, RpcConversionError> {
49 let proto_note_type = proto::note::NoteType::try_from(raw)
50 .map_err(|_| RpcConversionError::InvalidField(alloc::format!("note_type={raw}")))?;
51 match proto_note_type {
52 proto::note::NoteType::Public => Ok(NoteType::Public),
53 proto::note::NoteType::Private => Ok(NoteType::Private),
54 proto::note::NoteType::Unspecified => {
55 Err(RpcConversionError::InvalidField("note_type=NOTE_TYPE_UNSPECIFIED".into()))
56 },
57 }
58}
59
60fn note_type_to_proto(note_type: NoteType) -> i32 {
61 let proto_note_type = match note_type {
62 NoteType::Public => proto::note::NoteType::Public,
63 NoteType::Private => proto::note::NoteType::Private,
64 };
65 proto_note_type as i32
66}
67
68fn attachment_headers_from_proto(
72 schemes: &[u32],
73) -> Result<[NoteAttachmentHeader; NoteAttachments::MAX_COUNT], RpcConversionError> {
74 if schemes.len() > NoteAttachments::MAX_COUNT {
75 return Err(RpcConversionError::InvalidField(alloc::format!(
76 "attachment_schemes length {} exceeds NoteAttachments::MAX_COUNT",
77 schemes.len(),
78 )));
79 }
80 let mut headers = [NoteAttachmentHeader::absent(); NoteAttachments::MAX_COUNT];
81 for (slot, raw) in schemes.iter().enumerate() {
82 if *raw == 0 {
83 continue;
84 }
85 let raw_u16 = u16::try_from(*raw).map_err(|_| {
86 RpcConversionError::InvalidField(alloc::format!(
87 "attachment_schemes[{slot}]={raw} does not fit in u16",
88 ))
89 })?;
90 let scheme = NoteAttachmentScheme::new(raw_u16).map_err(|err| {
91 RpcConversionError::InvalidField(alloc::format!("attachment_schemes[{slot}]: {err}"))
92 })?;
93 headers[slot] = NoteAttachmentHeader::new(scheme);
94 }
95 Ok(headers)
96}
97
98fn attachment_schemes_to_proto(
99 headers: &[NoteAttachmentHeader; NoteAttachments::MAX_COUNT],
100) -> Vec<u32> {
101 let mut encoded: Vec<u32> = headers
104 .iter()
105 .map(|h| h.scheme().map_or(0, |s| u32::from(s.as_u16())))
106 .collect();
107 while matches!(encoded.last(), Some(0)) {
108 encoded.pop();
109 }
110 encoded
111}
112
113impl TryFrom<proto::note::NoteMetadata> for NoteMetadata {
114 type Error = RpcConversionError;
115
116 fn try_from(value: proto::note::NoteMetadata) -> Result<Self, Self::Error> {
117 let partial_metadata: PartialNoteMetadata = (&value).try_into()?;
118 let attachment_headers = attachment_headers_from_proto(&value.attachment_schemes)?;
119 let attachments_commitment = value
120 .attachments_commitment
121 .ok_or_else(|| {
122 proto::note::NoteMetadata::missing_field(stringify!(attachments_commitment))
123 })?
124 .try_into()?;
125
126 Ok(NoteMetadata::from_parts(
127 partial_metadata,
128 attachment_headers,
129 attachments_commitment,
130 ))
131 }
132}
133
134struct AttachmentCommitments(Vec<Word>);
142
143impl SequentialCommit for AttachmentCommitments {
144 type Commitment = Word;
145
146 fn to_elements(&self) -> Vec<Felt> {
147 let mut elements = Vec::with_capacity(self.0.len() * miden_protocol::WORD_SIZE);
148 for commitment in &self.0 {
149 elements.extend_from_slice(commitment.as_elements());
150 }
151 elements
152 }
153}
154
155impl TryFrom<proto::note::NoteSyncMetadata> for NoteMetadata {
156 type Error = RpcConversionError;
157
158 fn try_from(value: proto::note::NoteSyncMetadata) -> Result<Self, Self::Error> {
159 let sender = value
160 .sender
161 .ok_or_else(|| proto::note::NoteSyncMetadata::missing_field(stringify!(sender)))?
162 .try_into()?;
163 let note_type = note_type_from_proto(value.note_type)?;
164 let tag = NoteTag::new(value.tag);
165 let partial_metadata = PartialNoteMetadata::new(sender, note_type).with_tag(tag);
166
167 if value.attachments.len() > NoteAttachments::MAX_COUNT {
168 return Err(RpcConversionError::InvalidField(format!(
169 "attachments length {} exceeds NoteAttachments::MAX_COUNT",
170 value.attachments.len(),
171 )));
172 }
173
174 let mut attachment_headers = [NoteAttachmentHeader::absent(); NoteAttachments::MAX_COUNT];
175 let mut commitments = Vec::with_capacity(value.attachments.len());
176
177 for (slot, attachment) in value.attachments.into_iter().enumerate() {
178 let raw_scheme = u16::try_from(attachment.scheme).map_err(|_| {
179 RpcConversionError::InvalidField(format!(
180 "attachments[{slot}].scheme={} does not fit in u16",
181 attachment.scheme,
182 ))
183 })?;
184 let scheme = NoteAttachmentScheme::new(raw_scheme).map_err(|err| {
185 RpcConversionError::InvalidField(format!("attachments[{slot}].scheme: {err}"))
186 })?;
187 attachment_headers[slot] = NoteAttachmentHeader::new(scheme);
188
189 let payload = attachment.payload.ok_or_else(|| {
190 proto::note::NoteSyncAttachment::missing_field(stringify!(payload))
191 })?;
192 let commitment = match payload {
198 proto::note::note_sync_attachment::Payload::Value(value) => {
199 NoteAttachment::with_word(scheme, Word::try_from(value)?).to_commitment()
200 },
201 proto::note::note_sync_attachment::Payload::Commitment(commitment) => {
202 Word::try_from(commitment)?
203 },
204 };
205 commitments.push(commitment);
206 }
207
208 let attachments_commitment = AttachmentCommitments(commitments).to_commitment();
209
210 Ok(NoteMetadata::from_parts(
211 partial_metadata,
212 attachment_headers,
213 attachments_commitment,
214 ))
215 }
216}
217
218impl TryFrom<&proto::note::NoteMetadata> for PartialNoteMetadata {
219 type Error = RpcConversionError;
220
221 fn try_from(value: &proto::note::NoteMetadata) -> Result<Self, Self::Error> {
222 let sender = value
223 .sender
224 .clone()
225 .ok_or_else(|| proto::note::NoteMetadata::missing_field(stringify!(sender)))?
226 .try_into()?;
227 let note_type = note_type_from_proto(value.note_type)?;
228 let tag = NoteTag::new(value.tag);
229
230 Ok(PartialNoteMetadata::new(sender, note_type).with_tag(tag))
231 }
232}
233
234impl From<NoteMetadata> for proto::note::NoteMetadata {
235 fn from(value: NoteMetadata) -> Self {
236 proto::note::NoteMetadata {
237 sender: Some(value.sender().into()),
238 note_type: note_type_to_proto(value.note_type()),
239 tag: value.tag().as_u32(),
240 attachment_schemes: attachment_schemes_to_proto(value.attachment_headers()),
241 attachments_commitment: Some(value.attachments_commitment().into()),
242 }
243 }
244}
245
246impl TryFrom<proto::note::NoteHeader> for NoteHeader {
247 type Error = RpcConversionError;
248
249 fn try_from(value: proto::note::NoteHeader) -> Result<Self, Self::Error> {
250 let details_commitment_word: Word = value
251 .details_commitment
252 .ok_or(proto::note::NoteHeader::missing_field(stringify!(details_commitment)))?
253 .try_into()?;
254 let metadata = value
255 .metadata
256 .ok_or(proto::note::NoteHeader::missing_field(stringify!(metadata)))?
257 .try_into()?;
258 Ok(NoteHeader::new(
259 NoteDetailsCommitment::from_raw(details_commitment_word),
260 metadata,
261 ))
262 }
263}
264
265impl TryFrom<proto::note::NoteInclusionInBlockProof> for NoteInclusionProof {
266 type Error = RpcConversionError;
267
268 fn try_from(value: proto::note::NoteInclusionInBlockProof) -> Result<Self, Self::Error> {
269 Ok(NoteInclusionProof::new(
270 value.block_num.into(),
271 u16::try_from(value.note_index_in_block)
272 .map_err(|_| RpcConversionError::InvalidField("NoteIndexInBlock".into()))?,
273 value
274 .inclusion_path
275 .ok_or_else(|| {
276 proto::note::NoteInclusionInBlockProof::missing_field(stringify!(
277 inclusion_path
278 ))
279 })?
280 .try_into()?,
281 )?)
282 }
283}
284
285#[derive(Debug, Clone)]
290pub struct SyncNotesBlock {
291 pub block_header: BlockHeader,
293 pub mmr_path: MerklePath,
295 pub notes: BTreeMap<NoteId, CommittedNote>,
297}
298
299impl TryFrom<proto::rpc::sync_notes_response::NoteSyncBlock> for SyncNotesBlock {
300 type Error = RpcError;
301
302 fn try_from(
303 block: proto::rpc::sync_notes_response::NoteSyncBlock,
304 ) -> Result<Self, Self::Error> {
305 let block_header = block
306 .block_header
307 .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(blocks.block_header)))?
308 .try_into()?;
309
310 let mmr_path = block
311 .mmr_path
312 .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(blocks.mmr_path)))?
313 .try_into()?;
314
315 let notes: BTreeMap<NoteId, CommittedNote> = block
316 .notes
317 .into_iter()
318 .map(|n| {
319 let note = CommittedNote::try_from(n)?;
320 Ok((*note.note_id(), note))
321 })
322 .collect::<Result<_, RpcConversionError>>()?;
323
324 Ok(SyncNotesBlock { block_header, mmr_path, notes })
325 }
326}
327
328#[derive(Debug, Clone)]
338pub struct ResolvedSyncNotesBlock {
339 pub block_header: BlockHeader,
341 pub mmr_path: MerklePath,
343 pub notes: BTreeMap<NoteId, SyncedNote>,
345}
346
347#[derive(Debug, Clone)]
351pub struct SyncedNote {
352 pub committed: CommittedNote,
354 pub content: Option<ResolvedNoteContent>,
357}
358
359impl SyncedNote {
360 pub fn new(
376 committed: CommittedNote,
377 content: Option<ResolvedNoteContent>,
378 ) -> Result<Self, RpcError> {
379 match &content {
380 Some(resolved) => {
381 let expected_note_type = match resolved {
382 ResolvedNoteContent::Public { .. } => NoteType::Public,
383 ResolvedNoteContent::Private { .. } => NoteType::Private,
384 };
385 if committed.note_type() != expected_note_type {
386 return Err(RpcError::InvalidResponse(format!(
387 "content returned for note {} does not match the note's type",
388 committed.note_id()
389 )));
390 }
391
392 if resolved.attachments().to_commitment()
393 != committed.metadata().attachments_commitment()
394 {
395 return Err(RpcError::InvalidResponse(format!(
396 "attachment content returned for note {} does not match the note's \
397 attachments commitment",
398 committed.note_id()
399 )));
400 }
401 },
402 None => {
403 if committed.has_attachments() {
404 return Err(RpcError::InvalidResponse(format!(
405 "note {} advertises attachments but the node did not return their content",
406 committed.note_id()
407 )));
408 }
409 },
410 }
411
412 Ok(Self { committed, content })
413 }
414}
415
416#[derive(Debug, Clone)]
418#[allow(clippy::large_enum_variant)]
419pub enum ResolvedNoteContent {
420 Public {
422 details: NoteDetails,
424 attachments: NoteAttachments,
426 },
427 Private {
430 attachments: NoteAttachments,
432 },
433}
434
435impl ResolvedNoteContent {
436 pub fn attachments(&self) -> &NoteAttachments {
438 match self {
439 Self::Public { attachments, .. } | Self::Private { attachments } => attachments,
440 }
441 }
442
443 pub fn into_attachments(self) -> NoteAttachments {
445 match self {
446 Self::Public { attachments, .. } | Self::Private { attachments } => attachments,
447 }
448 }
449}
450
451#[derive(Debug, Clone)]
456pub struct CommittedNote {
457 note_id: NoteId,
459 metadata: NoteMetadata,
463 inclusion_proof: NoteInclusionProof,
465}
466
467impl CommittedNote {
468 pub fn new(
469 note_id: NoteId,
470 metadata: NoteMetadata,
471 inclusion_proof: NoteInclusionProof,
472 ) -> Self {
473 Self { note_id, metadata, inclusion_proof }
474 }
475
476 pub fn note_id(&self) -> &NoteId {
477 &self.note_id
478 }
479
480 pub fn note_type(&self) -> NoteType {
481 self.metadata.note_type()
482 }
483
484 pub fn tag(&self) -> NoteTag {
485 self.metadata.tag()
486 }
487
488 pub fn sender(&self) -> AccountId {
489 self.metadata.sender()
490 }
491
492 pub fn metadata(&self) -> &NoteMetadata {
494 &self.metadata
495 }
496
497 pub fn has_attachments(&self) -> bool {
503 self.metadata
504 .attachment_headers()
505 .iter()
506 .any(|header| header.scheme().is_some())
507 }
508
509 pub fn inclusion_proof(&self) -> &NoteInclusionProof {
510 &self.inclusion_proof
511 }
512
513 pub fn block_num(&self) -> BlockNumber {
515 self.inclusion_proof.location().block_num()
516 }
517}
518
519impl TryFrom<proto::note::NoteSyncRecord> for CommittedNote {
520 type Error = RpcConversionError;
521
522 fn try_from(note: proto::note::NoteSyncRecord) -> Result<Self, Self::Error> {
523 let proto_metadata = note
524 .metadata
525 .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(notes.metadata)))?;
526 let metadata: NoteMetadata = proto_metadata.try_into()?;
527
528 let proto_inclusion_proof = note.inclusion_proof.ok_or(
529 proto::rpc::SyncNotesResponse::missing_field(stringify!(notes.inclusion_proof)),
530 )?;
531
532 let note_id: NoteId = proto_inclusion_proof
533 .note_id
534 .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(
535 notes.inclusion_proof.note_id
536 )))?
537 .try_into()?;
538
539 let inclusion_proof: NoteInclusionProof = proto_inclusion_proof.try_into()?;
540
541 Ok(CommittedNote::new(note_id, metadata, inclusion_proof))
542 }
543}
544
545#[allow(clippy::large_enum_variant)]
550pub enum FetchedNote {
551 Private(NoteId, NoteMetadata, NoteAttachments, NoteInclusionProof),
557 Public(Note, NoteInclusionProof),
559}
560
561impl FetchedNote {
562 pub fn inclusion_proof(&self) -> &NoteInclusionProof {
564 match self {
565 FetchedNote::Private(_, _, _, inclusion_proof)
566 | FetchedNote::Public(_, inclusion_proof) => inclusion_proof,
567 }
568 }
569
570 pub fn metadata(&self) -> &NoteMetadata {
572 match self {
573 FetchedNote::Private(_, metadata, ..) => metadata,
574 FetchedNote::Public(note, _) => note.metadata(),
575 }
576 }
577
578 pub fn attachments(&self) -> &NoteAttachments {
580 match self {
581 FetchedNote::Private(_, _, attachments, _) => attachments,
582 FetchedNote::Public(note, _) => note.attachments(),
583 }
584 }
585
586 pub fn id(&self) -> NoteId {
588 match self {
589 FetchedNote::Private(note_id, ..) => *note_id,
590 FetchedNote::Public(note, _) => note.id(),
591 }
592 }
593}
594
595impl TryFrom<proto::note::CommittedNote> for FetchedNote {
596 type Error = RpcConversionError;
597
598 fn try_from(value: proto::note::CommittedNote) -> Result<Self, Self::Error> {
599 let inclusion_proof = value.inclusion_proof.ok_or_else(|| {
600 proto::note::CommittedNote::missing_field(stringify!(inclusion_proof))
601 })?;
602
603 let note_id: NoteId = inclusion_proof
604 .note_id
605 .ok_or_else(|| {
606 proto::note::CommittedNote::missing_field(stringify!(inclusion_proof.note_id))
607 })?
608 .try_into()?;
609
610 let inclusion_proof = NoteInclusionProof::try_from(inclusion_proof)?;
611
612 let note = value
613 .note
614 .ok_or_else(|| proto::note::CommittedNote::missing_field(stringify!(note)))?;
615
616 let proto_metadata = note
617 .metadata
618 .ok_or_else(|| proto::note::CommittedNote::missing_field(stringify!(note.metadata)))?;
619 let metadata: NoteMetadata = proto_metadata.clone().try_into()?;
620 let partial_metadata: PartialNoteMetadata = (&proto_metadata).try_into()?;
621
622 let attachments = if note.attachments.is_empty() {
623 NoteAttachments::empty()
624 } else {
625 NoteAttachments::read_from_bytes(¬e.attachments)?
626 };
627
628 if let Some(detail_bytes) = note.details {
629 let details = NoteDetails::read_from_bytes(&detail_bytes)?;
630 let (assets, recipient) = details.into_parts();
631
632 Ok(FetchedNote::Public(
633 Note::with_attachments(assets, partial_metadata, recipient, attachments),
634 inclusion_proof,
635 ))
636 } else {
637 Ok(FetchedNote::Private(note_id, metadata, attachments, inclusion_proof))
638 }
639 }
640}
641
642impl TryFrom<proto::note::NoteScript> for NoteScript {
646 type Error = RpcConversionError;
647
648 fn try_from(note_script: proto::note::NoteScript) -> Result<Self, Self::Error> {
649 let mast_forest = MastForest::read_from_bytes(¬e_script.mast)?;
650 let entrypoint = MastNodeId::from_u32_safe(note_script.entrypoint, &mast_forest)?;
651 Ok(NoteScript::from_parts(alloc::sync::Arc::new(mast_forest), entrypoint))
652 }
653}
654
655#[cfg(test)]
659mod tests {
660 use miden_protocol::account::{AccountIdVersion, AccountType, AssetCallbackFlag};
661
662 use super::*;
663
664 fn sender() -> AccountId {
665 AccountId::dummy(
666 [1; 15],
667 AccountIdVersion::Version1,
668 AccountType::Public,
669 AssetCallbackFlag::Disabled,
670 )
671 }
672
673 fn sync_attachments(attachments: &NoteAttachments) -> Vec<proto::note::NoteSyncAttachment> {
676 attachments
677 .iter()
678 .map(|attachment| {
679 let payload = if attachment.num_words() == 1 {
680 proto::note::note_sync_attachment::Payload::Value(
681 attachment.content().as_words()[0].into(),
682 )
683 } else {
684 proto::note::note_sync_attachment::Payload::Commitment(
685 attachment.to_commitment().into(),
686 )
687 };
688
689 proto::note::NoteSyncAttachment {
690 scheme: u32::from(attachment.attachment_scheme().as_u16()),
691 payload: Some(payload),
692 }
693 })
694 .collect()
695 }
696
697 fn sync_metadata(
698 attachments: Vec<proto::note::NoteSyncAttachment>,
699 ) -> proto::note::NoteSyncMetadata {
700 proto::note::NoteSyncMetadata {
701 sender: Some(sender().into()),
702 note_type: note_type_to_proto(NoteType::Private),
703 tag: 7,
704 attachments,
705 }
706 }
707
708 #[test]
709 fn sync_metadata_reconstructs_metadata_with_mixed_attachments() {
710 let attachments = NoteAttachments::new(vec![
711 NoteAttachment::with_word(
712 NoteAttachmentScheme::new(42).unwrap(),
713 Word::from([1u32, 2, 3, 4]),
714 ),
715 NoteAttachment::with_words(
716 NoteAttachmentScheme::new(100).unwrap(),
717 vec![Word::from([5u32, 6, 7, 8]), Word::from([9u32, 10, 11, 12])],
718 )
719 .unwrap(),
720 ])
721 .unwrap();
722
723 let expected = NoteMetadata::new(
724 PartialNoteMetadata::new(sender(), NoteType::Private).with_tag(NoteTag::new(7)),
725 &attachments,
726 );
727
728 let reconstructed: NoteMetadata =
729 sync_metadata(sync_attachments(&attachments)).try_into().unwrap();
730
731 assert_eq!(reconstructed, expected);
732 }
733
734 #[test]
735 fn sync_metadata_reconstructs_metadata_without_attachments() {
736 let attachments = NoteAttachments::empty();
737 let expected = NoteMetadata::new(
738 PartialNoteMetadata::new(sender(), NoteType::Private).with_tag(NoteTag::new(7)),
739 &attachments,
740 );
741
742 let reconstructed: NoteMetadata = sync_metadata(Vec::new()).try_into().unwrap();
743
744 assert_eq!(reconstructed, expected);
745 }
746
747 #[test]
748 fn sync_metadata_rejects_too_many_attachments() {
749 let attachment = proto::note::NoteSyncAttachment {
750 scheme: 42,
751 payload: Some(proto::note::note_sync_attachment::Payload::Value(Word::empty().into())),
752 };
753 let attachments = vec![attachment; NoteAttachments::MAX_COUNT + 1];
754
755 let err = NoteMetadata::try_from(sync_metadata(attachments)).unwrap_err();
756
757 assert!(matches!(err, RpcConversionError::InvalidField(_)), "got {err:?}");
758 }
759
760 #[test]
761 fn sync_metadata_rejects_reserved_absent_scheme() {
762 let attachments = vec![proto::note::NoteSyncAttachment {
763 scheme: 0,
764 payload: Some(proto::note::note_sync_attachment::Payload::Value(Word::empty().into())),
765 }];
766
767 let err = NoteMetadata::try_from(sync_metadata(attachments)).unwrap_err();
768
769 assert!(matches!(err, RpcConversionError::InvalidField(_)), "got {err:?}");
770 }
771
772 #[test]
773 fn sync_metadata_rejects_missing_attachment_payload() {
774 let attachments = vec![proto::note::NoteSyncAttachment { scheme: 42, payload: None }];
775
776 let err = NoteMetadata::try_from(sync_metadata(attachments)).unwrap_err();
777
778 assert!(
779 matches!(err, RpcConversionError::MissingFieldInProtobufRepresentation { .. }),
780 "got {err:?}"
781 );
782 }
783}