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 = 16;
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 note_script = Self::script();
93
94        let payback_serial_num = rng.draw_word();
95        let payback_recipient = P2idNoteStorage::new(sender).into_recipient(payback_serial_num);
96
97        let requested_asset_word: Word = requested_asset.into();
98        let payback_tag = NoteTag::with_account_target(sender);
99
100        let attachment_scheme = Felt::from(payback_note_attachment.attachment_scheme().as_u32());
101        let attachment_kind = Felt::from(payback_note_attachment.attachment_kind().as_u8());
102        let attachment = payback_note_attachment.content().to_word();
103
104        let mut inputs = Vec::with_capacity(16);
105        inputs.extend_from_slice(&[
106            payback_note_type.into(),
107            payback_tag.into(),
108            attachment_scheme,
109            attachment_kind,
110        ]);
111        inputs.extend_from_slice(attachment.as_elements());
112        inputs.extend_from_slice(requested_asset_word.as_elements());
113        inputs.extend_from_slice(payback_recipient.digest().as_elements());
114        let inputs = NoteStorage::new(inputs)?;
115
116        // build the tag for the SWAP use case
117        let tag = Self::build_tag(swap_note_type, &offered_asset, &requested_asset);
118        let serial_num = rng.draw_word();
119
120        // build the outgoing note
121        let metadata = NoteMetadata::new(sender, swap_note_type)
122            .with_tag(tag)
123            .with_attachment(swap_note_attachment);
124        let assets = NoteAssets::new(vec![offered_asset])?;
125        let recipient = NoteRecipient::new(serial_num, note_script, inputs);
126        let note = Note::new(assets, metadata, recipient);
127
128        // build the payback note details
129        let payback_assets = NoteAssets::new(vec![requested_asset])?;
130        let payback_note = NoteDetails::new(payback_assets, payback_recipient);
131
132        Ok((note, payback_note))
133    }
134
135    /// Returns a note tag for a swap note with the specified parameters.
136    ///
137    /// The tag is laid out as follows:
138    ///
139    /// ```text
140    /// [
141    ///   note_type (2 bits) | script_root (14 bits)
142    ///   | offered_asset_faucet_id (8 bits) | requested_asset_faucet_id (8 bits)
143    /// ]
144    /// ```
145    ///
146    /// The script root serves as the use case identifier of the SWAP tag.
147    pub fn build_tag(
148        note_type: NoteType,
149        offered_asset: &Asset,
150        requested_asset: &Asset,
151    ) -> NoteTag {
152        let swap_root_bytes = Self::script().root().as_bytes();
153        // Construct the swap use case ID from the 14 most significant bits of the script root. This
154        // leaves the two most significant bits zero.
155        let mut swap_use_case_id = (swap_root_bytes[0] as u16) << 6;
156        swap_use_case_id |= (swap_root_bytes[1] >> 2) as u16;
157
158        // Get bits 0..8 from the faucet IDs of both assets which will form the tag payload.
159        let offered_asset_id: u64 = offered_asset.faucet_id_prefix().into();
160        let offered_asset_tag = (offered_asset_id >> 56) as u8;
161
162        let requested_asset_id: u64 = requested_asset.faucet_id_prefix().into();
163        let requested_asset_tag = (requested_asset_id >> 56) as u8;
164
165        let asset_pair = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16);
166
167        let tag = ((note_type as u8 as u32) << 30)
168            | ((swap_use_case_id as u32) << 16)
169            | asset_pair as u32;
170
171        NoteTag::new(tag)
172    }
173}
174
175// TESTS
176// ================================================================================================
177
178#[cfg(test)]
179mod tests {
180    use miden_protocol::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType};
181    use miden_protocol::asset::{FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
182    use miden_protocol::{self};
183
184    use super::*;
185
186    #[test]
187    fn swap_tag() {
188        // Construct an ID that starts with 0xcdb1.
189        let mut fungible_faucet_id_bytes = [0; 15];
190        fungible_faucet_id_bytes[0] = 0xcd;
191        fungible_faucet_id_bytes[1] = 0xb1;
192
193        // Construct an ID that starts with 0xabec.
194        let mut non_fungible_faucet_id_bytes = [0; 15];
195        non_fungible_faucet_id_bytes[0] = 0xab;
196        non_fungible_faucet_id_bytes[1] = 0xec;
197
198        let offered_asset = Asset::Fungible(
199            FungibleAsset::new(
200                AccountId::dummy(
201                    fungible_faucet_id_bytes,
202                    AccountIdVersion::Version0,
203                    AccountType::FungibleFaucet,
204                    AccountStorageMode::Public,
205                ),
206                2500,
207            )
208            .unwrap(),
209        );
210
211        let requested_asset = Asset::NonFungible(
212            NonFungibleAsset::new(
213                &NonFungibleAssetDetails::new(
214                    AccountId::dummy(
215                        non_fungible_faucet_id_bytes,
216                        AccountIdVersion::Version0,
217                        AccountType::NonFungibleFaucet,
218                        AccountStorageMode::Public,
219                    )
220                    .prefix(),
221                    vec![0xaa, 0xbb, 0xcc, 0xdd],
222                )
223                .unwrap(),
224            )
225            .unwrap(),
226        );
227
228        // The fungible ID starts with 0xcdb1.
229        // The non fungible ID starts with 0xabec.
230        // The expected tag payload is thus 0xcdab.
231        let expected_asset_pair = 0xcdab;
232
233        let note_type = NoteType::Public;
234        let actual_tag = SwapNote::build_tag(note_type, &offered_asset, &requested_asset);
235
236        assert_eq!(actual_tag.as_u32() as u16, expected_asset_pair, "asset pair should match");
237        assert_eq!((actual_tag.as_u32() >> 30) as u8, note_type as u8, "note type should match");
238        // Check the 8 bits of the first script root byte.
239        assert_eq!(
240            (actual_tag.as_u32() >> 22) as u8,
241            SwapNote::script_root().as_bytes()[0],
242            "swap script root byte 0 should match"
243        );
244        // Extract the 6 bits of the second script root byte and shift for comparison.
245        assert_eq!(
246            ((actual_tag.as_u32() & 0b00000000_00111111_00000000_00000000) >> 16) as u8,
247            SwapNote::script_root().as_bytes()[1] >> 2,
248            "swap script root byte 1 should match with the lower two bits set to zero"
249        );
250    }
251}