Skip to main content

miden_standards/note/
swap.rs

1use alloc::vec::Vec;
2
3use miden_protocol::account::AccountId;
4use miden_protocol::assembly::Path;
5use miden_protocol::asset::Asset;
6use miden_protocol::crypto::rand::FeltRng;
7use miden_protocol::errors::NoteError;
8use miden_protocol::note::{
9    Note,
10    NoteAssets,
11    NoteAttachment,
12    NoteDetails,
13    NoteMetadata,
14    NoteRecipient,
15    NoteScript,
16    NoteStorage,
17    NoteTag,
18    NoteType,
19};
20use miden_protocol::utils::sync::LazyLock;
21use miden_protocol::{Felt, Word};
22
23use crate::StandardsLib;
24use crate::note::P2idNoteStorage;
25
26// NOTE SCRIPT
27// ================================================================================================
28
29/// Path to the SWAP note script procedure in the standards library.
30const SWAP_SCRIPT_PATH: &str = "::miden::standards::notes::swap::main";
31
32// Initialize the SWAP note script only once
33static SWAP_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
34    let standards_lib = StandardsLib::default();
35    let path = Path::new(SWAP_SCRIPT_PATH);
36    NoteScript::from_library_reference(standards_lib.as_ref(), path)
37        .expect("Standards library contains SWAP note script procedure")
38});
39
40// SWAP NOTE
41// ================================================================================================
42
43/// TODO: add docs
44pub struct SwapNote;
45
46impl SwapNote {
47    // CONSTANTS
48    // --------------------------------------------------------------------------------------------
49
50    /// Expected number of storage items of the SWAP note.
51    pub const NUM_STORAGE_ITEMS: usize = SwapNoteStorage::NUM_ITEMS;
52
53    // PUBLIC ACCESSORS
54    // --------------------------------------------------------------------------------------------
55
56    /// Returns the script of the SWAP note.
57    pub fn script() -> NoteScript {
58        SWAP_SCRIPT.clone()
59    }
60
61    /// Returns the SWAP note script root.
62    pub fn script_root() -> Word {
63        SWAP_SCRIPT.root()
64    }
65
66    // BUILDERS
67    // --------------------------------------------------------------------------------------------
68
69    /// Generates a SWAP note - swap of assets between two accounts - and returns the note as well
70    /// as [`NoteDetails`] for the payback note.
71    ///
72    /// This script enables a swap of 2 assets between the `sender` account and any other account
73    /// that is willing to consume the note. The consumer will receive the `offered_asset` and
74    /// will create a new P2ID note with `sender` as target, containing the `requested_asset`.
75    ///
76    /// # Errors
77    /// Returns an error if deserialization or compilation of the `SWAP` script fails.
78    pub fn create<R: FeltRng>(
79        sender: AccountId,
80        offered_asset: Asset,
81        requested_asset: Asset,
82        swap_note_type: NoteType,
83        swap_note_attachment: NoteAttachment,
84        payback_note_type: NoteType,
85        payback_note_attachment: NoteAttachment,
86        rng: &mut R,
87    ) -> Result<(Note, NoteDetails), NoteError> {
88        if requested_asset == offered_asset {
89            return Err(NoteError::other("requested asset same as offered asset"));
90        }
91
92        let payback_serial_num = rng.draw_word();
93
94        let swap_storage = SwapNoteStorage::new(
95            sender,
96            requested_asset,
97            payback_note_type,
98            payback_note_attachment,
99            payback_serial_num,
100        );
101
102        let serial_num = rng.draw_word();
103        let recipient = swap_storage.into_recipient(serial_num);
104
105        // build the tag for the SWAP use case
106        let tag = Self::build_tag(swap_note_type, &offered_asset, &requested_asset);
107
108        // build the outgoing note
109        let metadata = NoteMetadata::new(sender, swap_note_type)
110            .with_tag(tag)
111            .with_attachment(swap_note_attachment);
112        let assets = NoteAssets::new(vec![offered_asset])?;
113        let note = Note::new(assets, metadata, recipient);
114
115        // build the payback note details
116        let payback_recipient = P2idNoteStorage::new(sender).into_recipient(payback_serial_num);
117        let payback_assets = NoteAssets::new(vec![requested_asset])?;
118        let payback_note = NoteDetails::new(payback_assets, payback_recipient);
119
120        Ok((note, payback_note))
121    }
122
123    /// Returns a note tag for a swap note with the specified parameters.
124    ///
125    /// The tag is laid out as follows:
126    ///
127    /// ```text
128    /// [
129    ///   note_type (2 bits) | script_root (14 bits)
130    ///   | offered_asset_faucet_id (8 bits) | requested_asset_faucet_id (8 bits)
131    /// ]
132    /// ```
133    ///
134    /// The script root serves as the use case identifier of the SWAP tag.
135    pub fn build_tag(
136        note_type: NoteType,
137        offered_asset: &Asset,
138        requested_asset: &Asset,
139    ) -> NoteTag {
140        let swap_root_bytes = Self::script().root().as_bytes();
141        // Construct the swap use case ID from the 14 most significant bits of the script root. This
142        // leaves the two most significant bits zero.
143        let mut swap_use_case_id = (swap_root_bytes[0] as u16) << 6;
144        swap_use_case_id |= (swap_root_bytes[1] >> 2) as u16;
145
146        // Get bits 0..8 from the faucet IDs of both assets which will form the tag payload.
147        let offered_asset_id: u64 = offered_asset.faucet_id().prefix().into();
148        let offered_asset_tag = (offered_asset_id >> 56) as u8;
149
150        let requested_asset_id: u64 = requested_asset.faucet_id().prefix().into();
151        let requested_asset_tag = (requested_asset_id >> 56) as u8;
152
153        let asset_pair = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16);
154
155        let tag = ((note_type as u8 as u32) << 30)
156            | ((swap_use_case_id as u32) << 16)
157            | asset_pair as u32;
158
159        NoteTag::new(tag)
160    }
161}
162
163// SWAP NOTE STORAGE
164// ================================================================================================
165
166/// Canonical storage representation for a SWAP note.
167///
168/// Contains the payback note configuration and the requested asset that the
169/// swap creator wants to receive in exchange for the offered asset contained
170/// in the note's vault.
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct SwapNoteStorage {
173    payback_note_type: NoteType,
174    payback_tag: NoteTag,
175    payback_attachment: NoteAttachment,
176    requested_asset: Asset,
177    payback_recipient_digest: Word,
178}
179
180impl SwapNoteStorage {
181    // CONSTANTS
182    // --------------------------------------------------------------------------------------------
183
184    /// Expected number of storage items of the SWAP note.
185    pub const NUM_ITEMS: usize = 20;
186
187    // CONSTRUCTORS
188    // --------------------------------------------------------------------------------------------
189
190    /// Creates new SWAP note storage with the specified parameters.
191    pub fn new(
192        sender: AccountId,
193        requested_asset: Asset,
194        payback_note_type: NoteType,
195        payback_attachment: NoteAttachment,
196        payback_serial_number: Word,
197    ) -> Self {
198        let payback_recipient = P2idNoteStorage::new(sender).into_recipient(payback_serial_number);
199        let payback_tag = NoteTag::with_account_target(sender);
200
201        Self::from_parts(
202            payback_note_type,
203            payback_tag,
204            payback_attachment,
205            requested_asset,
206            payback_recipient.digest(),
207        )
208    }
209
210    /// Creates a [`SwapNoteStorage`] from raw parts.
211    pub fn from_parts(
212        payback_note_type: NoteType,
213        payback_tag: NoteTag,
214        payback_attachment: NoteAttachment,
215        requested_asset: Asset,
216        payback_recipient_digest: Word,
217    ) -> Self {
218        Self {
219            payback_note_type,
220            payback_tag,
221            payback_attachment,
222            requested_asset,
223            payback_recipient_digest,
224        }
225    }
226
227    /// Returns the payback note type.
228    pub fn payback_note_type(&self) -> NoteType {
229        self.payback_note_type
230    }
231
232    /// Returns the payback note tag.
233    pub fn payback_tag(&self) -> NoteTag {
234        self.payback_tag
235    }
236
237    /// Returns the payback note attachment.
238    pub fn payback_attachment(&self) -> &NoteAttachment {
239        &self.payback_attachment
240    }
241
242    /// Returns the requested asset.
243    pub fn requested_asset(&self) -> Asset {
244        self.requested_asset
245    }
246
247    /// Returns the payback recipient digest.
248    pub fn payback_recipient_digest(&self) -> Word {
249        self.payback_recipient_digest
250    }
251
252    /// Consumes the storage and returns a SWAP [`NoteRecipient`] with the provided serial number.
253    ///
254    /// Notes created with this recipient will be SWAP notes whose storage encodes the payback
255    /// configuration and the requested asset stored in this [`SwapNoteStorage`].
256    pub fn into_recipient(self, serial_num: Word) -> NoteRecipient {
257        NoteRecipient::new(serial_num, SwapNote::script(), NoteStorage::from(self))
258    }
259}
260
261impl From<SwapNoteStorage> for NoteStorage {
262    fn from(storage: SwapNoteStorage) -> Self {
263        let attachment_scheme = Felt::from(storage.payback_attachment.attachment_scheme().as_u32());
264        let attachment_kind = Felt::from(storage.payback_attachment.attachment_kind().as_u8());
265        let attachment = storage.payback_attachment.content().to_word();
266
267        let mut storage_values = Vec::with_capacity(SwapNoteStorage::NUM_ITEMS);
268        storage_values.extend_from_slice(&[
269            storage.payback_note_type.into(),
270            storage.payback_tag.into(),
271            attachment_scheme,
272            attachment_kind,
273        ]);
274        storage_values.extend_from_slice(attachment.as_elements());
275        storage_values.extend_from_slice(&storage.requested_asset.as_elements());
276        storage_values.extend_from_slice(storage.payback_recipient_digest.as_elements());
277
278        NoteStorage::new(storage_values)
279            .expect("number of storage items should not exceed max storage items")
280    }
281}
282
283// NOTE: TryFrom<&[Felt]> for SwapNoteStorage is not implemented because
284// array attachment content cannot be reconstructed from storage alone. See https://github.com/0xMiden/protocol/issues/2555
285
286// TESTS
287// ================================================================================================
288
289#[cfg(test)]
290mod tests {
291    use miden_protocol::Felt;
292    use miden_protocol::account::{AccountIdVersion, AccountStorageMode, AccountType};
293    use miden_protocol::asset::{FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
294    use miden_protocol::note::{NoteAttachment, NoteStorage, NoteTag, NoteType};
295    use miden_protocol::testing::account_id::{
296        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
297        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
298    };
299
300    use super::*;
301
302    fn fungible_faucet() -> AccountId {
303        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap()
304    }
305
306    fn non_fungible_faucet() -> AccountId {
307        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into().unwrap()
308    }
309
310    fn fungible_asset() -> Asset {
311        Asset::Fungible(FungibleAsset::new(fungible_faucet(), 1000).unwrap())
312    }
313
314    fn non_fungible_asset() -> Asset {
315        let details =
316            NonFungibleAssetDetails::new(non_fungible_faucet(), vec![0xaa, 0xbb]).unwrap();
317        Asset::NonFungible(NonFungibleAsset::new(&details).unwrap())
318    }
319
320    #[test]
321    fn swap_note_storage() {
322        let payback_note_type = NoteType::Private;
323        let payback_tag = NoteTag::new(0x12345678);
324        let payback_attachment = NoteAttachment::default();
325        let requested_asset = fungible_asset();
326        let payback_recipient_digest =
327            Word::new([Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]);
328
329        let storage = SwapNoteStorage::from_parts(
330            payback_note_type,
331            payback_tag,
332            payback_attachment.clone(),
333            requested_asset,
334            payback_recipient_digest,
335        );
336
337        assert_eq!(storage.payback_note_type(), payback_note_type);
338        assert_eq!(storage.payback_tag(), payback_tag);
339        assert_eq!(storage.payback_attachment(), &payback_attachment);
340        assert_eq!(storage.requested_asset(), requested_asset);
341        assert_eq!(storage.payback_recipient_digest(), payback_recipient_digest);
342
343        // Convert to NoteStorage
344        let note_storage = NoteStorage::from(storage);
345        assert_eq!(note_storage.num_items() as usize, SwapNoteStorage::NUM_ITEMS);
346    }
347
348    #[test]
349    fn swap_note_storage_with_non_fungible_asset() {
350        let payback_note_type = NoteType::Public;
351        let payback_tag = NoteTag::new(0xaabbccdd);
352        let payback_attachment = NoteAttachment::default();
353        let requested_asset = non_fungible_asset();
354        let payback_recipient_digest =
355            Word::new([Felt::new(10), Felt::new(20), Felt::new(30), Felt::new(40)]);
356
357        let storage = SwapNoteStorage::from_parts(
358            payback_note_type,
359            payback_tag,
360            payback_attachment,
361            requested_asset,
362            payback_recipient_digest,
363        );
364
365        assert_eq!(storage.payback_note_type(), payback_note_type);
366        assert_eq!(storage.requested_asset(), requested_asset);
367
368        let note_storage = NoteStorage::from(storage);
369        assert_eq!(note_storage.num_items() as usize, SwapNoteStorage::NUM_ITEMS);
370    }
371
372    #[test]
373    fn swap_tag() {
374        // Construct an ID that starts with 0xcdb1.
375        let mut fungible_faucet_id_bytes = [0; 15];
376        fungible_faucet_id_bytes[0] = 0xcd;
377        fungible_faucet_id_bytes[1] = 0xb1;
378
379        // Construct an ID that starts with 0xabec.
380        let mut non_fungible_faucet_id_bytes = [0; 15];
381        non_fungible_faucet_id_bytes[0] = 0xab;
382        non_fungible_faucet_id_bytes[1] = 0xec;
383
384        let offered_asset = Asset::Fungible(
385            FungibleAsset::new(
386                AccountId::dummy(
387                    fungible_faucet_id_bytes,
388                    AccountIdVersion::Version0,
389                    AccountType::FungibleFaucet,
390                    AccountStorageMode::Public,
391                ),
392                2500,
393            )
394            .unwrap(),
395        );
396
397        let requested_asset = Asset::NonFungible(
398            NonFungibleAsset::new(
399                &NonFungibleAssetDetails::new(
400                    AccountId::dummy(
401                        non_fungible_faucet_id_bytes,
402                        AccountIdVersion::Version0,
403                        AccountType::NonFungibleFaucet,
404                        AccountStorageMode::Public,
405                    ),
406                    vec![0xaa, 0xbb, 0xcc, 0xdd],
407                )
408                .unwrap(),
409            )
410            .unwrap(),
411        );
412
413        // The fungible ID starts with 0xcdb1.
414        // The non fungible ID starts with 0xabec.
415        // The expected tag payload is thus 0xcdab.
416        let expected_asset_pair = 0xcdab;
417
418        let note_type = NoteType::Public;
419        let actual_tag = SwapNote::build_tag(note_type, &offered_asset, &requested_asset);
420
421        assert_eq!(actual_tag.as_u32() as u16, expected_asset_pair, "asset pair should match");
422        assert_eq!((actual_tag.as_u32() >> 30) as u8, note_type as u8, "note type should match");
423        // Check the 8 bits of the first script root byte.
424        assert_eq!(
425            (actual_tag.as_u32() >> 22) as u8,
426            SwapNote::script_root().as_bytes()[0],
427            "swap script root byte 0 should match"
428        );
429        // Extract the 6 bits of the second script root byte and shift for comparison.
430        assert_eq!(
431            ((actual_tag.as_u32() & 0b00000000_00111111_00000000_00000000) >> 16) as u8,
432            SwapNote::script_root().as_bytes()[1] >> 2,
433            "swap script root byte 1 should match with the lower two bits set to zero"
434        );
435    }
436}