use alloc::vec;
use miden_protocol::account::AccountId;
use miden_protocol::assembly::Path;
use miden_protocol::asset::{Asset, AssetAmount, AssetCallbackFlag, FungibleAsset};
use miden_protocol::errors::NoteError;
use miden_protocol::note::{
Note,
NoteAssets,
NoteAttachment,
NoteAttachmentScheme,
NoteAttachments,
NoteRecipient,
NoteScript,
NoteScriptRoot,
NoteStorage,
NoteTag,
NoteType,
PartialNoteMetadata,
};
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, ONE, Word, ZERO};
use crate::StandardsLib;
use crate::note::{P2idNoteStorage, StandardNoteAttachment};
const PSWAP_SCRIPT_PATH: &str = "::miden::standards::notes::pswap::main";
static PSWAP_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let standards_lib = StandardsLib::default();
let path = Path::new(PSWAP_SCRIPT_PATH);
NoteScript::from_library_reference(standards_lib.as_ref(), path)
.expect("Standards library contains PSWAP note script procedure")
});
#[derive(Debug, Clone, PartialEq, Eq, bon::Builder)]
pub struct PswapNoteStorage {
requested_asset: FungibleAsset,
creator_account_id: AccountId,
#[builder(default = NoteType::Private)]
payback_note_type: NoteType,
}
impl PswapNoteStorage {
pub const NUM_STORAGE_ITEMS: usize = 7;
pub fn into_recipient(self, serial_num: Word) -> NoteRecipient {
NoteRecipient::new(serial_num, PswapNote::script(), NoteStorage::from(self))
}
pub fn requested_asset(&self) -> &FungibleAsset {
&self.requested_asset
}
pub fn payback_note_tag(&self) -> NoteTag {
NoteTag::with_account_target(self.creator_account_id)
}
pub fn creator_account_id(&self) -> AccountId {
self.creator_account_id
}
pub fn payback_note_type(&self) -> NoteType {
self.payback_note_type
}
pub fn requested_faucet_id(&self) -> AccountId {
self.requested_asset.faucet_id()
}
pub fn requested_asset_amount(&self) -> u64 {
self.requested_asset.amount().as_u64()
}
}
impl From<PswapNoteStorage> for NoteStorage {
fn from(storage: PswapNoteStorage) -> Self {
let storage_items = vec![
Felt::from(storage.requested_asset.callbacks().as_u8()),
storage.requested_asset.faucet_id().suffix(),
storage.requested_asset.faucet_id().prefix().as_felt(),
Felt::from(storage.requested_asset.amount()),
Felt::from(storage.payback_note_type.as_u8()),
storage.creator_account_id.prefix().as_felt(),
storage.creator_account_id.suffix(),
];
NoteStorage::new(storage_items)
.expect("number of storage items should not exceed max storage items")
}
}
impl TryFrom<&[Felt]> for PswapNoteStorage {
type Error = NoteError;
fn try_from(note_storage: &[Felt]) -> Result<Self, Self::Error> {
if note_storage.len() != Self::NUM_STORAGE_ITEMS {
return Err(NoteError::InvalidNoteStorageLength {
expected: Self::NUM_STORAGE_ITEMS,
actual: note_storage.len(),
});
}
let callbacks = AssetCallbackFlag::try_from(
u8::try_from(note_storage[0].as_canonical_u64())
.map_err(|_| NoteError::other("enable_callbacks exceeds u8"))?,
)
.map_err(|e| NoteError::other_with_source("failed to parse asset callback flag", e))?;
let faucet_id = AccountId::try_from_elements(note_storage[1], note_storage[2])
.map_err(|e| NoteError::other_with_source("failed to parse requested faucet ID", e))?;
let amount = note_storage[3].as_canonical_u64();
let requested_asset = FungibleAsset::new(faucet_id, amount)
.map_err(|e| NoteError::other_with_source("failed to create requested asset", e))?
.with_callbacks(callbacks);
let payback_note_type = NoteType::try_from(
u8::try_from(note_storage[4].as_canonical_u64())
.map_err(|_| NoteError::other("payback_note_type exceeds u8"))?,
)
.map_err(|e| NoteError::other_with_source("failed to parse payback note type", e))?;
let creator_account_id = AccountId::try_from_elements(note_storage[6], note_storage[5])
.map_err(|e| NoteError::other_with_source("failed to parse creator account ID", e))?;
Ok(Self {
requested_asset,
creator_account_id,
payback_note_type,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PswapNoteAttachment {
amount: AssetAmount,
order_id: Felt,
depth: u32,
}
impl PswapNoteAttachment {
pub fn new(amount: AssetAmount, order_id: Felt, depth: u32) -> Self {
Self { amount, order_id, depth }
}
pub fn amount(&self) -> AssetAmount {
self.amount
}
pub fn order_id(&self) -> Felt {
self.order_id
}
pub fn depth(&self) -> u32 {
self.depth
}
}
impl From<PswapNoteAttachment> for NoteAttachment {
fn from(attachment: PswapNoteAttachment) -> Self {
let word = Word::from([
Felt::from(attachment.amount),
attachment.order_id,
Felt::from(attachment.depth),
ZERO,
]);
NoteAttachment::with_word(PswapNote::PSWAP_ATTACHMENT_SCHEME, word)
}
}
#[derive(Debug, Clone, bon::Builder)]
#[builder(finish_fn(vis = "", name = build_internal))]
pub struct PswapNote {
sender: AccountId,
storage: PswapNoteStorage,
serial_number: Word,
#[builder(default = NoteType::Private)]
note_type: NoteType,
offered_asset: FungibleAsset,
attachment: Option<NoteAttachment>,
}
impl<S: pswap_note_builder::State> PswapNoteBuilder<S>
where
S: pswap_note_builder::IsComplete,
{
pub fn build(self) -> Result<PswapNote, NoteError> {
let note = self.build_internal();
if note.offered_asset.faucet_id() == note.storage.requested_faucet_id() {
return Err(NoteError::other(
"offered and requested assets must have different faucets",
));
}
Ok(note)
}
}
impl PswapNote {
pub const NUM_STORAGE_ITEMS: usize = PswapNoteStorage::NUM_STORAGE_ITEMS;
pub const PSWAP_ATTACHMENT_SCHEME: NoteAttachmentScheme =
StandardNoteAttachment::PswapAttachment.attachment_scheme();
const PARENT_ATTACHMENT_DEPTH_OFFSET: usize = 2;
pub fn script() -> NoteScript {
PSWAP_SCRIPT.clone()
}
pub fn script_root() -> NoteScriptRoot {
PSWAP_SCRIPT.root()
}
pub fn create_args(account_fill: u64, note_fill: u64) -> Result<Word, NoteError> {
let account_fill = Felt::try_from(account_fill)
.map_err(|e| NoteError::other_with_source("account_fill is not a valid felt", e))?;
let note_fill = Felt::try_from(note_fill)
.map_err(|e| NoteError::other_with_source("note_fill is not a valid felt", e))?;
Ok(Word::from([account_fill, note_fill, ZERO, ZERO]))
}
pub fn sender(&self) -> AccountId {
self.sender
}
pub fn storage(&self) -> &PswapNoteStorage {
&self.storage
}
pub fn serial_number(&self) -> Word {
self.serial_number
}
pub fn note_type(&self) -> NoteType {
self.note_type
}
pub fn offered_asset(&self) -> &FungibleAsset {
&self.offered_asset
}
pub fn attachments(&self) -> Option<&NoteAttachment> {
self.attachment.as_ref()
}
pub fn order_id(&self) -> Felt {
self.serial_number[1]
}
pub fn parent_depth(&self) -> u64 {
match self.attachment.as_ref() {
Some(att) if att.attachment_scheme() == Self::PSWAP_ATTACHMENT_SCHEME => {
let attachment_word = att.content().as_words()[0];
attachment_word[Self::PARENT_ATTACHMENT_DEPTH_OFFSET].as_canonical_u64()
},
_ => 0,
}
}
pub fn execute_full_fill(&self, consumer_account_id: AccountId) -> Result<Note, NoteError> {
let requested_faucet_id = self.storage.requested_faucet_id();
let total_requested_amount = self.storage.requested_asset_amount();
let fill_asset = FungibleAsset::new(requested_faucet_id, total_requested_amount)
.map_err(|e| NoteError::other_with_source("failed to create full fill asset", e))?
.with_callbacks(self.storage.requested_asset().callbacks());
self.create_payback_note(consumer_account_id, fill_asset, total_requested_amount)
}
pub fn execute(
&self,
consumer_account_id: AccountId,
account_fill_asset: Option<FungibleAsset>,
note_fill_asset: Option<FungibleAsset>,
) -> Result<(Note, Option<PswapNote>), NoteError> {
let payback_asset = match (account_fill_asset, note_fill_asset) {
(Some(account_fill), Some(note_fill)) => account_fill.add(note_fill).map_err(|e| {
NoteError::other_with_source(
"failed to combine account fill and note fill assets",
e,
)
})?,
(Some(asset), None) | (None, Some(asset)) => asset,
(None, None) => {
return Err(NoteError::other(
"at least one of account_fill_asset or note_fill_asset must be provided",
));
},
};
let fill_amount = payback_asset.amount().as_u64();
let total_offered_amount = self.offered_asset.amount().as_u64();
let requested_faucet_id = self.storage.requested_faucet_id();
let total_requested_amount = self.storage.requested_asset_amount();
if fill_amount == 0 {
return Err(NoteError::other("Fill amount must be greater than 0"));
}
if fill_amount > total_requested_amount {
return Err(NoteError::other(alloc::format!(
"Fill amount {} exceeds requested amount {}",
fill_amount,
total_requested_amount
)));
}
let account_fill_amount = account_fill_asset.as_ref().map_or(0, |a| a.amount().as_u64());
let note_fill_amount = note_fill_asset.as_ref().map_or(0, |a| a.amount().as_u64());
let payout_for_account_fill = Self::calculate_output_amount(
total_offered_amount,
total_requested_amount,
account_fill_amount,
)?;
let payout_for_note_fill = Self::calculate_output_amount(
total_offered_amount,
total_requested_amount,
note_fill_amount,
)?;
let offered_amount_for_fill = payout_for_account_fill + payout_for_note_fill;
let payback_note =
self.create_payback_note(consumer_account_id, payback_asset, fill_amount)?;
let remainder = if fill_amount < total_requested_amount {
let remaining_offered = total_offered_amount - offered_amount_for_fill;
let remaining_requested = total_requested_amount - fill_amount;
let remaining_offered_asset =
FungibleAsset::new(self.offered_asset.faucet_id(), remaining_offered)
.map_err(|e| {
NoteError::other_with_source("failed to create remainder asset", e)
})?
.with_callbacks(self.offered_asset.callbacks());
let remaining_requested_asset =
FungibleAsset::new(requested_faucet_id, remaining_requested)
.map_err(|e| {
NoteError::other_with_source(
"failed to create remaining requested asset",
e,
)
})?
.with_callbacks(self.storage.requested_asset().callbacks());
Some(self.create_remainder_pswap_note(
consumer_account_id,
remaining_offered_asset,
remaining_requested_asset,
offered_amount_for_fill,
)?)
} else {
None
};
Ok((payback_note, remainder))
}
pub fn calculate_offered_for_requested(&self, fill_amount: u64) -> Result<u64, NoteError> {
let total_requested = self.storage.requested_asset_amount();
let total_offered = self.offered_asset.amount().as_u64();
Self::calculate_output_amount(total_offered, total_requested, fill_amount)
}
pub fn payback_note(
&self,
consumer_account_id: AccountId,
attachment: &PswapNoteAttachment,
) -> Result<Note, NoteError> {
let depth = attachment.depth();
if depth == 0 {
return Err(NoteError::other("depth must be >= 1"));
}
let parent_depth = Felt::from(depth - 1);
let p2id_serial = Word::from([
self.serial_number[0] + ONE,
self.serial_number[1],
self.serial_number[2],
self.serial_number[3] + parent_depth,
]);
let recipient =
P2idNoteStorage::new(self.storage.creator_account_id).into_recipient(p2id_serial);
let fill_asset =
FungibleAsset::new(self.storage.requested_faucet_id(), u64::from(attachment.amount()))
.map_err(|e| NoteError::other_with_source("invalid fill amount", e))?
.with_callbacks(self.storage.requested_asset().callbacks());
let assets = NoteAssets::new(vec![fill_asset.into()])?;
let metadata =
PartialNoteMetadata::new(consumer_account_id, self.storage.payback_note_type)
.with_tag(self.storage.payback_note_tag());
Ok(Note::with_attachments(
assets,
metadata,
recipient,
NoteAttachments::from(NoteAttachment::from(*attachment)),
))
}
pub fn remainder_note(
&self,
consumer_account_id: AccountId,
attachment: &PswapNoteAttachment,
remaining_offered: AssetAmount,
remaining_requested: AssetAmount,
) -> Result<Note, NoteError> {
let depth = attachment.depth();
if depth == 0 {
return Err(NoteError::other("depth must be >= 1"));
}
let remainder_serial = Word::from([
self.serial_number[0],
self.serial_number[1],
self.serial_number[2],
self.serial_number[3] + Felt::from(depth),
]);
let requested_asset =
FungibleAsset::new(self.storage.requested_faucet_id(), u64::from(remaining_requested))
.map_err(|e| NoteError::other_with_source("invalid remaining_requested amount", e))?
.with_callbacks(self.storage.requested_asset().callbacks());
let offered_asset =
FungibleAsset::new(self.offered_asset.faucet_id(), u64::from(remaining_offered))
.map_err(|e| NoteError::other_with_source("invalid remaining_offered amount", e))?
.with_callbacks(self.offered_asset.callbacks());
let new_storage = PswapNoteStorage::builder()
.requested_asset(requested_asset)
.creator_account_id(self.storage.creator_account_id)
.payback_note_type(self.storage.payback_note_type)
.build();
let recipient = new_storage.into_recipient(remainder_serial);
let assets = NoteAssets::new(vec![offered_asset.into()])?;
let tag = Self::create_tag(self.note_type, &offered_asset, &requested_asset);
let metadata = PartialNoteMetadata::new(consumer_account_id, self.note_type).with_tag(tag);
Ok(Note::with_attachments(
assets,
metadata,
recipient,
NoteAttachments::from(NoteAttachment::from(*attachment)),
))
}
pub fn create_tag(
note_type: NoteType,
offered_asset: &FungibleAsset,
requested_asset: &FungibleAsset,
) -> NoteTag {
let pswap_root_bytes = Self::script().root().as_bytes();
let mut pswap_use_case_id = (pswap_root_bytes[0] as u16) << 6;
pswap_use_case_id |= (pswap_root_bytes[1] >> 2) 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) << 30)
| ((pswap_use_case_id as u32) << 16)
| asset_pair as u32;
NoteTag::new(tag)
}
fn calculate_output_amount(
offered_total: u64,
requested_total: u64,
fill_amount: u64,
) -> Result<u64, NoteError> {
let product = (offered_total as u128) * (fill_amount as u128);
let quotient = product / (requested_total as u128);
let amount = u64::try_from(quotient)
.map_err(|_| NoteError::other("payout quotient does not fit in u64"))?;
AssetAmount::new(amount).map_err(|e| {
NoteError::other_with_source("payout amount exceeds max fungible asset amount", e)
})?;
Ok(amount)
}
fn pswap_output_attachment(
amount: u64,
order_id: Felt,
depth: u64,
) -> Result<NoteAttachment, NoteError> {
let amount = AssetAmount::new(amount)
.map_err(|e| NoteError::other_with_source("amount is not a valid asset amount", e))?;
let depth = u32::try_from(depth)
.map_err(|_| NoteError::other("PSWAP depth does not fit in u32"))?;
Ok(PswapNoteAttachment::new(amount, order_id, depth).into())
}
fn create_payback_note(
&self,
consumer_account_id: AccountId,
payback_asset: FungibleAsset,
fill_amount: u64,
) -> Result<Note, NoteError> {
let payback_note_tag = self.storage.payback_note_tag();
let p2id_serial_num = Word::from([
self.serial_number[0] + ONE,
self.serial_number[1],
self.serial_number[2],
self.serial_number[3],
]);
let recipient =
P2idNoteStorage::new(self.storage.creator_account_id).into_recipient(p2id_serial_num);
let current_depth = self.parent_depth() + 1;
let attachment =
Self::pswap_output_attachment(fill_amount, self.order_id(), current_depth)?;
let p2id_assets = NoteAssets::new(vec![payback_asset.into()])?;
let p2id_metadata =
PartialNoteMetadata::new(consumer_account_id, self.storage.payback_note_type)
.with_tag(payback_note_tag);
Ok(Note::with_attachments(
p2id_assets,
p2id_metadata,
recipient,
NoteAttachments::from(attachment),
))
}
fn create_remainder_pswap_note(
&self,
consumer_account_id: AccountId,
remaining_offered_asset: FungibleAsset,
remaining_requested_asset: FungibleAsset,
offered_amount_for_fill: u64,
) -> Result<PswapNote, NoteError> {
let new_storage = PswapNoteStorage::builder()
.requested_asset(remaining_requested_asset)
.creator_account_id(self.storage.creator_account_id)
.payback_note_type(self.storage.payback_note_type)
.build();
let remainder_serial_num = Word::from([
self.serial_number[0],
self.serial_number[1],
self.serial_number[2],
self.serial_number[3] + ONE,
]);
let current_depth = self.parent_depth() + 1;
let attachment =
Self::pswap_output_attachment(offered_amount_for_fill, self.order_id(), current_depth)?;
PswapNote::builder()
.sender(consumer_account_id)
.storage(new_storage)
.serial_number(remainder_serial_num)
.note_type(self.note_type)
.offered_asset(remaining_offered_asset)
.attachment(attachment)
.build()
}
}
impl From<PswapNote> for Note {
fn from(pswap: PswapNote) -> Self {
let tag = PswapNote::create_tag(
pswap.note_type,
&pswap.offered_asset,
pswap.storage.requested_asset(),
);
let recipient = pswap.storage.into_recipient(pswap.serial_number);
let assets = NoteAssets::new(vec![pswap.offered_asset.into()])
.expect("single fungible asset should be valid");
let metadata = PartialNoteMetadata::new(pswap.sender, pswap.note_type).with_tag(tag);
let attachments = pswap.attachment.map(NoteAttachments::from).unwrap_or_default();
Note::with_attachments(assets, metadata, recipient, attachments)
}
}
impl TryFrom<&Note> for PswapNote {
type Error = NoteError;
fn try_from(note: &Note) -> Result<Self, Self::Error> {
if note.recipient().script().root() != PswapNote::script_root() {
return Err(NoteError::other("note script root does not match PSWAP script root"));
}
let storage = PswapNoteStorage::try_from(note.recipient().storage().items())?;
if note.assets().num_assets() != 1 {
return Err(NoteError::other("PSWAP note must have exactly one asset"));
}
let offered_asset = match note.assets().iter().next().unwrap() {
Asset::Fungible(fa) => *fa,
Asset::NonFungible(_) => {
return Err(NoteError::other("PSWAP note asset must be fungible"));
},
};
let attachment = match note.attachments().num_attachments() {
0 => None,
1 => {
Some(note.attachments().get(0).expect("length should have been validated").clone())
},
_ => return Err(NoteError::other("pswap note supports only one attachment")),
};
PswapNote::builder()
.sender(note.metadata().sender())
.storage(storage)
.serial_number(note.recipient().serial_num())
.note_type(note.metadata().note_type())
.offered_asset(offered_asset)
.maybe_attachment(attachment)
.build()
}
}
#[cfg(test)]
mod tests {
use miden_protocol::account::{AccountId, AccountIdVersion, AccountType};
use miden_protocol::asset::FungibleAsset;
use miden_protocol::crypto::rand::{FeltRng, RandomCoin};
use super::*;
fn dummy_faucet_id(byte: u8) -> AccountId {
let mut bytes = [0; 15];
bytes[0] = byte;
AccountId::dummy(bytes, AccountIdVersion::Version1, AccountType::Public)
}
fn dummy_creator_id() -> AccountId {
AccountId::dummy([1; 15], AccountIdVersion::Version1, AccountType::Public)
}
fn dummy_consumer_id() -> AccountId {
AccountId::dummy([2; 15], AccountIdVersion::Version1, AccountType::Public)
}
fn build_pswap_note(
offered_asset: FungibleAsset,
requested_asset: FungibleAsset,
creator_id: AccountId,
) -> (PswapNote, Note) {
let mut rng = RandomCoin::new(Word::default());
let storage = PswapNoteStorage::builder()
.requested_asset(requested_asset)
.creator_account_id(creator_id)
.build();
let pswap = PswapNote::builder()
.sender(creator_id)
.storage(storage)
.serial_number(rng.draw_word())
.note_type(NoteType::Public)
.offered_asset(offered_asset)
.build()
.unwrap();
let note: Note = pswap.clone().into();
(pswap, note)
}
#[test]
fn pswap_note_creation_and_script() {
let creator_id = dummy_creator_id();
let offered_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 1000).unwrap();
let requested_asset = FungibleAsset::new(dummy_faucet_id(0xbb), 500).unwrap();
let (pswap, note) = build_pswap_note(offered_asset, requested_asset, creator_id);
assert_eq!(pswap.sender(), creator_id);
assert_eq!(pswap.note_type(), NoteType::Public);
let script = PswapNote::script();
assert!(Word::from(script.root()) != Word::default(), "Script root should not be zero");
assert_eq!(note.metadata().sender(), creator_id);
assert_eq!(note.metadata().note_type(), NoteType::Public);
assert_eq!(note.assets().num_assets(), 1);
assert_eq!(note.recipient().script().root(), script.root());
assert_eq!(
note.recipient().storage().num_items(),
PswapNoteStorage::NUM_STORAGE_ITEMS as u16,
);
}
#[test]
fn pswap_note_builder() {
let creator_id = dummy_creator_id();
let offered_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 1000).unwrap();
let requested_asset = FungibleAsset::new(dummy_faucet_id(0xbb), 500).unwrap();
let (pswap, note) = build_pswap_note(offered_asset, requested_asset, creator_id);
assert_eq!(pswap.sender(), creator_id);
assert_eq!(pswap.note_type(), NoteType::Public);
assert_eq!(note.metadata().sender(), creator_id);
assert_eq!(note.metadata().note_type(), NoteType::Public);
assert_eq!(note.assets().num_assets(), 1);
assert_eq!(
note.recipient().storage().num_items(),
PswapNoteStorage::NUM_STORAGE_ITEMS as u16,
);
}
#[test]
fn pswap_tag() {
let mut offered_faucet_bytes = [0; 15];
offered_faucet_bytes[0] = 0xcd;
offered_faucet_bytes[1] = 0xb1;
let mut requested_faucet_bytes = [0; 15];
requested_faucet_bytes[0] = 0xab;
requested_faucet_bytes[1] = 0xec;
let offered_asset = FungibleAsset::new(
AccountId::dummy(offered_faucet_bytes, AccountIdVersion::Version1, AccountType::Public),
100,
)
.unwrap();
let requested_asset = FungibleAsset::new(
AccountId::dummy(
requested_faucet_bytes,
AccountIdVersion::Version1,
AccountType::Public,
),
200,
)
.unwrap();
let tag = PswapNote::create_tag(NoteType::Public, &offered_asset, &requested_asset);
let tag_u32 = u32::from(tag);
let note_type_bits = tag_u32 >> 30;
assert_eq!(note_type_bits, NoteType::Public as u32);
}
#[test]
fn calculate_output_amount() {
assert_eq!(PswapNote::calculate_output_amount(100, 100, 50).unwrap(), 50); assert_eq!(PswapNote::calculate_output_amount(200, 100, 50).unwrap(), 100); assert_eq!(PswapNote::calculate_output_amount(100, 200, 50).unwrap(), 25);
let result = PswapNote::calculate_output_amount(100, 73, 7).unwrap();
assert!(result > 0, "Should produce non-zero output");
}
#[test]
fn pswap_note_storage_try_from() {
let creator_id = dummy_creator_id();
let requested_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 500).unwrap();
let storage_items = vec![
Felt::from(requested_asset.callbacks().as_u8()),
requested_asset.faucet_id().suffix(),
requested_asset.faucet_id().prefix().as_felt(),
Felt::from(requested_asset.amount()),
Felt::from(NoteType::Private.as_u8()), creator_id.prefix().as_felt(),
creator_id.suffix(),
];
let parsed = PswapNoteStorage::try_from(storage_items.as_slice()).unwrap();
assert_eq!(parsed.creator_account_id(), creator_id);
assert_eq!(parsed.requested_asset_amount(), 500);
}
#[test]
fn pswap_note_storage_roundtrip() {
let creator_id = dummy_creator_id();
let requested_asset = FungibleAsset::new(dummy_faucet_id(0xaa), 500).unwrap();
let storage = PswapNoteStorage::builder()
.requested_asset(requested_asset)
.creator_account_id(creator_id)
.build();
let note_storage = NoteStorage::from(storage.clone());
let parsed = PswapNoteStorage::try_from(note_storage.items()).unwrap();
assert_eq!(parsed.creator_account_id(), creator_id);
assert_eq!(parsed.requested_asset_amount(), 500);
}
#[test]
fn pswap_execute_combined_account_fill_and_note_fill_partial_fill() {
let creator_id = dummy_creator_id();
let consumer_id = dummy_consumer_id();
let offered_faucet = dummy_faucet_id(0xaa);
let requested_faucet = dummy_faucet_id(0xbb);
let offered_asset = FungibleAsset::new(offered_faucet, 100).unwrap();
let requested_asset = FungibleAsset::new(requested_faucet, 50).unwrap();
let (pswap, _) = build_pswap_note(offered_asset, requested_asset, creator_id);
let account_fill = FungibleAsset::new(requested_faucet, 10).unwrap();
let note_fill = FungibleAsset::new(requested_faucet, 20).unwrap();
let (payback, remainder) =
pswap.execute(consumer_id, Some(account_fill), Some(note_fill)).unwrap();
assert_eq!(payback.assets().num_assets(), 1);
let payback_asset = payback.assets().iter().next().unwrap();
let Asset::Fungible(fa) = payback_asset else {
panic!("expected fungible payback asset");
};
assert_eq!(fa.faucet_id(), requested_faucet);
assert_eq!(fa.amount().as_u64(), 30);
let remainder = remainder.expect("partial fill should produce remainder");
assert_eq!(remainder.storage().requested_asset_amount(), 20);
assert_eq!(remainder.offered_asset().amount().as_u64(), 40);
assert_eq!(remainder.storage().creator_account_id(), creator_id);
}
#[test]
fn pswap_execute_combined_account_fill_and_note_fill_full_fill() {
let creator_id = dummy_creator_id();
let consumer_id = dummy_consumer_id();
let offered_faucet = dummy_faucet_id(0xaa);
let requested_faucet = dummy_faucet_id(0xbb);
let offered_asset = FungibleAsset::new(offered_faucet, 100).unwrap();
let requested_asset = FungibleAsset::new(requested_faucet, 50).unwrap();
let (pswap, _) = build_pswap_note(offered_asset, requested_asset, creator_id);
let account_fill = FungibleAsset::new(requested_faucet, 30).unwrap();
let note_fill = FungibleAsset::new(requested_faucet, 20).unwrap();
let (payback, remainder) =
pswap.execute(consumer_id, Some(account_fill), Some(note_fill)).unwrap();
assert_eq!(payback.assets().num_assets(), 1);
let payback_asset = payback.assets().iter().next().unwrap();
let Asset::Fungible(fa) = payback_asset else {
panic!("expected fungible payback asset");
};
assert_eq!(fa.faucet_id(), requested_faucet);
assert_eq!(fa.amount().as_u64(), 50);
assert!(remainder.is_none(), "full fill must not produce a remainder");
}
#[test]
fn pswap_output_assets_preserve_callback_flag() {
let creator_id = dummy_creator_id();
let consumer_id = dummy_consumer_id();
let offered_faucet = dummy_faucet_id(0xaa);
let requested_faucet = dummy_faucet_id(0xbb);
let offered_asset = FungibleAsset::new(offered_faucet, 100)
.unwrap()
.with_callbacks(AssetCallbackFlag::Enabled);
let requested_asset = FungibleAsset::new(requested_faucet, 50)
.unwrap()
.with_callbacks(AssetCallbackFlag::Enabled);
let (pswap, _) = build_pswap_note(offered_asset, requested_asset, creator_id);
let account_fill = FungibleAsset::new(requested_faucet, 20)
.unwrap()
.with_callbacks(AssetCallbackFlag::Enabled);
let (payback, remainder) = pswap.execute(consumer_id, Some(account_fill), None).unwrap();
let Asset::Fungible(fa) = payback.assets().iter().next().unwrap() else {
panic!("expected fungible payback asset");
};
assert_eq!(fa.callbacks(), AssetCallbackFlag::Enabled);
let remainder = remainder.expect("partial fill should produce remainder");
assert_eq!(
remainder.offered_asset().callbacks(),
AssetCallbackFlag::Enabled,
"remainder offered asset must inherit callbacks",
);
assert_eq!(
remainder.storage().requested_asset().callbacks(),
AssetCallbackFlag::Enabled,
"remainder storage's requested asset must inherit callbacks",
);
let payback_attachment =
PswapNoteAttachment::new(AssetAmount::new(20).unwrap(), pswap.order_id(), 1);
let reconstructed_payback = pswap.payback_note(consumer_id, &payback_attachment).unwrap();
let Asset::Fungible(fa) = reconstructed_payback.assets().iter().next().unwrap() else {
panic!("expected fungible payback asset");
};
assert_eq!(
fa.callbacks(),
AssetCallbackFlag::Enabled,
"payback_note must preserve requested asset's callback flag",
);
let remainder_attachment =
PswapNoteAttachment::new(AssetAmount::new(40).unwrap(), pswap.order_id(), 1);
let reconstructed_remainder = pswap
.remainder_note(
consumer_id,
&remainder_attachment,
AssetAmount::new(60).unwrap(),
AssetAmount::new(30).unwrap(),
)
.unwrap();
let Asset::Fungible(fa) = reconstructed_remainder.assets().iter().next().unwrap() else {
panic!("expected fungible remainder asset");
};
assert_eq!(
fa.callbacks(),
AssetCallbackFlag::Enabled,
"remainder_note must preserve offered asset's callback flag",
);
}
}