Skip to main content

miden_standards/note/
p2ide.rs

1use alloc::vec::Vec;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::assembly::Path;
5use miden_protocol::asset::Asset;
6use miden_protocol::block::BlockNumber;
7use miden_protocol::crypto::rand::FeltRng;
8use miden_protocol::errors::NoteError;
9use miden_protocol::note::{
10    Note,
11    NoteAssets,
12    NoteAttachment,
13    NoteAttachments,
14    NoteRecipient,
15    NoteScript,
16    NoteScriptRoot,
17    NoteStorage,
18    NoteTag,
19    NoteType,
20    PartialNoteMetadata,
21};
22use miden_protocol::utils::sync::LazyLock;
23use miden_protocol::{Felt, Word};
24
25use super::decode_optional_block_height;
26use crate::StandardsLib;
27use crate::note::costs::{NoteConsumptionCost, P2IDE_CONSUMPTION_CYCLES};
28// NOTE SCRIPT
29// ================================================================================================
30
31/// Path to the P2IDE note script procedure in the standards library.
32const P2IDE_SCRIPT_PATH: &str = "::miden::standards::notes::p2ide::main";
33
34// Initialize the P2IDE note script only once
35static P2IDE_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
36    let standards_lib = StandardsLib::default();
37    let path = Path::new(P2IDE_SCRIPT_PATH);
38    NoteScript::from_package_reference(standards_lib.as_ref(), path)
39        .expect("Standards library contains P2IDE note script procedure")
40});
41
42// P2IDE NOTE
43// ================================================================================================
44
45/// Pay-to-ID Extended (P2IDE) note abstraction.
46///
47/// A P2IDE note enables transferring assets to a target account specified in the note storage.
48/// The note may optionally include:
49///
50/// - A reclaim height allowing the reclaimer to recover assets if the note remains unconsumed
51/// - A timelock height preventing consumption before a given block
52///
53/// These constraints are encoded in `P2ideNoteStorage` and enforced by the associated note script.
54#[derive(Debug, Clone)]
55pub struct P2ideNote {
56    sender: AccountId,
57    storage: P2ideNoteStorage,
58    serial_number: Word,
59    note_type: NoteType,
60    assets: NoteAssets,
61    attachments: NoteAttachments,
62}
63
64#[bon::bon]
65impl P2ideNote {
66    /// Builds a new [`P2ideNote`].
67    ///
68    /// # Errors
69    ///
70    /// Returns an error if:
71    /// - No assets were provided.
72    /// - The assets or attachments exceed their protocol limits (see [`NoteAssets::new`] and
73    ///   [`NoteAttachments::new`]).
74    #[builder]
75    pub fn new(
76        #[builder(field)] assets: Vec<Asset>,
77        #[builder(field)] attachments: Vec<NoteAttachment>,
78        sender: AccountId,
79        target: AccountId,
80        reclaimer: Option<AccountId>,
81        reclaim_height: Option<BlockNumber>,
82        timelock_height: Option<BlockNumber>,
83        serial_number: Word,
84        #[builder(default)] note_type: NoteType,
85    ) -> Result<Self, NoteError> {
86        if assets.is_empty() {
87            return Err(NoteError::other("a P2IDE note must contain at least one asset"));
88        }
89
90        // The reclaimer is the account allowed to reclaim the note; it defaults to the sender.
91        let reclaimer = reclaimer.unwrap_or(sender);
92        let storage = P2ideNoteStorage::new(reclaimer, target, reclaim_height, timelock_height);
93        let assets = NoteAssets::new(assets)?;
94        let attachments = NoteAttachments::new(attachments)?;
95
96        Ok(Self {
97            sender,
98            storage,
99            serial_number,
100            note_type,
101            assets,
102            attachments,
103        })
104    }
105}
106
107impl P2ideNote {
108    // CONSTANTS
109    // --------------------------------------------------------------------------------------------
110
111    /// Expected number of storage items of the P2IDE note.
112    pub const NUM_STORAGE_ITEMS: usize = P2ideNoteStorage::NUM_ITEMS;
113
114    // PUBLIC ACCESSORS
115    // --------------------------------------------------------------------------------------------
116
117    /// Returns the script of the P2IDE (Pay-to-ID extended) note.
118    pub fn script() -> NoteScript {
119        P2IDE_SCRIPT.clone()
120    }
121
122    /// Returns the P2IDE (Pay-to-ID extended) note script root.
123    pub fn script_root() -> NoteScriptRoot {
124        P2IDE_SCRIPT.root()
125    }
126
127    /// Returns the account ID of the note's sender.
128    pub fn sender(&self) -> AccountId {
129        self.sender
130    }
131
132    /// Returns the note's storage.
133    pub fn storage(&self) -> P2ideNoteStorage {
134        self.storage
135    }
136
137    /// Returns the account ID of the note's target (the only account that can consume it).
138    pub fn target(&self) -> AccountId {
139        self.storage.target()
140    }
141
142    /// Returns the account ID of the note's reclaimer.
143    pub fn reclaimer(&self) -> AccountId {
144        self.storage.reclaimer()
145    }
146
147    /// Returns the reclaim block height (if any).
148    pub fn reclaim_height(&self) -> Option<BlockNumber> {
149        self.storage.reclaim_height()
150    }
151
152    /// Returns the timelock block height (if any).
153    pub fn timelock_height(&self) -> Option<BlockNumber> {
154        self.storage.timelock_height()
155    }
156
157    /// Returns the note's serial number.
158    pub fn serial_number(&self) -> Word {
159        self.serial_number
160    }
161
162    /// Returns the note's type.
163    pub fn note_type(&self) -> NoteType {
164        self.note_type
165    }
166
167    /// Returns the assets carried by the note.
168    pub fn assets(&self) -> &NoteAssets {
169        &self.assets
170    }
171
172    /// Returns the attachments carried by the note.
173    pub fn attachments(&self) -> &NoteAttachments {
174        &self.attachments
175    }
176}
177
178// BUILDER EXTENSIONS
179// ================================================================================================
180
181impl<S: p2ide_note_builder::State> P2ideNoteBuilder<S> {
182    /// Adds a single asset to the note. At least one asset is required for `.build()` to succeed.
183    pub fn asset(mut self, asset: impl Into<Asset>) -> Self {
184        self.assets.push(asset.into());
185        self
186    }
187
188    /// Adds multiple assets to the note.
189    pub fn assets(mut self, assets: impl IntoIterator<Item = impl Into<Asset>>) -> Self {
190        self.assets.extend(assets.into_iter().map(Into::into));
191        self
192    }
193
194    /// Adds a single attachment to the note.
195    pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
196        self.attachments.push(attachment.into());
197        self
198    }
199
200    /// Adds multiple attachments to the note.
201    pub fn attachments(
202        mut self,
203        attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
204    ) -> Self {
205        self.attachments.extend(attachments.into_iter().map(Into::into));
206        self
207    }
208}
209
210impl<S: p2ide_note_builder::State> P2ideNoteBuilder<S>
211where
212    S::SerialNumber: p2ide_note_builder::IsUnset,
213{
214    /// Draws a serial number from `rng` and sets it on the builder.
215    pub fn generate_serial_number(
216        self,
217        rng: &mut impl FeltRng,
218    ) -> P2ideNoteBuilder<p2ide_note_builder::SetSerialNumber<S>> {
219        self.serial_number(rng.draw_word())
220    }
221}
222
223// CONVERSIONS
224// ================================================================================================
225
226impl From<P2ideNote> for Note {
227    fn from(note: P2ideNote) -> Self {
228        let recipient = note.storage.into_recipient(note.serial_number);
229        let tag = NoteTag::with_account_target(note.storage.target());
230        let metadata = PartialNoteMetadata::new(note.sender, note.note_type).with_tag(tag);
231
232        Note::with_attachments(note.assets, metadata, recipient, note.attachments)
233    }
234}
235
236// P2IDE NOTE STORAGE
237// ================================================================================================
238
239/// Canonical storage representation for a P2IDE note.
240///
241/// Stores the reclaimer account ID and the target account ID together with optional reclaim
242/// and timelock constraints controlling when the note can be spent or reclaimed.
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244pub struct P2ideNoteStorage {
245    reclaimer: AccountId,
246    target: AccountId,
247    reclaim_height: Option<BlockNumber>,
248    timelock_height: Option<BlockNumber>,
249}
250
251impl P2ideNoteStorage {
252    // CONSTANTS
253    // --------------------------------------------------------------------------------------------
254
255    /// Expected number of storage items of the P2IDE note.
256    pub const NUM_ITEMS: usize = 6;
257
258    // Indices of the storage items. Must match the `*_ITEM` offsets from `STORAGE_PTR` in
259    // `asm/standards/notes/p2ide.masm`.
260    const RECLAIMER_SUFFIX_IDX: usize = 0;
261    const RECLAIMER_PREFIX_IDX: usize = 1;
262    const TARGET_SUFFIX_IDX: usize = 2;
263    const TARGET_PREFIX_IDX: usize = 3;
264    const RECLAIM_HEIGHT_IDX: usize = 4;
265    const TIMELOCK_HEIGHT_IDX: usize = 5;
266
267    /// Creates new P2IDE note storage.
268    pub fn new(
269        reclaimer: AccountId,
270        target: AccountId,
271        reclaim_height: Option<BlockNumber>,
272        timelock_height: Option<BlockNumber>,
273    ) -> Self {
274        Self {
275            reclaimer,
276            target,
277            reclaim_height,
278            timelock_height,
279        }
280    }
281
282    /// Consumes the storage and returns a P2IDE [`NoteRecipient`] with the provided serial number.
283    pub fn into_recipient(self, serial_num: Word) -> NoteRecipient {
284        NoteRecipient::new(serial_num, P2ideNote::script(), self.into())
285    }
286
287    /// Returns the reclaimer account ID.
288    pub fn reclaimer(&self) -> AccountId {
289        self.reclaimer
290    }
291
292    /// Returns the target account ID.
293    pub fn target(&self) -> AccountId {
294        self.target
295    }
296
297    /// Returns the reclaim block height (if any).
298    pub fn reclaim_height(&self) -> Option<BlockNumber> {
299        self.reclaim_height
300    }
301
302    /// Returns the timelock block height (if any).
303    pub fn timelock_height(&self) -> Option<BlockNumber> {
304        self.timelock_height
305    }
306}
307
308impl From<P2ideNoteStorage> for NoteStorage {
309    fn from(storage: P2ideNoteStorage) -> Self {
310        // an absent height is encoded as zero
311        let reclaim = storage.reclaim_height.map_or(Felt::ZERO, Felt::from);
312        let timelock = storage.timelock_height.map_or(Felt::ZERO, Felt::from);
313
314        // the item order must match the `*_IDX` constants that `try_from` decodes with
315        NoteStorage::new(vec![
316            storage.reclaimer.suffix(),
317            storage.reclaimer.prefix().as_felt(),
318            storage.target.suffix(),
319            storage.target.prefix().as_felt(),
320            reclaim,
321            timelock,
322        ])
323        .expect("number of storage items should not exceed max storage items")
324    }
325}
326
327impl TryFrom<&[Felt]> for P2ideNoteStorage {
328    type Error = NoteError;
329
330    fn try_from(note_storage: &[Felt]) -> Result<Self, Self::Error> {
331        if note_storage.len() != P2ideNote::NUM_STORAGE_ITEMS {
332            return Err(NoteError::InvalidNoteStorageLength {
333                expected: P2ideNote::NUM_STORAGE_ITEMS,
334                actual: note_storage.len(),
335            });
336        }
337
338        let reclaimer = AccountId::try_from_elements(
339            note_storage[Self::RECLAIMER_SUFFIX_IDX],
340            note_storage[Self::RECLAIMER_PREFIX_IDX],
341        )
342        .map_err(|err| {
343            NoteError::other_with_source("failed to create reclaimer account id", err)
344        })?;
345
346        let target = AccountId::try_from_elements(
347            note_storage[Self::TARGET_SUFFIX_IDX],
348            note_storage[Self::TARGET_PREFIX_IDX],
349        )
350        .map_err(|err| NoteError::other_with_source("failed to create target account id", err))?;
351
352        let reclaim_height = decode_optional_block_height(
353            note_storage[Self::RECLAIM_HEIGHT_IDX],
354            "invalid reclaim height in note storage",
355        )?;
356        let timelock_height = decode_optional_block_height(
357            note_storage[Self::TIMELOCK_HEIGHT_IDX],
358            "invalid timelock height in note storage",
359        )?;
360
361        Ok(Self {
362            reclaimer,
363            target,
364            reclaim_height,
365            timelock_height,
366        })
367    }
368}
369
370// NOTE CONSUMPTION COST
371// ================================================================================================
372
373impl NoteConsumptionCost for P2ideNote {
374    fn consumption_cycles() -> u32 {
375        P2IDE_CONSUMPTION_CYCLES
376    }
377}
378
379// TESTS
380// ================================================================================================
381
382#[cfg(test)]
383mod tests {
384    use assert_matches::assert_matches;
385    use miden_protocol::account::{AccountId, AccountType};
386    use miden_protocol::asset::FungibleAsset;
387    use miden_protocol::block::BlockNumber;
388    use miden_protocol::crypto::rand::RandomCoin;
389    use miden_protocol::errors::NoteError;
390    use miden_protocol::{Felt, Word};
391
392    use super::*;
393
394    // The suffix and prefix of an ID that `AccountId::try_from_elements` rejects. Both felts are
395    // individually invalid, but the prefix's version check runs first, so that is the error the
396    // pair produces: the version is the prefix's least significant nibble, and `888 & 0xf == 8` is
397    // not a known version.
398    const INVALID_ID_SUFFIX: Felt = Felt::new_unchecked(999);
399    const INVALID_ID_PREFIX: Felt = Felt::new_unchecked(888);
400
401    fn dummy_account() -> AccountId {
402        AccountId::builder()
403            .account_type(AccountType::Private)
404            .build_with_seed([3u8; 32])
405    }
406
407    // STORAGE TESTS
408    // --------------------------------------------------------------------------------------------
409
410    #[test]
411    fn try_from_valid_storage_with_all_fields_succeeds() {
412        let reclaimer = sender();
413        let target = dummy_account();
414
415        let storage = vec![
416            reclaimer.suffix(),
417            reclaimer.prefix().as_felt(),
418            target.suffix(),
419            target.prefix().as_felt(),
420            Felt::from(42u32),
421            Felt::from(100u32),
422        ];
423
424        let decoded = P2ideNoteStorage::try_from(storage.as_slice())
425            .expect("valid P2IDE storage should decode");
426
427        assert_eq!(decoded.reclaimer(), reclaimer);
428        assert_eq!(decoded.target(), target);
429        assert_eq!(decoded.reclaim_height(), Some(BlockNumber::from(42u32)));
430        assert_eq!(decoded.timelock_height(), Some(BlockNumber::from(100u32)));
431    }
432
433    #[test]
434    fn try_from_zero_heights_map_to_none() {
435        let reclaimer = sender();
436        let target = dummy_account();
437
438        let storage = vec![
439            reclaimer.suffix(),
440            reclaimer.prefix().as_felt(),
441            target.suffix(),
442            target.prefix().as_felt(),
443            Felt::ZERO,
444            Felt::ZERO,
445        ];
446
447        let decoded = P2ideNoteStorage::try_from(storage.as_slice()).unwrap();
448
449        assert_eq!(decoded.reclaim_height(), None);
450        assert_eq!(decoded.timelock_height(), None);
451    }
452
453    #[test]
454    fn try_from_invalid_length_fails() {
455        let storage = vec![Felt::ZERO; 3];
456
457        let err =
458            P2ideNoteStorage::try_from(storage.as_slice()).expect_err("wrong length must fail");
459
460        assert!(matches!(
461            err,
462            NoteError::InvalidNoteStorageLength {
463                expected: P2ideNote::NUM_STORAGE_ITEMS,
464                actual: 3
465            }
466        ));
467    }
468
469    /// The reclaimer and the target are decoded from different storage items, so each must
470    /// be validated on its own.
471    #[test]
472    fn try_from_invalid_reclaimer_fails() {
473        let target = dummy_account();
474
475        let storage = vec![
476            INVALID_ID_SUFFIX,
477            INVALID_ID_PREFIX,
478            target.suffix(),
479            target.prefix().as_felt(),
480            Felt::ZERO,
481            Felt::ZERO,
482        ];
483
484        let err = P2ideNoteStorage::try_from(storage.as_slice())
485            .expect_err("invalid reclaimer encoding must fail");
486
487        assert_matches!(err, NoteError::Other { error_msg, source: Some(_), .. } => {
488            assert!(error_msg.contains("reclaimer"));
489        });
490    }
491
492    #[test]
493    fn try_from_invalid_target_fails() {
494        let reclaimer = sender();
495
496        let storage = vec![
497            reclaimer.suffix(),
498            reclaimer.prefix().as_felt(),
499            INVALID_ID_SUFFIX,
500            INVALID_ID_PREFIX,
501            Felt::ZERO,
502            Felt::ZERO,
503        ];
504
505        let err = P2ideNoteStorage::try_from(storage.as_slice())
506            .expect_err("invalid target encoding must fail");
507
508        assert_matches!(err, NoteError::Other { error_msg, source: Some(_), .. } => {
509            assert!(error_msg.contains("target"));
510        });
511    }
512
513    /// The encoder and the decoder must agree on the item order. This does not pin the order to
514    /// `p2ide.masm` - a transposition applied to both halves round-trips fine. That contract is
515    /// held by the hand-built storage vectors in the `try_from_*` tests above, which spell the
516    /// layout out literally, and by the note script execution tests in `miden-testing`.
517    ///
518    /// Zero means "disabled" rather than a height, so it is excluded here, see
519    /// [`zero_reclaim_height_means_reclaim_disabled`].
520    #[test]
521    fn storage_round_trips_through_note_storage() {
522        let storage = P2ideNoteStorage::new(
523            sender(),
524            target(),
525            Some(BlockNumber::from(42u32)),
526            Some(BlockNumber::from(100u32)),
527        );
528
529        let encoded: NoteStorage = storage.into();
530        let decoded = P2ideNoteStorage::try_from(encoded.items()).unwrap();
531
532        assert_eq!(decoded, storage);
533    }
534
535    /// A zero reclaim height means "reclaim disabled", both in the storage encoding and in the note
536    /// script, which rejects it with `ERR_P2IDE_RECLAIM_DISABLED`. Zero is thus not a height, and
537    /// `Some(BlockNumber::GENESIS)` encodes identically to `None`.
538    #[test]
539    fn zero_reclaim_height_means_reclaim_disabled() {
540        let storage = P2ideNoteStorage::new(sender(), target(), Some(BlockNumber::GENESIS), None);
541
542        let encoded: NoteStorage = storage.into();
543        let decoded = P2ideNoteStorage::try_from(encoded.items()).unwrap();
544
545        assert_eq!(decoded.reclaim_height(), None);
546    }
547
548    #[test]
549    fn try_from_reclaim_height_overflow_fails() {
550        let reclaimer = sender();
551        let target = dummy_account();
552
553        // > u32::MAX
554        let overflow = Felt::new_unchecked(u64::from(u32::MAX) + 1);
555
556        let storage = vec![
557            reclaimer.suffix(),
558            reclaimer.prefix().as_felt(),
559            target.suffix(),
560            target.prefix().as_felt(),
561            overflow,
562            Felt::ZERO,
563        ];
564
565        let err = P2ideNoteStorage::try_from(storage.as_slice())
566            .expect_err("overflow reclaim height must fail");
567
568        assert_matches!(err, NoteError::Other { error_msg, source: Some(_), .. } => {
569            assert!(error_msg.contains("reclaim height"));
570        });
571    }
572
573    #[test]
574    fn try_from_timelock_height_overflow_fails() {
575        let reclaimer = sender();
576        let target = dummy_account();
577
578        let overflow = Felt::new_unchecked(u64::from(u32::MAX) + 10);
579
580        let storage = vec![
581            reclaimer.suffix(),
582            reclaimer.prefix().as_felt(),
583            target.suffix(),
584            target.prefix().as_felt(),
585            Felt::ZERO,
586            overflow,
587        ];
588
589        let err = P2ideNoteStorage::try_from(storage.as_slice())
590            .expect_err("overflow timelock height must fail");
591
592        assert_matches!(err, NoteError::Other { error_msg, source: Some(_), .. } => {
593            assert!(error_msg.contains("timelock height"));
594        });
595    }
596
597    // BUILDER TESTS
598    // --------------------------------------------------------------------------------------------
599
600    fn sender() -> AccountId {
601        AccountId::builder()
602            .account_type(AccountType::Private)
603            .build_with_seed([1u8; 32])
604    }
605
606    fn target() -> AccountId {
607        AccountId::builder()
608            .account_type(AccountType::Private)
609            .build_with_seed([2u8; 32])
610    }
611
612    fn faucet_a() -> AccountId {
613        AccountId::builder()
614            .account_type(AccountType::Public)
615            .build_with_seed([3u8; 32])
616    }
617
618    fn faucet_b() -> AccountId {
619        AccountId::builder()
620            .account_type(AccountType::Public)
621            .build_with_seed([4u8; 32])
622    }
623
624    /// The minimal builder uses defaults for everything but the required fields (no reclaim or
625    /// timelock height, private note type).
626    #[test]
627    fn builder_minimal_uses_defaults() {
628        let note = P2ideNote::builder()
629            .sender(sender())
630            .target(target())
631            .serial_number(Word::empty())
632            .asset(FungibleAsset::new(faucet_a(), 1).unwrap())
633            .build()
634            .unwrap();
635
636        assert_eq!(note.sender(), sender());
637        assert_eq!(note.target(), target());
638        // the reclaimer defaults to the sender when not set explicitly
639        assert_eq!(note.reclaimer(), sender());
640        assert_eq!(note.note_type(), NoteType::default());
641        assert_eq!(note.reclaim_height(), None);
642        assert_eq!(note.timelock_height(), None);
643        assert_eq!(note.assets().num_assets(), 1);
644        assert_eq!(note.attachments().num_attachments(), 0);
645    }
646
647    /// `.asset()` and `.assets()` both append, so they can be combined and called repeatedly.
648    #[test]
649    fn builder_accumulates_assets() {
650        let mut rng = RandomCoin::new(Word::empty());
651        let note = P2ideNote::builder()
652            .sender(sender())
653            .target(target())
654            .asset(FungibleAsset::new(faucet_a(), 100).unwrap())
655            .assets([Asset::from(FungibleAsset::new(faucet_b(), 200).unwrap())])
656            .generate_serial_number(&mut rng)
657            .build()
658            .unwrap();
659
660        assert_eq!(note.assets().num_assets(), 2);
661        assert_ne!(note.serial_number(), Word::empty());
662    }
663
664    /// A P2IDE note must carry at least one asset.
665    #[test]
666    fn builder_rejects_empty_assets() {
667        let err = P2ideNote::builder()
668            .sender(sender())
669            .target(target())
670            .serial_number(Word::empty())
671            .build()
672            .expect_err("a note without assets must be rejected");
673
674        assert_matches!(err, NoteError::Other { error_msg, .. } => {
675            assert!(error_msg.contains("note must contain at least one asset"))
676        });
677    }
678
679    /// The reclaim and timelock heights are optional and surfaced through the getters.
680    #[test]
681    fn builder_sets_reclaim_and_timelock() {
682        let note = P2ideNote::builder()
683            .sender(sender())
684            .target(target())
685            .serial_number(Word::empty())
686            .asset(FungibleAsset::new(faucet_a(), 1).unwrap())
687            .reclaim_height(BlockNumber::from(42u32))
688            .timelock_height(BlockNumber::from(100u32))
689            .build()
690            .unwrap();
691
692        assert_eq!(note.reclaim_height(), Some(BlockNumber::from(42u32)));
693        assert_eq!(note.timelock_height(), Some(BlockNumber::from(100u32)));
694    }
695
696    /// An explicit reclaimer (distinct from the sender) is stored and surfaced via
697    /// `reclaimer()`, and round-trips through the note storage.
698    #[test]
699    fn builder_explicit_reclaimer_differs_from_sender() {
700        let note = P2ideNote::builder()
701            .sender(sender())
702            .target(target())
703            .reclaimer(dummy_account())
704            .serial_number(Word::empty())
705            .asset(FungibleAsset::new(faucet_a(), 1).unwrap())
706            .build()
707            .unwrap();
708
709        assert_eq!(note.sender(), sender());
710        assert_eq!(note.reclaimer(), dummy_account());
711        assert_ne!(note.reclaimer(), note.sender());
712
713        // the explicit reclaimer round-trips through the encoded note storage
714        let storage: NoteStorage = note.storage().into();
715        let decoded = P2ideNoteStorage::try_from(storage.items()).unwrap();
716        assert_eq!(decoded.reclaimer(), dummy_account());
717        assert_eq!(decoded.target(), target());
718    }
719}