Skip to main content

miden_standards/note/
swap.rs

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