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,
NoteAttachment,
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::costs::{NoteConsumptionCost, SWAP_CONSUMPTION_CYCLES};
use crate::note::{P2idNote, 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_package_reference(standards_lib.as_ref(), path)
.expect("Standards library contains SWAP note script procedure")
});
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SwapNote {
sender: AccountId,
offered_asset: Asset,
serial_number: Word,
note_type: NoteType,
storage: SwapNoteStorage,
attachments: NoteAttachments,
}
#[bon::bon]
impl SwapNote {
#[builder]
pub fn new(
#[builder(field)] attachments: Vec<NoteAttachment>,
sender: AccountId,
#[builder(into)] offered_asset: Asset,
#[builder(into)] requested_asset: Asset,
serial_number: Word,
#[builder(default)]
note_type: NoteType,
#[builder(default)]
payback_note_type: NoteType,
) -> Result<Self, NoteError> {
if requested_asset == offered_asset {
return Err(NoteError::other("requested asset same as offered asset"));
}
let attachments = NoteAttachments::new(attachments)?;
let payback_tag = NoteTag::with_account_target(sender);
let storage = match payback_note_type {
NoteType::Private => SwapNoteStorage::new_private(
requested_asset,
Self::payback_recipient(sender, serial_number).digest(),
payback_tag,
),
NoteType::Public => SwapNoteStorage::new_public(requested_asset, sender, payback_tag),
};
Ok(Self {
sender,
offered_asset,
serial_number,
note_type,
storage,
attachments,
})
}
}
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 sender(&self) -> AccountId {
self.sender
}
pub fn offered_asset(&self) -> Asset {
self.offered_asset
}
pub fn requested_asset(&self) -> Asset {
self.storage().requested_asset()
}
pub fn serial_number(&self) -> Word {
self.serial_number
}
pub fn note_type(&self) -> NoteType {
self.note_type
}
pub fn payback_note_type(&self) -> NoteType {
self.storage().payback_note_type()
}
pub fn attachments(&self) -> &NoteAttachments {
&self.attachments
}
pub fn storage(&self) -> &SwapNoteStorage {
&self.storage
}
pub fn payback_note_details(&self) -> NoteDetails {
let assets = NoteAssets::new(vec![self.requested_asset()])
.expect("a single asset never exceeds the note asset limit");
NoteDetails::new(assets, Self::payback_recipient(self.sender, self.serial_number))
}
pub fn create_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)
}
fn payback_recipient(sender: AccountId, serial_number: Word) -> NoteRecipient {
P2idNoteStorage::new(sender).into_recipient(payback_serial_from_swap(serial_number))
}
}
impl<S: swap_note_builder::State> SwapNoteBuilder<S> {
pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
self.attachments.push(attachment.into());
self
}
pub fn attachments(
mut self,
attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
) -> Self {
self.attachments.extend(attachments.into_iter().map(Into::into));
self
}
}
impl<S: swap_note_builder::State> SwapNoteBuilder<S>
where
S::SerialNumber: swap_note_builder::IsUnset,
{
pub fn generate_serial_number(
self,
rng: &mut impl FeltRng,
) -> SwapNoteBuilder<swap_note_builder::SetSerialNumber<S>> {
self.serial_number(rng.draw_word())
}
}
impl From<SwapNote> for Note {
fn from(note: SwapNote) -> Self {
let SwapNote {
sender,
offered_asset,
serial_number,
note_type,
storage,
attachments,
} = note;
let tag = SwapNote::create_tag(note_type, &offered_asset, &storage.requested_asset());
let metadata = PartialNoteMetadata::new(sender, note_type).with_tag(tag);
let recipient = storage.into_recipient(serial_number);
let assets = NoteAssets::new(vec![offered_asset])
.expect("a single asset never exceeds the note asset limit");
Note::with_attachments(assets, metadata, recipient, attachments)
}
}
#[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]])
}
impl NoteConsumptionCost for SwapNote {
fn consumption_cycles() -> u32 {
SWAP_CONSUMPTION_CYCLES
}
fn created_notes() -> Vec<NoteScriptRoot> {
vec![P2idNote::script_root()]
}
}
#[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::crypto::rand::RandomCoin;
use miden_protocol::note::{NoteStorage, NoteType};
use miden_protocol::testing::account_id::{
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
};
use rstest::rstest;
use super::*;
use crate::note::{NetworkAccountTarget, P2idNote};
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)
}
#[rstest]
#[case::private_payback(NoteType::Private)]
#[case::public_payback(NoteType::Public)]
fn swap_note_builder(#[case] payback_note_type: NoteType) -> anyhow::Result<()> {
let sender = dummy_target_id();
let attachment = NoteAttachment::with_word(
NetworkAccountTarget::ATTACHMENT_SCHEME,
dummy_recipient_digest(),
);
let swap_note_type = NoteType::Public;
let swap_note = SwapNote::builder()
.sender(sender)
.offered_asset(fungible_asset())
.requested_asset(non_fungible_asset())
.note_type(swap_note_type)
.payback_note_type(payback_note_type)
.attachment(attachment.clone())
.generate_serial_number(&mut RandomCoin::new(Word::from([1, 2, 3, 4u32])))
.build()?;
let serial_number = swap_note.serial_number();
let storage = swap_note.storage().clone();
let payback_note = swap_note.payback_note_details();
let note = Note::from(swap_note);
assert_eq!(note.metadata().sender(), sender);
assert_eq!(note.metadata().note_type(), swap_note_type);
assert_eq!(
note.metadata().tag(),
SwapNote::create_tag(swap_note_type, &fungible_asset(), &non_fungible_asset())
);
assert_eq!(note.assets().num_assets(), 1);
assert_eq!(note.assets().iter().next(), Some(&fungible_asset()));
assert_eq!(note.attachments().get(0), Some(&attachment));
assert_eq!(note.recipient().script().root(), SwapNote::script_root());
assert_eq!(
SwapNoteStorage::try_from(note.recipient().storage().items())?,
storage,
"the note's storage must match the one derived from the SWAP note"
);
assert_eq!(storage.payback_tag(), NoteTag::with_account_target(sender));
assert_eq!(payback_note.assets().iter().next(), Some(&non_fungible_asset()));
assert_eq!(payback_note.recipient().serial_num(), payback_serial_from_swap(serial_number));
assert_eq!(payback_note.recipient().script().root(), P2idNote::script_root());
match (payback_note_type, storage.payback()) {
(NoteType::Private, SwapPayback::Private { recipient }) => {
assert_eq!(*recipient, payback_note.recipient().digest());
},
(NoteType::Public, SwapPayback::Public { payback_target_id }) => {
assert_eq!(*payback_target_id, sender);
},
(note_type, payback) => panic!("payback {payback:?} does not match {note_type:?}"),
}
Ok(())
}
#[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::create_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"
);
}
}