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<'a>(&'a [Word]);
139
140impl SequentialCommit for AttachmentCommitments<'_> {
141 type Commitment = Word;
142
143 fn to_elements(&self) -> Vec<Felt> {
144 let mut elements = Vec::with_capacity(self.0.len() * miden_protocol::WORD_SIZE);
145 for commitment in self.0 {
146 elements.extend_from_slice(commitment.as_elements());
147 }
148 elements
149 }
150}
151
152#[derive(Debug)]
155enum ReportedAttachments {
156 Full(NoteAttachments),
158 Commitments(Vec<Word>),
161}
162
163impl ReportedAttachments {
164 fn to_commitment(&self) -> Word {
166 match self {
167 Self::Full(attachments) => attachments.to_commitment(),
168 Self::Commitments(commitments) => AttachmentCommitments(commitments).to_commitment(),
169 }
170 }
171
172 fn into_content(self) -> Option<NoteAttachments> {
174 match self {
175 Self::Full(attachments) => Some(attachments),
176 Self::Commitments(_) => None,
177 }
178 }
179}
180
181#[derive(Debug)]
184struct SyncNoteMetadata {
185 metadata: NoteMetadata,
187 attachments: ReportedAttachments,
189}
190
191impl TryFrom<proto::note::NoteSyncMetadata> for SyncNoteMetadata {
192 type Error = RpcConversionError;
193
194 fn try_from(value: proto::note::NoteSyncMetadata) -> Result<Self, Self::Error> {
195 let sender = value
196 .sender
197 .ok_or_else(|| proto::note::NoteSyncMetadata::missing_field(stringify!(sender)))?
198 .try_into()?;
199 let note_type = note_type_from_proto(value.note_type)?;
200 let tag = NoteTag::new(value.tag);
201 let partial_metadata = PartialNoteMetadata::new(sender, note_type).with_tag(tag);
202
203 if value.attachments.len() > NoteAttachments::MAX_COUNT {
204 return Err(RpcConversionError::InvalidField(format!(
205 "attachments length {} exceeds NoteAttachments::MAX_COUNT",
206 value.attachments.len(),
207 )));
208 }
209
210 let mut attachment_headers = [NoteAttachmentHeader::absent(); NoteAttachments::MAX_COUNT];
211 let mut commitments = Vec::with_capacity(value.attachments.len());
212 let mut contents = Some(Vec::with_capacity(value.attachments.len()));
215
216 for (slot, attachment) in value.attachments.into_iter().enumerate() {
217 let raw_scheme = u16::try_from(attachment.scheme).map_err(|_| {
218 RpcConversionError::InvalidField(format!(
219 "attachments[{slot}].scheme={} does not fit in u16",
220 attachment.scheme,
221 ))
222 })?;
223 let scheme = NoteAttachmentScheme::new(raw_scheme).map_err(|err| {
224 RpcConversionError::InvalidField(format!("attachments[{slot}].scheme: {err}"))
225 })?;
226 attachment_headers[slot] = NoteAttachmentHeader::new(scheme);
227
228 let payload = attachment.payload.ok_or_else(|| {
229 proto::note::NoteSyncAttachment::missing_field(stringify!(payload))
230 })?;
231 match payload {
236 proto::note::note_sync_attachment::Payload::Value(value) => {
237 let attachment = NoteAttachment::with_word(scheme, Word::try_from(value)?);
238 commitments.push(attachment.to_commitment());
239 if let Some(contents) = contents.as_mut() {
240 contents.push(attachment);
241 }
242 },
243 proto::note::note_sync_attachment::Payload::Commitment(commitment) => {
244 commitments.push(Word::try_from(commitment)?);
245 contents = None;
246 },
247 }
248 }
249
250 let attachments = match contents {
251 Some(contents) => {
252 ReportedAttachments::Full(NoteAttachments::new(contents).map_err(|err| {
253 RpcConversionError::InvalidField(format!("attachments: {err}"))
254 })?)
255 },
256 None => ReportedAttachments::Commitments(commitments),
257 };
258
259 Ok(SyncNoteMetadata {
260 metadata: NoteMetadata::from_parts(
261 partial_metadata,
262 attachment_headers,
263 attachments.to_commitment(),
264 ),
265 attachments,
266 })
267 }
268}
269
270impl TryFrom<&proto::note::NoteMetadata> for PartialNoteMetadata {
271 type Error = RpcConversionError;
272
273 fn try_from(value: &proto::note::NoteMetadata) -> Result<Self, Self::Error> {
274 let sender = value
275 .sender
276 .clone()
277 .ok_or_else(|| proto::note::NoteMetadata::missing_field(stringify!(sender)))?
278 .try_into()?;
279 let note_type = note_type_from_proto(value.note_type)?;
280 let tag = NoteTag::new(value.tag);
281
282 Ok(PartialNoteMetadata::new(sender, note_type).with_tag(tag))
283 }
284}
285
286impl From<NoteMetadata> for proto::note::NoteMetadata {
287 fn from(value: NoteMetadata) -> Self {
288 proto::note::NoteMetadata {
289 sender: Some(value.sender().into()),
290 note_type: note_type_to_proto(value.note_type()),
291 tag: value.tag().as_u32(),
292 attachment_schemes: attachment_schemes_to_proto(value.attachment_headers()),
293 attachments_commitment: Some(value.attachments_commitment().into()),
294 }
295 }
296}
297
298impl TryFrom<proto::note::NoteHeader> for NoteHeader {
299 type Error = RpcConversionError;
300
301 fn try_from(value: proto::note::NoteHeader) -> Result<Self, Self::Error> {
302 let details_commitment_word: Word = value
303 .details_commitment
304 .ok_or(proto::note::NoteHeader::missing_field(stringify!(details_commitment)))?
305 .try_into()?;
306 let metadata = value
307 .metadata
308 .ok_or(proto::note::NoteHeader::missing_field(stringify!(metadata)))?
309 .try_into()?;
310 Ok(NoteHeader::new(
311 NoteDetailsCommitment::from_raw(details_commitment_word),
312 metadata,
313 ))
314 }
315}
316
317impl TryFrom<proto::note::NoteInclusionInBlockProof> for NoteInclusionProof {
318 type Error = RpcConversionError;
319
320 fn try_from(value: proto::note::NoteInclusionInBlockProof) -> Result<Self, Self::Error> {
321 Ok(NoteInclusionProof::new(
322 value.block_num.into(),
323 u16::try_from(value.note_index_in_block)
324 .map_err(|_| RpcConversionError::InvalidField("NoteIndexInBlock".into()))?,
325 value
326 .inclusion_path
327 .ok_or_else(|| {
328 proto::note::NoteInclusionInBlockProof::missing_field(stringify!(
329 inclusion_path
330 ))
331 })?
332 .try_into()?,
333 )?)
334 }
335}
336
337#[derive(Debug, Clone)]
342pub struct SyncNotesBlock {
343 pub block_header: BlockHeader,
345 pub mmr_path: MerklePath,
347 pub notes: BTreeMap<NoteId, CommittedNote>,
349}
350
351impl TryFrom<proto::rpc::sync_notes_response::NoteSyncBlock> for SyncNotesBlock {
352 type Error = RpcError;
353
354 fn try_from(
355 block: proto::rpc::sync_notes_response::NoteSyncBlock,
356 ) -> Result<Self, Self::Error> {
357 let block_header = block
358 .block_header
359 .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(blocks.block_header)))?
360 .try_into()?;
361
362 let mmr_path = block
363 .mmr_path
364 .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(blocks.mmr_path)))?
365 .try_into()?;
366
367 let notes: BTreeMap<NoteId, CommittedNote> = block
368 .notes
369 .into_iter()
370 .map(|n| {
371 let note = CommittedNote::try_from(n)?;
372 Ok((*note.note_id(), note))
373 })
374 .collect::<Result<_, RpcConversionError>>()?;
375
376 Ok(SyncNotesBlock { block_header, mmr_path, notes })
377 }
378}
379
380#[derive(Debug, Clone)]
389pub struct ResolvedSyncNotesBlock {
390 pub block_header: BlockHeader,
392 pub mmr_path: MerklePath,
394 pub notes: BTreeMap<NoteId, SyncedNote>,
396}
397
398#[derive(Debug, Clone)]
402pub struct SyncedNote {
403 pub committed: CommittedNote,
405 pub details: Option<NoteDetails>,
408 pub attachments: NoteAttachments,
411}
412
413impl SyncedNote {
414 pub fn new(
430 committed: CommittedNote,
431 details: Option<NoteDetails>,
432 attachments: NoteAttachments,
433 ) -> Result<Self, RpcError> {
434 if details.is_some() && committed.note_type() != NoteType::Public {
435 return Err(RpcError::InvalidResponse(format!(
436 "a note body was returned for private note {}",
437 committed.note_id()
438 )));
439 }
440
441 if attachments.to_commitment() != committed.metadata().attachments_commitment() {
442 return Err(RpcError::InvalidResponse(format!(
443 "the attachments resolved for note {} do not match the note's attachments \
444 commitment",
445 committed.note_id()
446 )));
447 }
448
449 Ok(Self { committed, details, attachments })
450 }
451}
452
453#[derive(Debug, Clone)]
458pub struct CommittedNote {
459 note_id: NoteId,
461 metadata: NoteMetadata,
464 inclusion_proof: NoteInclusionProof,
466 attachments: Option<NoteAttachments>,
469}
470
471impl CommittedNote {
472 pub fn new(
473 note_id: NoteId,
474 metadata: NoteMetadata,
475 inclusion_proof: NoteInclusionProof,
476 ) -> Self {
477 Self {
478 note_id,
479 metadata,
480 inclusion_proof,
481 attachments: None,
482 }
483 }
484
485 pub fn with_attachments(
494 mut self,
495 attachments: NoteAttachments,
496 ) -> Result<Self, RpcConversionError> {
497 if attachments.to_commitment() != self.metadata.attachments_commitment() {
498 return Err(RpcConversionError::InvalidField(format!(
499 "attachments recorded for note {} do not match its attachments commitment",
500 self.note_id,
501 )));
502 }
503
504 self.attachments = Some(attachments);
505 Ok(self)
506 }
507
508 pub fn note_id(&self) -> &NoteId {
509 &self.note_id
510 }
511
512 pub fn note_type(&self) -> NoteType {
513 self.metadata.note_type()
514 }
515
516 pub fn tag(&self) -> NoteTag {
517 self.metadata.tag()
518 }
519
520 pub fn sender(&self) -> AccountId {
521 self.metadata.sender()
522 }
523
524 pub fn metadata(&self) -> &NoteMetadata {
526 &self.metadata
527 }
528
529 pub fn has_attachments(&self) -> bool {
531 self.metadata.has_attachments()
532 }
533
534 pub fn attachments(&self) -> Option<&NoteAttachments> {
540 self.attachments.as_ref()
541 }
542
543 pub fn needs_attachment_fetch(&self) -> bool {
546 self.has_attachments() && self.attachments.is_none()
547 }
548
549 pub fn inclusion_proof(&self) -> &NoteInclusionProof {
550 &self.inclusion_proof
551 }
552
553 pub fn block_num(&self) -> BlockNumber {
555 self.inclusion_proof.location().block_num()
556 }
557}
558
559impl TryFrom<proto::note::NoteSyncRecord> for CommittedNote {
560 type Error = RpcConversionError;
561
562 fn try_from(note: proto::note::NoteSyncRecord) -> Result<Self, Self::Error> {
563 let proto_metadata = note
564 .metadata
565 .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(notes.metadata)))?;
566 let SyncNoteMetadata { metadata, attachments } = proto_metadata.try_into()?;
567
568 let proto_inclusion_proof = note.inclusion_proof.ok_or(
569 proto::rpc::SyncNotesResponse::missing_field(stringify!(notes.inclusion_proof)),
570 )?;
571
572 let note_id: NoteId = proto_inclusion_proof
573 .note_id
574 .ok_or(proto::rpc::SyncNotesResponse::missing_field(stringify!(
575 notes.inclusion_proof.note_id
576 )))?
577 .try_into()?;
578
579 let inclusion_proof: NoteInclusionProof = proto_inclusion_proof.try_into()?;
580
581 let committed = CommittedNote::new(note_id, metadata, inclusion_proof);
582
583 match attachments.into_content() {
584 Some(attachments) => committed.with_attachments(attachments),
585 None => Ok(committed),
586 }
587 }
588}
589
590#[allow(clippy::large_enum_variant)]
595pub enum FetchedNote {
596 Private(NoteId, NoteMetadata, NoteAttachments, NoteInclusionProof),
602 Public(Note, NoteInclusionProof),
604}
605
606impl FetchedNote {
607 pub fn inclusion_proof(&self) -> &NoteInclusionProof {
609 match self {
610 FetchedNote::Private(_, _, _, inclusion_proof)
611 | FetchedNote::Public(_, inclusion_proof) => inclusion_proof,
612 }
613 }
614
615 pub fn metadata(&self) -> &NoteMetadata {
617 match self {
618 FetchedNote::Private(_, metadata, ..) => metadata,
619 FetchedNote::Public(note, _) => note.metadata(),
620 }
621 }
622
623 pub fn attachments(&self) -> &NoteAttachments {
625 match self {
626 FetchedNote::Private(_, _, attachments, _) => attachments,
627 FetchedNote::Public(note, _) => note.attachments(),
628 }
629 }
630
631 pub fn id(&self) -> NoteId {
633 match self {
634 FetchedNote::Private(note_id, ..) => *note_id,
635 FetchedNote::Public(note, _) => note.id(),
636 }
637 }
638}
639
640impl TryFrom<proto::note::CommittedNote> for FetchedNote {
641 type Error = RpcConversionError;
642
643 fn try_from(value: proto::note::CommittedNote) -> Result<Self, Self::Error> {
644 let inclusion_proof = value.inclusion_proof.ok_or_else(|| {
645 proto::note::CommittedNote::missing_field(stringify!(inclusion_proof))
646 })?;
647
648 let note_id: NoteId = inclusion_proof
649 .note_id
650 .ok_or_else(|| {
651 proto::note::CommittedNote::missing_field(stringify!(inclusion_proof.note_id))
652 })?
653 .try_into()?;
654
655 let inclusion_proof = NoteInclusionProof::try_from(inclusion_proof)?;
656
657 let note = value
658 .note
659 .ok_or_else(|| proto::note::CommittedNote::missing_field(stringify!(note)))?;
660
661 let proto_metadata = note
662 .metadata
663 .ok_or_else(|| proto::note::CommittedNote::missing_field(stringify!(note.metadata)))?;
664 let metadata: NoteMetadata = proto_metadata.clone().try_into()?;
665 let partial_metadata: PartialNoteMetadata = (&proto_metadata).try_into()?;
666
667 let attachments = if note.attachments.is_empty() {
668 NoteAttachments::empty()
669 } else {
670 NoteAttachments::read_from_bytes(¬e.attachments)?
671 };
672
673 if let Some(detail_bytes) = note.details {
674 let details = NoteDetails::read_from_bytes(&detail_bytes)?;
675 let (assets, recipient) = details.into_parts();
676
677 Ok(FetchedNote::Public(
678 Note::with_attachments(assets, partial_metadata, recipient, attachments),
679 inclusion_proof,
680 ))
681 } else {
682 Ok(FetchedNote::Private(note_id, metadata, attachments, inclusion_proof))
683 }
684 }
685}
686
687impl TryFrom<proto::note::NoteScript> for NoteScript {
691 type Error = RpcConversionError;
692
693 fn try_from(note_script: proto::note::NoteScript) -> Result<Self, Self::Error> {
694 let mast_forest = MastForest::read_from_bytes(¬e_script.mast)?;
695 let entrypoint = MastNodeId::from_u32_safe(note_script.entrypoint, &mast_forest)?;
696 Ok(NoteScript::from_parts(alloc::sync::Arc::new(mast_forest), entrypoint))
697 }
698}
699
700#[cfg(test)]
704mod tests {
705 use miden_protocol::account::{AccountIdVersion, AccountType, AssetCallbackFlag};
706 use miden_protocol::crypto::merkle::SparseMerklePath;
707 use miden_protocol::note::{NoteAssets, NoteRecipient, NoteStorage};
708 use miden_standards::code_builder::CodeBuilder;
709
710 use super::*;
711
712 fn sender() -> AccountId {
713 AccountId::dummy(
714 [1; 15],
715 AccountIdVersion::Version1,
716 AccountType::Public,
717 AssetCallbackFlag::Disabled,
718 )
719 }
720
721 fn single_word_attachment(scheme: u16, word: u32) -> NoteAttachment {
722 NoteAttachment::with_word(
723 NoteAttachmentScheme::new(scheme).unwrap(),
724 Word::from([word, word, word, word]),
725 )
726 }
727
728 fn multi_word_attachment(scheme: u16) -> NoteAttachment {
729 NoteAttachment::with_words(
730 NoteAttachmentScheme::new(scheme).unwrap(),
731 vec![Word::from([5u32, 6, 7, 8]), Word::from([9u32, 10, 11, 12])],
732 )
733 .unwrap()
734 }
735
736 fn decode_sync_metadata(attachments: &NoteAttachments) -> SyncNoteMetadata {
739 sync_metadata(sync_attachments(attachments)).try_into().unwrap()
740 }
741
742 fn bare_committed_note(metadata: NoteMetadata) -> CommittedNote {
743 let path = SparseMerklePath::from_parts(0, Vec::new()).unwrap();
744 let inclusion_proof =
745 NoteInclusionProof::new(BlockNumber::GENESIS, 0, path).expect("index 0 is in range");
746
747 CommittedNote::new(NoteId::from_raw(Word::empty()), metadata, inclusion_proof)
748 }
749
750 fn committed_note(decoded: SyncNoteMetadata) -> CommittedNote {
751 let committed = bare_committed_note(decoded.metadata);
752
753 match decoded.attachments.into_content() {
754 Some(attachments) => committed.with_attachments(attachments).unwrap(),
755 None => committed,
756 }
757 }
758
759 fn sync_attachments(attachments: &NoteAttachments) -> Vec<proto::note::NoteSyncAttachment> {
762 attachments
763 .iter()
764 .map(|attachment| {
765 let payload = if attachment.num_words() == 1 {
766 proto::note::note_sync_attachment::Payload::Value(
767 attachment.content().as_words()[0].into(),
768 )
769 } else {
770 proto::note::note_sync_attachment::Payload::Commitment(
771 attachment.to_commitment().into(),
772 )
773 };
774
775 proto::note::NoteSyncAttachment {
776 scheme: u32::from(attachment.attachment_scheme().as_u16()),
777 payload: Some(payload),
778 }
779 })
780 .collect()
781 }
782
783 fn sync_metadata(
784 attachments: Vec<proto::note::NoteSyncAttachment>,
785 ) -> proto::note::NoteSyncMetadata {
786 proto::note::NoteSyncMetadata {
787 sender: Some(sender().into()),
788 note_type: note_type_to_proto(NoteType::Private),
789 tag: 7,
790 attachments,
791 }
792 }
793
794 #[test]
795 fn sync_metadata_reconstructs_metadata_with_mixed_attachments() {
796 let attachments =
797 NoteAttachments::new(vec![single_word_attachment(42, 1), multi_word_attachment(100)])
798 .unwrap();
799
800 let expected = NoteMetadata::new(
801 PartialNoteMetadata::new(sender(), NoteType::Private).with_tag(NoteTag::new(7)),
802 &attachments,
803 );
804
805 assert_eq!(decode_sync_metadata(&attachments).metadata, expected);
806 }
807
808 #[test]
809 fn sync_metadata_reconstructs_metadata_without_attachments() {
810 let attachments = NoteAttachments::empty();
811 let expected = NoteMetadata::new(
812 PartialNoteMetadata::new(sender(), NoteType::Private).with_tag(NoteTag::new(7)),
813 &attachments,
814 );
815
816 let decoded: SyncNoteMetadata = sync_metadata(Vec::new()).try_into().unwrap();
817
818 assert_eq!(decoded.metadata, expected);
819 }
820
821 #[test]
824 fn sync_metadata_reports_attachments_sent_verbatim() {
825 let attachments = NoteAttachments::new(vec![
826 single_word_attachment(42, 1),
827 single_word_attachment(64, 2),
828 ])
829 .unwrap();
830
831 let decoded = decode_sync_metadata(&attachments);
832
833 let committed = committed_note(decoded);
834 assert_eq!(committed.attachments(), Some(&attachments));
835 assert!(!committed.needs_attachment_fetch());
836 }
837
838 #[test]
841 fn sync_metadata_reconstructs_a_full_attachment_set() {
842 let attachments = NoteAttachments::new(
843 (0..NoteAttachments::MAX_COUNT)
844 .map(|i| {
845 let scheme = u16::try_from(i).unwrap() + 42;
846 single_word_attachment(scheme, u32::try_from(i).unwrap() + 1)
847 })
848 .collect(),
849 )
850 .unwrap();
851
852 let expected = NoteMetadata::new(
853 PartialNoteMetadata::new(sender(), NoteType::Private).with_tag(NoteTag::new(7)),
854 &attachments,
855 );
856 let decoded = decode_sync_metadata(&attachments);
857
858 assert_eq!(decoded.metadata, expected);
859 let committed = committed_note(decoded);
860 assert_eq!(committed.attachments(), Some(&attachments));
861 assert!(!committed.needs_attachment_fetch());
862 }
863
864 #[test]
866 fn sync_metadata_reports_empty_attachments() {
867 let decoded: SyncNoteMetadata = sync_metadata(Vec::new()).try_into().unwrap();
868
869 let committed = committed_note(decoded);
870 assert_eq!(committed.attachments(), Some(&NoteAttachments::empty()));
871 assert!(!committed.needs_attachment_fetch());
872 }
873
874 #[test]
877 fn sync_metadata_withholds_partially_reported_attachments() {
878 let attachments =
879 NoteAttachments::new(vec![single_word_attachment(42, 1), multi_word_attachment(100)])
880 .unwrap();
881
882 let decoded = decode_sync_metadata(&attachments);
883
884 assert!(matches!(decoded.attachments, ReportedAttachments::Commitments(_)));
885 assert!(committed_note(decoded).needs_attachment_fetch());
886 }
887
888 #[test]
891 fn sync_metadata_withholds_single_word_attachment_sent_as_commitment() {
892 let attachment = single_word_attachment(42, 1);
893 let proto_attachments = vec![proto::note::NoteSyncAttachment {
894 scheme: u32::from(attachment.attachment_scheme().as_u16()),
895 payload: Some(proto::note::note_sync_attachment::Payload::Commitment(
896 attachment.to_commitment().into(),
897 )),
898 }];
899 let attachments = NoteAttachments::new(vec![attachment]).unwrap();
900
901 let decoded: SyncNoteMetadata = sync_metadata(proto_attachments).try_into().unwrap();
902
903 assert_eq!(
905 decoded.metadata,
906 NoteMetadata::new(
907 PartialNoteMetadata::new(sender(), NoteType::Private).with_tag(NoteTag::new(7)),
908 &attachments,
909 )
910 );
911 assert!(matches!(decoded.attachments, ReportedAttachments::Commitments(_)));
912 assert!(committed_note(decoded).needs_attachment_fetch());
913 }
914
915 #[test]
916 fn sync_metadata_rejects_too_many_attachments() {
917 let attachment = proto::note::NoteSyncAttachment {
918 scheme: 42,
919 payload: Some(proto::note::note_sync_attachment::Payload::Value(Word::empty().into())),
920 };
921 let attachments = vec![attachment; NoteAttachments::MAX_COUNT + 1];
922
923 let err = SyncNoteMetadata::try_from(sync_metadata(attachments)).unwrap_err();
924
925 assert!(matches!(err, RpcConversionError::InvalidField(_)), "got {err:?}");
926 }
927
928 #[test]
929 fn sync_metadata_rejects_reserved_absent_scheme() {
930 let attachments = vec![proto::note::NoteSyncAttachment {
931 scheme: 0,
932 payload: Some(proto::note::note_sync_attachment::Payload::Value(Word::empty().into())),
933 }];
934
935 let err = SyncNoteMetadata::try_from(sync_metadata(attachments)).unwrap_err();
936
937 assert!(matches!(err, RpcConversionError::InvalidField(_)), "got {err:?}");
938 }
939
940 #[test]
941 fn sync_metadata_rejects_missing_attachment_payload() {
942 let attachments = vec![proto::note::NoteSyncAttachment { scheme: 42, payload: None }];
943
944 let err = SyncNoteMetadata::try_from(sync_metadata(attachments)).unwrap_err();
945
946 assert!(
947 matches!(err, RpcConversionError::MissingFieldInProtobufRepresentation { .. }),
948 "got {err:?}"
949 );
950 }
951
952 #[test]
953 fn synced_note_rejects_a_body_for_a_private_note() {
954 let decoded: SyncNoteMetadata = sync_metadata(Vec::new()).try_into().unwrap();
955 let committed = committed_note(decoded);
956
957 let note_script = CodeBuilder::new()
958 .compile_note_script("@note_script\npub proc main\n nop\nend")
959 .unwrap();
960 let recipient =
961 NoteRecipient::new(Word::empty(), note_script, NoteStorage::new(vec![]).unwrap());
962 let details = NoteDetails::new(NoteAssets::new(vec![]).unwrap(), recipient);
963
964 let err = SyncedNote::new(committed, Some(details), NoteAttachments::empty()).unwrap_err();
965
966 assert!(matches!(err, RpcError::InvalidResponse(_)), "got {err:?}");
967 }
968
969 #[test]
972 fn synced_note_rejects_unresolved_attachments() {
973 let attachments = NoteAttachments::new(vec![multi_word_attachment(100)]).unwrap();
974 let committed = committed_note(decode_sync_metadata(&attachments));
975
976 let err = SyncedNote::new(committed, None, NoteAttachments::empty()).unwrap_err();
977
978 assert!(matches!(err, RpcError::InvalidResponse(_)), "got {err:?}");
979 }
980}