use alloc::vec::Vec;
use miden_protocol::account::AccountId;
use miden_protocol::assembly::Path;
use miden_protocol::asset::Asset;
use miden_protocol::crypto::rand::FeltRng;
use miden_protocol::errors::NoteError;
use miden_protocol::note::{
Note,
NoteAssets,
NoteAttachments,
NoteDetails,
NoteRecipient,
NoteScript,
NoteScriptRoot,
NoteStorage,
NoteTag,
NoteType,
PartialNoteMetadata,
};
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, ONE, Word};
use crate::StandardsLib;
use crate::note::P2idNoteStorage;
const SWAP_SCRIPT_PATH: &str = "::miden::standards::notes::swap::main";
static SWAP_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let standards_lib = StandardsLib::default();
let path = Path::new(SWAP_SCRIPT_PATH);
NoteScript::from_library_reference(standards_lib.as_ref(), path)
.expect("Standards library contains SWAP note script procedure")
});
pub struct SwapNote;
impl SwapNote {
pub const NUM_STORAGE_ITEMS: usize = SwapNoteStorage::NUM_ITEMS;
pub fn script() -> NoteScript {
SWAP_SCRIPT.clone()
}
pub fn script_root() -> NoteScriptRoot {
SWAP_SCRIPT.root()
}
pub fn create<R: FeltRng>(
sender: AccountId,
offered_asset: Asset,
requested_asset: Asset,
swap_note_type: NoteType,
swap_note_attachments: NoteAttachments,
payback_note_type: NoteType,
rng: &mut R,
) -> Result<(Note, NoteDetails), NoteError> {
if requested_asset == offered_asset {
return Err(NoteError::other("requested asset same as offered asset"));
}
let serial_num = rng.draw_word();
let payback_serial_num = payback_serial_from_swap(serial_num);
let payback_recipient = P2idNoteStorage::new(sender).into_recipient(payback_serial_num);
let payback_assets = NoteAssets::new(vec![requested_asset])?;
let payback_note = NoteDetails::new(payback_assets, payback_recipient.clone());
let payback_tag = NoteTag::with_account_target(sender);
let swap_storage = match payback_note_type {
NoteType::Private => SwapNoteStorage::new_private(
requested_asset,
payback_recipient.digest(),
payback_tag,
),
NoteType::Public => SwapNoteStorage::new_public(requested_asset, sender, payback_tag),
};
let recipient = swap_storage.into_recipient(serial_num);
let tag = Self::build_tag(swap_note_type, &offered_asset, &requested_asset);
let metadata = PartialNoteMetadata::new(sender, swap_note_type).with_tag(tag);
let assets = NoteAssets::new(vec![offered_asset])?;
let note = Note::with_attachments(assets, metadata, recipient, swap_note_attachments);
Ok((note, payback_note))
}
pub fn build_tag(
note_type: NoteType,
offered_asset: &Asset,
requested_asset: &Asset,
) -> NoteTag {
let swap_root_bytes = Self::script().root().as_bytes();
let mut swap_use_case_id = (swap_root_bytes[0] as u16) << 7;
swap_use_case_id |= (swap_root_bytes[1] >> 1) as u16;
let offered_asset_id: u64 = offered_asset.faucet_id().prefix().into();
let offered_asset_tag = (offered_asset_id >> 56) as u8;
let requested_asset_id: u64 = requested_asset.faucet_id().prefix().into();
let requested_asset_tag = (requested_asset_id >> 56) as u8;
let asset_pair = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16);
let tag = ((note_type as u8 as u32) << 31)
| ((swap_use_case_id as u32) << 16)
| asset_pair as u32;
NoteTag::new(tag)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SwapNoteStorage {
requested_asset: Asset,
payback_tag: NoteTag,
payback: SwapPayback,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SwapPayback {
Private {
recipient: Word,
},
Public {
payback_target_id: AccountId,
},
}
impl SwapNoteStorage {
pub const NUM_ITEMS: usize = 16;
pub fn new_private(
requested_asset: Asset,
payback_recipient: Word,
payback_tag: NoteTag,
) -> Self {
Self {
requested_asset,
payback_tag,
payback: SwapPayback::Private { recipient: payback_recipient },
}
}
pub fn new_public(
requested_asset: Asset,
payback_target_id: AccountId,
payback_tag: NoteTag,
) -> Self {
Self {
requested_asset,
payback_tag,
payback: SwapPayback::Public { payback_target_id },
}
}
pub fn payback_note_type(&self) -> NoteType {
match self.payback {
SwapPayback::Private { .. } => NoteType::Private,
SwapPayback::Public { .. } => NoteType::Public,
}
}
pub fn requested_asset(&self) -> Asset {
self.requested_asset
}
pub fn payback_tag(&self) -> NoteTag {
self.payback_tag
}
pub fn payback(&self) -> &SwapPayback {
&self.payback
}
pub fn into_recipient(self, serial_num: Word) -> NoteRecipient {
NoteRecipient::new(serial_num, SwapNote::script(), NoteStorage::from(self))
}
}
impl From<SwapNoteStorage> for NoteStorage {
fn from(storage: SwapNoteStorage) -> Self {
let mut storage_values = Vec::with_capacity(SwapNoteStorage::NUM_ITEMS);
storage_values.extend_from_slice(&storage.requested_asset.as_elements());
match storage.payback {
SwapPayback::Private { recipient } => {
storage_values.extend_from_slice(recipient.as_elements());
storage_values.push(Felt::from(NoteType::Private.as_u8()));
storage_values.push(Felt::from(storage.payback_tag.as_u32()));
storage_values.extend_from_slice(&[Felt::ZERO; 2]);
},
SwapPayback::Public { payback_target_id } => {
storage_values.extend_from_slice(&[Felt::ZERO; 4]);
storage_values.push(Felt::from(NoteType::Public.as_u8()));
storage_values.push(Felt::from(storage.payback_tag.as_u32()));
storage_values.push(payback_target_id.suffix());
storage_values.push(payback_target_id.prefix().as_felt());
},
}
NoteStorage::new(storage_values)
.expect("number of storage items should not exceed max storage items")
}
}
impl TryFrom<&[Felt]> for SwapNoteStorage {
type Error = NoteError;
fn try_from(note_storage: &[Felt]) -> Result<Self, Self::Error> {
if note_storage.len() != Self::NUM_ITEMS {
return Err(NoteError::InvalidNoteStorageLength {
expected: Self::NUM_ITEMS,
actual: note_storage.len(),
});
}
let key = Word::new([note_storage[0], note_storage[1], note_storage[2], note_storage[3]]);
let value = Word::new([note_storage[4], note_storage[5], note_storage[6], note_storage[7]]);
let requested_asset = Asset::from_id_and_value_words(key, value)
.map_err(|err| NoteError::other_with_source("failed to parse requested asset", err))?;
let payback_note_type = NoteType::try_from(
u8::try_from(note_storage[12].as_canonical_u64())
.map_err(|_| NoteError::other("payback_note_type exceeds u8"))?,
)
.map_err(|err| NoteError::other_with_source("failed to parse payback note type", err))?;
let payback_tag_u32 = u32::try_from(note_storage[13].as_canonical_u64())
.map_err(|_| NoteError::other("SWAP payback_tag exceeds u32"))?;
let payback_tag = NoteTag::new(payback_tag_u32);
let payback = match payback_note_type {
NoteType::Private => {
if note_storage[14].as_canonical_u64() != 0
|| note_storage[15].as_canonical_u64() != 0
{
return Err(NoteError::other(
"SWAP private payback must have payback target id slots cleared",
));
}
let recipient = Word::new([
note_storage[8],
note_storage[9],
note_storage[10],
note_storage[11],
]);
SwapPayback::Private { recipient }
},
NoteType::Public => {
if note_storage[8..=11].iter().any(|felt| felt.as_canonical_u64() != 0) {
return Err(NoteError::other(
"SWAP public payback must have recipient slots cleared",
));
}
let payback_target_id = AccountId::try_from_elements(
note_storage[14],
note_storage[15],
)
.map_err(|err| {
NoteError::other_with_source("failed to parse payback target account ID", err)
})?;
SwapPayback::Public { payback_target_id }
},
};
Ok(Self { requested_asset, payback_tag, payback })
}
}
pub fn payback_serial_from_swap(swap_serial: Word) -> Word {
let elements = swap_serial.as_elements();
Word::new([elements[0] + ONE, elements[1], elements[2], elements[3]])
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use miden_protocol::account::{AccountIdVersion, AccountType, AssetCallbackFlag};
use miden_protocol::asset::{FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
use miden_protocol::note::{NoteStorage, NoteType};
use miden_protocol::testing::account_id::{
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
};
use super::*;
fn fungible_faucet() -> AccountId {
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap()
}
fn non_fungible_faucet() -> AccountId {
ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET.try_into().unwrap()
}
fn fungible_asset() -> Asset {
Asset::Fungible(FungibleAsset::new(fungible_faucet(), 1000).unwrap())
}
fn non_fungible_asset() -> Asset {
let details = NonFungibleAssetDetails::new(non_fungible_faucet(), vec![0xaa, 0xbb]);
Asset::NonFungible(NonFungibleAsset::new(&details))
}
fn dummy_target_id() -> AccountId {
AccountId::builder().account_type(AccountType::Private).build_with_seed([1; 32])
}
fn dummy_recipient_digest() -> Word {
Word::new([Felt::from(7u32), Felt::from(11u32), Felt::from(13u32), Felt::from(17u32)])
}
fn dummy_payback_tag() -> NoteTag {
NoteTag::new(0xabcd1234)
}
#[test]
fn swap_note_storage_round_trip_fungible_private() {
let storage = SwapNoteStorage::new_private(
fungible_asset(),
dummy_recipient_digest(),
dummy_payback_tag(),
);
let note_storage = NoteStorage::from(storage.clone());
assert_eq!(note_storage.num_items() as usize, SwapNoteStorage::NUM_ITEMS);
assert_eq!(storage.payback_note_type(), NoteType::Private);
assert_eq!(storage.requested_asset(), fungible_asset());
assert_eq!(storage.payback_tag(), dummy_payback_tag());
match storage.payback() {
SwapPayback::Private { recipient } => {
assert_eq!(*recipient, dummy_recipient_digest());
},
SwapPayback::Public { .. } => panic!("expected private payback"),
}
let parsed =
SwapNoteStorage::try_from(note_storage.items()).expect("round trip should succeed");
assert_eq!(parsed, storage);
}
#[test]
fn swap_note_storage_round_trip_non_fungible_public() {
let target = dummy_target_id();
let storage =
SwapNoteStorage::new_public(non_fungible_asset(), target, dummy_payback_tag());
let note_storage = NoteStorage::from(storage.clone());
assert_eq!(note_storage.num_items() as usize, SwapNoteStorage::NUM_ITEMS);
assert_eq!(storage.payback_note_type(), NoteType::Public);
assert_eq!(storage.requested_asset(), non_fungible_asset());
assert_eq!(storage.payback_tag(), dummy_payback_tag());
match storage.payback() {
SwapPayback::Public { payback_target_id } => {
assert_eq!(*payback_target_id, target);
},
SwapPayback::Private { .. } => panic!("expected public payback"),
}
let parsed =
SwapNoteStorage::try_from(note_storage.items()).expect("round trip should succeed");
assert_eq!(parsed, storage);
}
#[test]
fn swap_note_storage_private_rejects_dirty_target_slots() {
let mut items: Vec<Felt> = NoteStorage::from(SwapNoteStorage::new_private(
fungible_asset(),
dummy_recipient_digest(),
dummy_payback_tag(),
))
.items()
.to_vec();
items[14] = Felt::from(1u32);
let err = SwapNoteStorage::try_from(items.as_slice())
.expect_err("private payback with a dirty target slot must be rejected");
assert_matches!(
err,
NoteError::Other { error_msg, .. }
if error_msg == "SWAP private payback must have payback target id slots cleared".into()
);
}
#[test]
fn swap_note_storage_public_rejects_dirty_private_slots() {
let mut items: Vec<Felt> = NoteStorage::from(SwapNoteStorage::new_public(
fungible_asset(),
dummy_target_id(),
dummy_payback_tag(),
))
.items()
.to_vec();
items[8] = Felt::from(1u32);
let err = SwapNoteStorage::try_from(items.as_slice())
.expect_err("public payback with a dirty recipient slot must be rejected");
assert_matches!(
err,
NoteError::Other { error_msg, .. }
if error_msg == "SWAP public payback must have recipient slots cleared".into()
);
}
#[test]
fn swap_tag() {
let mut fungible_faucet_id_bytes = [0; 15];
fungible_faucet_id_bytes[0] = 0xcd;
fungible_faucet_id_bytes[1] = 0xb1;
let mut non_fungible_faucet_id_bytes = [0; 15];
non_fungible_faucet_id_bytes[0] = 0xab;
non_fungible_faucet_id_bytes[1] = 0xec;
let offered_asset = Asset::Fungible(
FungibleAsset::new(
AccountId::dummy(
fungible_faucet_id_bytes,
AccountIdVersion::Version1,
AccountType::Public,
AssetCallbackFlag::Disabled,
),
2500,
)
.unwrap(),
);
let requested_asset =
Asset::NonFungible(NonFungibleAsset::new(&NonFungibleAssetDetails::new(
AccountId::dummy(
non_fungible_faucet_id_bytes,
AccountIdVersion::Version1,
AccountType::Public,
AssetCallbackFlag::Disabled,
),
vec![0xaa, 0xbb, 0xcc, 0xdd],
)));
let expected_asset_pair = 0xcdab;
let note_type = NoteType::Public;
let actual_tag = SwapNote::build_tag(note_type, &offered_asset, &requested_asset);
assert_eq!(actual_tag.as_u32() as u16, expected_asset_pair, "asset pair should match");
assert_eq!((actual_tag.as_u32() >> 31) as u8, note_type as u8, "note type should match");
assert_eq!(
(actual_tag.as_u32() >> 23) as u8,
SwapNote::script_root().as_bytes()[0],
"swap script root byte 0 should match"
);
assert_eq!(
((actual_tag.as_u32() & 0b00000000_01111111_00000000_00000000) >> 16) as u8,
SwapNote::script_root().as_bytes()[1] >> 1,
"swap script root byte 1 should match with the highest bit set to zero"
);
}
}