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
27const SWAP_SCRIPT_PATH: &str = "::miden::standards::notes::swap::main";
32
33static 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
41pub struct SwapNote;
46
47impl SwapNote {
48 pub const NUM_STORAGE_ITEMS: usize = SwapNoteStorage::NUM_ITEMS;
53
54 pub fn script() -> NoteScript {
59 SWAP_SCRIPT.clone()
60 }
61
62 pub fn script_root() -> NoteScriptRoot {
64 SWAP_SCRIPT.root()
65 }
66
67 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 let tag = Self::build_tag(swap_note_type, &offered_asset, &requested_asset);
102
103 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 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 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 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 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#[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 pub const NUM_ITEMS: usize = 14;
178
179 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 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 pub fn payback_note_type(&self) -> NoteType {
217 self.payback_note_type
218 }
219
220 pub fn payback_tag(&self) -> NoteTag {
222 self.payback_tag
223 }
224
225 pub fn requested_asset(&self) -> Asset {
227 self.requested_asset
228 }
229
230 pub fn payback_recipient_digest(&self) -> Word {
232 self.payback_recipient_digest
233 }
234
235 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#[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 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 let mut fungible_faucet_id_bytes = [0; 15];
339 fungible_faucet_id_bytes[0] = 0xcd;
340 fungible_faucet_id_bytes[1] = 0xb1;
341
342 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 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 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 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}