1use alloc::collections::BTreeMap;
2use alloc::format;
3use alloc::vec::Vec;
4
5use miden_objects::DecodeMessageExt;
6use miden_protocol::account::AccountId;
7use miden_protocol::block::{BlockHeader, BlockNumber};
8use miden_protocol::crypto::SequentialCommit;
9use miden_protocol::crypto::merkle::MerklePath;
10use miden_protocol::note::{
11 Note,
12 NoteAttachment,
13 NoteAttachmentHeader,
14 NoteAttachmentScheme,
15 NoteAttachments,
16 NoteDetails,
17 NoteId,
18 NoteInclusionProof,
19 NoteMetadata,
20 NoteTag,
21 NoteType,
22 PartialNoteMetadata,
23};
24use miden_protocol::{Felt, Word};
25
26use super::{MissingFieldHelper, RpcConversionError};
27use crate::rpc::{RpcError, generated as proto};
28
29pub(crate) fn note_id_from_proto(value: proto::note::NoteId) -> Result<NoteId, RpcConversionError> {
32 Ok(value.decode_and_verify()?)
33}
34
35pub(crate) fn note_inclusion_proof_from_proto(
37 value: proto::note::NoteInclusionProof,
38) -> Result<(NoteId, NoteInclusionProof), RpcConversionError> {
39 Ok(value.decode_and_verify()?)
40}
41
42struct AttachmentCommitments<'a>(&'a [Word]);
47
48impl SequentialCommit for AttachmentCommitments<'_> {
49 type Commitment = Word;
50
51 fn to_elements(&self) -> Vec<Felt> {
52 let mut elements = Vec::with_capacity(self.0.len() * miden_protocol::WORD_SIZE);
53 for commitment in self.0 {
54 elements.extend_from_slice(commitment.as_elements());
55 }
56 elements
57 }
58}
59
60#[derive(Debug)]
63enum ReportedAttachment {
64 Full(NoteAttachment),
66 Commitment(Word),
68}
69
70impl ReportedAttachment {
71 fn to_commitment(&self) -> Word {
73 match self {
74 Self::Full(attachment) => attachment.to_commitment(),
75 Self::Commitment(commitment) => *commitment,
76 }
77 }
78}
79
80#[derive(Debug)]
83enum ReportedAttachments {
84 Full(NoteAttachments),
86 Commitments(Vec<Word>),
89}
90
91impl ReportedAttachments {
92 fn from_reports(reports: Vec<ReportedAttachment>) -> Result<Self, RpcConversionError> {
96 let commitments: Vec<Word> =
97 reports.iter().map(ReportedAttachment::to_commitment).collect();
98 let contents: Option<Vec<NoteAttachment>> = reports
99 .into_iter()
100 .map(|report| match report {
101 ReportedAttachment::Full(attachment) => Some(attachment),
102 ReportedAttachment::Commitment(_) => None,
103 })
104 .collect();
105
106 match contents {
107 Some(contents) => {
108 let attachments = NoteAttachments::new(contents).map_err(|err| {
109 RpcConversionError::InvalidField(format!("attachments: {err}"))
110 })?;
111 Ok(Self::Full(attachments))
112 },
113 None => Ok(Self::Commitments(commitments)),
114 }
115 }
116
117 fn to_commitment(&self) -> Word {
119 match self {
120 Self::Full(attachments) => attachments.to_commitment(),
121 Self::Commitments(commitments) => AttachmentCommitments(commitments).to_commitment(),
122 }
123 }
124
125 fn into_content(self) -> Option<NoteAttachments> {
127 match self {
128 Self::Full(attachments) => Some(attachments),
129 Self::Commitments(_) => None,
130 }
131 }
132}
133
134#[derive(Debug)]
137struct SyncNoteMetadata {
138 metadata: NoteMetadata,
140 attachments: ReportedAttachments,
142}
143
144impl TryFrom<proto::rpc::NoteSyncMetadata> for SyncNoteMetadata {
145 type Error = RpcConversionError;
146
147 fn try_from(value: proto::rpc::NoteSyncMetadata) -> Result<Self, Self::Error> {
148 let partial_metadata: PartialNoteMetadata = proto::note::PartialNoteMetadata {
151 version: value.version,
152 sender: value.sender,
153 note_type: value.note_type,
154 tag: value.tag,
155 }
156 .decode_and_verify()?;
157
158 if value.attachments.len() > NoteAttachments::MAX_COUNT {
159 return Err(RpcConversionError::InvalidField(format!(
160 "attachments length {} exceeds NoteAttachments::MAX_COUNT",
161 value.attachments.len(),
162 )));
163 }
164
165 let mut attachment_headers = [NoteAttachmentHeader::absent(); NoteAttachments::MAX_COUNT];
166 let mut reports = Vec::with_capacity(value.attachments.len());
167
168 for (slot, attachment) in value.attachments.into_iter().enumerate() {
169 let raw_scheme = u16::try_from(attachment.scheme).map_err(|_| {
170 RpcConversionError::InvalidField(format!(
171 "attachments[{slot}].scheme={} does not fit in u16",
172 attachment.scheme,
173 ))
174 })?;
175 let scheme = NoteAttachmentScheme::new(raw_scheme).map_err(|err| {
176 RpcConversionError::InvalidField(format!("attachments[{slot}].scheme: {err}"))
177 })?;
178 attachment_headers[slot] = NoteAttachmentHeader::new(scheme);
179
180 let payload = attachment.payload.ok_or_else(|| {
181 proto::rpc::NoteSyncAttachment::missing_field(stringify!(payload))
182 })?;
183 let report = match payload {
188 proto::rpc::note_sync_attachment::Payload::Value(value) => {
189 ReportedAttachment::Full(NoteAttachment::with_word(
190 scheme,
191 Word::try_from(value)?,
192 ))
193 },
194 proto::rpc::note_sync_attachment::Payload::Commitment(commitment) => {
195 ReportedAttachment::Commitment(Word::try_from(commitment)?)
196 },
197 };
198 reports.push(report);
199 }
200
201 let attachments = ReportedAttachments::from_reports(reports)?;
202
203 Ok(SyncNoteMetadata {
204 metadata: NoteMetadata::from_parts(
205 partial_metadata,
206 attachment_headers,
207 attachments.to_commitment(),
208 ),
209 attachments,
210 })
211 }
212}
213
214#[derive(Debug, Clone)]
219pub struct SyncNotesBlock {
220 pub block_header: BlockHeader,
222 pub mmr_path: MerklePath,
224 pub notes: BTreeMap<NoteId, CommittedNote>,
226}
227
228impl TryFrom<proto::rpc::sync_notes_response::NoteSyncBlock> for SyncNotesBlock {
229 type Error = RpcError;
230
231 fn try_from(
232 block: proto::rpc::sync_notes_response::NoteSyncBlock,
233 ) -> Result<Self, Self::Error> {
234 let block_header: BlockHeader = block
235 .block_header
236 .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(blocks.block_header)))?
237 .decode_and_build_unchecked()?;
238
239 let mmr_path: MerklePath = block
240 .mmr_path
241 .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(blocks.mmr_path)))?
242 .decode_and_verify()?;
243
244 let notes: BTreeMap<NoteId, CommittedNote> = block
245 .notes
246 .into_iter()
247 .map(|n| {
248 let note = CommittedNote::try_from(n)?;
249 Ok((*note.note_id(), note))
250 })
251 .collect::<Result<_, RpcConversionError>>()?;
252
253 Ok(SyncNotesBlock { block_header, mmr_path, notes })
254 }
255}
256
257#[derive(Debug, Clone)]
266pub struct ResolvedSyncNotesBlock {
267 pub block_header: BlockHeader,
269 pub mmr_path: MerklePath,
271 pub notes: BTreeMap<NoteId, SyncedNote>,
273}
274
275#[derive(Debug, Clone)]
279pub struct SyncedNote {
280 pub note_id: NoteId,
282 pub metadata: NoteMetadata,
284 pub inclusion_proof: NoteInclusionProof,
286 pub details: Option<NoteDetails>,
289 pub attachments: NoteAttachments,
292}
293
294impl SyncedNote {
295 pub fn new(
311 committed: CommittedNote,
312 details: Option<NoteDetails>,
313 attachments: NoteAttachments,
314 ) -> Result<Self, RpcError> {
315 if details.is_some() && committed.note_type() != NoteType::Public {
316 return Err(RpcError::InvalidResponse(format!(
317 "a note body was returned for private note {}",
318 committed.note_id()
319 )));
320 }
321
322 if attachments.to_commitment() != committed.metadata().attachments_commitment() {
323 return Err(RpcError::InvalidResponse(format!(
324 "the attachments resolved for note {} do not match the note's attachments \
325 commitment",
326 committed.note_id()
327 )));
328 }
329
330 let CommittedNote {
331 note_id,
332 metadata,
333 inclusion_proof,
334 attachments: _,
335 } = committed;
336
337 Ok(Self {
338 note_id,
339 metadata,
340 inclusion_proof,
341 details,
342 attachments,
343 })
344 }
345
346 pub fn block_num(&self) -> BlockNumber {
348 self.inclusion_proof.location().block_num()
349 }
350
351 pub fn into_committed_note(self) -> CommittedNote {
358 CommittedNote {
359 note_id: self.note_id,
360 metadata: self.metadata,
361 inclusion_proof: self.inclusion_proof,
362 attachments: Some(self.attachments),
363 }
364 }
365}
366
367#[derive(Debug, Clone)]
372pub struct CommittedNote {
373 note_id: NoteId,
375 metadata: NoteMetadata,
378 inclusion_proof: NoteInclusionProof,
380 attachments: Option<NoteAttachments>,
383}
384
385impl CommittedNote {
386 pub fn new(
387 note_id: NoteId,
388 metadata: NoteMetadata,
389 inclusion_proof: NoteInclusionProof,
390 ) -> Self {
391 Self {
392 note_id,
393 metadata,
394 inclusion_proof,
395 attachments: None,
396 }
397 }
398
399 pub fn with_attachments(
408 mut self,
409 attachments: NoteAttachments,
410 ) -> Result<Self, RpcConversionError> {
411 if attachments.to_commitment() != self.metadata.attachments_commitment() {
412 return Err(RpcConversionError::InvalidField(format!(
413 "attachments recorded for note {} do not match its attachments commitment",
414 self.note_id,
415 )));
416 }
417
418 self.attachments = Some(attachments);
419 Ok(self)
420 }
421
422 pub fn note_id(&self) -> &NoteId {
423 &self.note_id
424 }
425
426 pub fn note_type(&self) -> NoteType {
427 self.metadata.note_type()
428 }
429
430 pub fn tag(&self) -> NoteTag {
431 self.metadata.tag()
432 }
433
434 pub fn sender(&self) -> AccountId {
435 self.metadata.sender()
436 }
437
438 pub fn metadata(&self) -> &NoteMetadata {
440 &self.metadata
441 }
442
443 pub fn has_attachments(&self) -> bool {
445 self.metadata.has_attachments()
446 }
447
448 pub fn attachments(&self) -> Option<&NoteAttachments> {
454 self.attachments.as_ref()
455 }
456
457 pub fn needs_attachment_fetch(&self) -> bool {
460 self.has_attachments() && self.attachments.is_none()
461 }
462
463 pub fn inclusion_proof(&self) -> &NoteInclusionProof {
464 &self.inclusion_proof
465 }
466
467 pub fn block_num(&self) -> BlockNumber {
469 self.inclusion_proof.location().block_num()
470 }
471}
472
473impl TryFrom<proto::rpc::NoteSyncRecord> for CommittedNote {
474 type Error = RpcConversionError;
475
476 fn try_from(note: proto::rpc::NoteSyncRecord) -> Result<Self, Self::Error> {
477 let proto_metadata = note
478 .metadata
479 .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(notes.metadata)))?;
480 let SyncNoteMetadata { metadata, attachments } = proto_metadata.try_into()?;
481
482 let proto_inclusion_proof = note.inclusion_proof.ok_or(
483 proto::rpc::SyncNotesResponse::missing_field(stringify!(notes.inclusion_proof)),
484 )?;
485
486 let (note_id, inclusion_proof) = note_inclusion_proof_from_proto(proto_inclusion_proof)?;
487
488 let committed = CommittedNote::new(note_id, metadata, inclusion_proof);
489
490 match attachments.into_content() {
491 Some(attachments) => committed.with_attachments(attachments),
492 None => Ok(committed),
493 }
494 }
495}
496
497#[allow(clippy::large_enum_variant)]
502pub enum FetchedNote {
503 Private(NoteId, NoteMetadata, NoteAttachments, NoteInclusionProof),
509 Public(Note, NoteInclusionProof),
511}
512
513impl FetchedNote {
514 pub fn inclusion_proof(&self) -> &NoteInclusionProof {
516 match self {
517 FetchedNote::Private(_, _, _, inclusion_proof)
518 | FetchedNote::Public(_, inclusion_proof) => inclusion_proof,
519 }
520 }
521
522 pub fn metadata(&self) -> &NoteMetadata {
524 match self {
525 FetchedNote::Private(_, metadata, ..) => metadata,
526 FetchedNote::Public(note, _) => note.metadata(),
527 }
528 }
529
530 pub fn attachments(&self) -> &NoteAttachments {
532 match self {
533 FetchedNote::Private(_, _, attachments, _) => attachments,
534 FetchedNote::Public(note, _) => note.attachments(),
535 }
536 }
537
538 pub fn id(&self) -> NoteId {
540 match self {
541 FetchedNote::Private(note_id, ..) => *note_id,
542 FetchedNote::Public(note, _) => note.id(),
543 }
544 }
545}
546
547impl TryFrom<proto::rpc::CommittedNote> for FetchedNote {
548 type Error = RpcConversionError;
549
550 fn try_from(value: proto::rpc::CommittedNote) -> Result<Self, Self::Error> {
551 let proto_inclusion_proof = value
552 .inclusion_proof
553 .ok_or_else(|| proto::rpc::CommittedNote::missing_field(stringify!(inclusion_proof)))?;
554 let (note_id, inclusion_proof) = note_inclusion_proof_from_proto(proto_inclusion_proof)?;
555
556 let note = value
557 .note
558 .ok_or_else(|| proto::rpc::CommittedNote::missing_field(stringify!(note)))?;
559
560 let partial_metadata: PartialNoteMetadata = note
561 .metadata
562 .ok_or_else(|| proto::rpc::CommittedNote::missing_field(stringify!(note.metadata)))?
563 .decode_and_verify()?;
564
565 match partial_metadata.note_type() {
568 NoteType::Public => {
569 if note.note_details.is_none() {
570 return Err(RpcConversionError::InvalidField(format!(
571 "no note details were returned for public note {note_id}"
572 )));
573 }
574
575 Ok(FetchedNote::Public(note.decode_and_verify()?, inclusion_proof))
576 },
577 NoteType::Private => {
578 if note.note_details.is_some() {
579 return Err(RpcConversionError::InvalidField(format!(
580 "note details were returned for private note {note_id}"
581 )));
582 }
583
584 let attachments: NoteAttachments = note
585 .note_attachments
586 .ok_or_else(|| {
587 proto::rpc::CommittedNote::missing_field(stringify!(note.note_attachments))
588 })?
589 .decode_and_verify()?;
590 let metadata = NoteMetadata::new(partial_metadata, &attachments);
591
592 Ok(FetchedNote::Private(note_id, metadata, attachments, inclusion_proof))
593 },
594 }
595 }
596}
597
598#[cfg(test)]
602mod tests {
603 use miden_protocol::account::{AccountIdVersion, AccountType, AssetCallbackFlag};
604 use miden_protocol::crypto::merkle::SparseMerklePath;
605 use miden_protocol::note::{NoteAssets, NoteRecipient, NoteStorage};
606 use miden_standards::code_builder::CodeBuilder;
607
608 use super::*;
609
610 fn sender() -> AccountId {
611 AccountId::dummy(
612 [1; 15],
613 AccountIdVersion::Version1,
614 AccountType::Public,
615 AssetCallbackFlag::Disabled,
616 )
617 }
618
619 fn single_word_attachment(scheme: u16, word: u32) -> NoteAttachment {
620 NoteAttachment::with_word(
621 NoteAttachmentScheme::new(scheme).unwrap(),
622 Word::from([word, word, word, word]),
623 )
624 }
625
626 fn multi_word_attachment(scheme: u16) -> NoteAttachment {
627 NoteAttachment::with_words(
628 NoteAttachmentScheme::new(scheme).unwrap(),
629 vec![Word::from([5u32, 6, 7, 8]), Word::from([9u32, 10, 11, 12])],
630 )
631 .unwrap()
632 }
633
634 fn decode_sync_metadata(attachments: &NoteAttachments) -> SyncNoteMetadata {
637 sync_metadata(sync_attachments(attachments)).try_into().unwrap()
638 }
639
640 fn bare_committed_note(metadata: NoteMetadata) -> CommittedNote {
641 let path = SparseMerklePath::from_parts(0, Vec::new()).unwrap();
642 let inclusion_proof =
643 NoteInclusionProof::new(BlockNumber::GENESIS, 0, path).expect("index 0 is in range");
644
645 CommittedNote::new(NoteId::from_raw(Word::empty()), metadata, inclusion_proof)
646 }
647
648 fn committed_note(decoded: SyncNoteMetadata) -> CommittedNote {
649 let committed = bare_committed_note(decoded.metadata);
650
651 match decoded.attachments.into_content() {
652 Some(attachments) => committed.with_attachments(attachments).unwrap(),
653 None => committed,
654 }
655 }
656
657 fn sync_attachments(attachments: &NoteAttachments) -> Vec<proto::rpc::NoteSyncAttachment> {
660 attachments
661 .iter()
662 .map(|attachment| {
663 let payload = if attachment.num_words() == 1 {
664 proto::rpc::note_sync_attachment::Payload::Value(
665 attachment.content().as_words()[0].into(),
666 )
667 } else {
668 proto::rpc::note_sync_attachment::Payload::Commitment(
669 attachment.to_commitment().into(),
670 )
671 };
672
673 proto::rpc::NoteSyncAttachment {
674 scheme: u32::from(attachment.attachment_scheme().as_u16()),
675 payload: Some(payload),
676 }
677 })
678 .collect()
679 }
680
681 fn sync_metadata(
682 attachments: Vec<proto::rpc::NoteSyncAttachment>,
683 ) -> proto::rpc::NoteSyncMetadata {
684 proto::rpc::NoteSyncMetadata {
685 sender: Some(sender().into()),
686 note_type: proto::note::NoteType::from(NoteType::Private) as i32,
687 tag: 7,
688 attachments,
689 version: proto::note::NoteVersion::V1 as i32,
690 }
691 }
692
693 #[test]
694 fn sync_metadata_reconstructs_metadata_with_mixed_attachments() {
695 let attachments =
696 NoteAttachments::new(vec![single_word_attachment(42, 1), multi_word_attachment(100)])
697 .unwrap();
698
699 let expected = NoteMetadata::new(
700 PartialNoteMetadata::new(sender(), NoteType::Private).with_tag(NoteTag::new(7)),
701 &attachments,
702 );
703
704 assert_eq!(decode_sync_metadata(&attachments).metadata, expected);
705 }
706
707 #[test]
708 fn sync_metadata_reconstructs_metadata_without_attachments() {
709 let attachments = NoteAttachments::empty();
710 let expected = NoteMetadata::new(
711 PartialNoteMetadata::new(sender(), NoteType::Private).with_tag(NoteTag::new(7)),
712 &attachments,
713 );
714
715 let decoded: SyncNoteMetadata = sync_metadata(Vec::new()).try_into().unwrap();
716
717 assert_eq!(decoded.metadata, expected);
718 }
719
720 #[test]
723 fn sync_metadata_reports_attachments_sent_verbatim() {
724 let attachments = NoteAttachments::new(vec![
725 single_word_attachment(42, 1),
726 single_word_attachment(64, 2),
727 ])
728 .unwrap();
729
730 let decoded = decode_sync_metadata(&attachments);
731
732 let committed = committed_note(decoded);
733 assert_eq!(committed.attachments(), Some(&attachments));
734 assert!(!committed.needs_attachment_fetch());
735 }
736
737 #[test]
740 fn sync_metadata_reconstructs_a_full_attachment_set() {
741 let attachments = NoteAttachments::new(
742 (0..NoteAttachments::MAX_COUNT)
743 .map(|i| {
744 let scheme = u16::try_from(i).unwrap() + 42;
745 single_word_attachment(scheme, u32::try_from(i).unwrap() + 1)
746 })
747 .collect(),
748 )
749 .unwrap();
750
751 let expected = NoteMetadata::new(
752 PartialNoteMetadata::new(sender(), NoteType::Private).with_tag(NoteTag::new(7)),
753 &attachments,
754 );
755 let decoded = decode_sync_metadata(&attachments);
756
757 assert_eq!(decoded.metadata, expected);
758 let committed = committed_note(decoded);
759 assert_eq!(committed.attachments(), Some(&attachments));
760 assert!(!committed.needs_attachment_fetch());
761 }
762
763 #[test]
765 fn sync_metadata_reports_empty_attachments() {
766 let decoded: SyncNoteMetadata = sync_metadata(Vec::new()).try_into().unwrap();
767
768 let committed = committed_note(decoded);
769 assert_eq!(committed.attachments(), Some(&NoteAttachments::empty()));
770 assert!(!committed.needs_attachment_fetch());
771 }
772
773 #[test]
776 fn sync_metadata_withholds_partially_reported_attachments() {
777 let attachments =
778 NoteAttachments::new(vec![single_word_attachment(42, 1), multi_word_attachment(100)])
779 .unwrap();
780
781 let decoded = decode_sync_metadata(&attachments);
782
783 assert!(matches!(decoded.attachments, ReportedAttachments::Commitments(_)));
784 assert!(committed_note(decoded).needs_attachment_fetch());
785 }
786
787 #[test]
790 fn sync_metadata_withholds_single_word_attachment_sent_as_commitment() {
791 let attachment = single_word_attachment(42, 1);
792 let proto_attachments = vec![proto::rpc::NoteSyncAttachment {
793 scheme: u32::from(attachment.attachment_scheme().as_u16()),
794 payload: Some(proto::rpc::note_sync_attachment::Payload::Commitment(
795 attachment.to_commitment().into(),
796 )),
797 }];
798 let attachments = NoteAttachments::new(vec![attachment]).unwrap();
799
800 let decoded: SyncNoteMetadata = sync_metadata(proto_attachments).try_into().unwrap();
801
802 assert_eq!(
804 decoded.metadata,
805 NoteMetadata::new(
806 PartialNoteMetadata::new(sender(), NoteType::Private).with_tag(NoteTag::new(7)),
807 &attachments,
808 )
809 );
810 assert!(matches!(decoded.attachments, ReportedAttachments::Commitments(_)));
811 assert!(committed_note(decoded).needs_attachment_fetch());
812 }
813
814 #[test]
815 fn sync_metadata_rejects_too_many_attachments() {
816 let attachment = proto::rpc::NoteSyncAttachment {
817 scheme: 42,
818 payload: Some(proto::rpc::note_sync_attachment::Payload::Value(Word::empty().into())),
819 };
820 let attachments = vec![attachment; NoteAttachments::MAX_COUNT + 1];
821
822 let err = SyncNoteMetadata::try_from(sync_metadata(attachments)).unwrap_err();
823
824 assert!(matches!(err, RpcConversionError::InvalidField(_)), "got {err:?}");
825 }
826
827 #[test]
828 fn sync_metadata_rejects_reserved_absent_scheme() {
829 let attachments = vec![proto::rpc::NoteSyncAttachment {
830 scheme: 0,
831 payload: Some(proto::rpc::note_sync_attachment::Payload::Value(Word::empty().into())),
832 }];
833
834 let err = SyncNoteMetadata::try_from(sync_metadata(attachments)).unwrap_err();
835
836 assert!(matches!(err, RpcConversionError::InvalidField(_)), "got {err:?}");
837 }
838
839 #[test]
840 fn sync_metadata_rejects_missing_attachment_payload() {
841 let attachments = vec![proto::rpc::NoteSyncAttachment { scheme: 42, payload: None }];
842
843 let err = SyncNoteMetadata::try_from(sync_metadata(attachments)).unwrap_err();
844
845 assert!(
846 matches!(err, RpcConversionError::MissingFieldInProtobufRepresentation { .. }),
847 "got {err:?}"
848 );
849 }
850
851 #[test]
852 fn sync_metadata_rejects_an_unusable_note_version() {
853 for version in [proto::note::NoteVersion::Unspecified as i32, 999] {
854 let mut wire = sync_metadata(Vec::new());
855 wire.version = version;
856
857 let err = SyncNoteMetadata::try_from(wire).unwrap_err();
858
859 assert!(matches!(err, RpcConversionError::CanonicalConversion(_)), "got {err:?}");
860 }
861 }
862
863 #[test]
864 fn synced_note_rejects_a_body_for_a_private_note() {
865 let decoded: SyncNoteMetadata = sync_metadata(Vec::new()).try_into().unwrap();
866 let committed = committed_note(decoded);
867
868 let note_script = CodeBuilder::new()
869 .compile_note_script("@note_script\npub proc main\n nop\nend")
870 .unwrap();
871 let recipient =
872 NoteRecipient::new(Word::empty(), note_script, NoteStorage::new(vec![]).unwrap());
873 let details = NoteDetails::new(NoteAssets::new(vec![]).unwrap(), recipient);
874
875 let err = SyncedNote::new(committed, Some(details), NoteAttachments::empty()).unwrap_err();
876
877 assert!(matches!(err, RpcError::InvalidResponse(_)), "got {err:?}");
878 }
879
880 #[test]
883 fn synced_note_rejects_unresolved_attachments() {
884 let attachments = NoteAttachments::new(vec![multi_word_attachment(100)]).unwrap();
885 let committed = committed_note(decode_sync_metadata(&attachments));
886
887 let err = SyncedNote::new(committed, None, NoteAttachments::empty()).unwrap_err();
888
889 assert!(matches!(err, RpcError::InvalidResponse(_)), "got {err:?}");
890 }
891}