use alloy::primitives::{Address, U256};
use crate::protocol::traits::VerificationError;
use super::super::transfers::Transfer;
use super::receipt_logs::ReceiptSenderPolicy;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct TransferEffect {
pub(super) token: Address,
pub(super) from: Option<Address>,
pub(super) to: Address,
pub(super) amount: U256,
pub(super) memo: Option<[u8; 32]>,
}
pub(super) enum TransferSource<'a> {
Calls,
ReceiptLogs(ReceiptSenderPolicy<'a>),
}
impl TransferSource<'_> {
fn allows_sender(&self, transfer: &TransferEffect) -> bool {
match self {
Self::Calls => true,
Self::ReceiptLogs(policy) => transfer.from.is_some_and(|from| policy.allows(from)),
}
}
}
pub(super) fn match_transfers(
transfers: &[TransferEffect],
source: TransferSource<'_>,
currency: Address,
expected: &[Transfer],
) -> Result<Vec<Option<[u8; 32]>>, VerificationError> {
let mut sorted_expected: Vec<&Transfer> = expected.iter().collect();
sorted_expected.sort_by_key(|t| if t.memo.is_some() { 0 } else { 1 });
let mut used = vec![false; transfers.len()];
let mut matched_memos = Vec::with_capacity(expected.len());
for expected in sorted_expected {
if expected.amount.is_zero() {
return Err(VerificationError::new(
"Invalid amount: expected_amount must be greater than zero".to_string(),
));
}
if expected.recipient.is_zero() {
return Err(VerificationError::new(
"Invalid recipient: expected_recipient cannot be the zero address".to_string(),
));
}
let find = |with_memo: Option<bool>| {
transfers.iter().enumerate().position(|(index, transfer)| {
!used[index]
&& transfer.token == currency
&& transfer.to == expected.recipient
&& transfer.amount == expected.amount
&& match expected.memo {
Some(memo) => transfer.memo == Some(memo),
None => with_memo.is_none_or(|with_memo| transfer.memo.is_some() == with_memo),
}
&& source.allows_sender(transfer)
})
};
let index = match source {
TransferSource::ReceiptLogs(_) if expected.memo.is_none() => {
find(Some(true)).or_else(|| find(Some(false)))
}
_ => find(None),
};
let Some(index) = index else {
let memo = if expected.memo.is_some() {
" with memo"
} else {
""
};
return Err(VerificationError::new(match source {
TransferSource::Calls => format!(
"Invalid transaction: no matching transfer call found for {} to {}{memo}",
expected.amount, expected.recipient
),
TransferSource::ReceiptLogs(_) => format!(
"No matching transfer event found for {} to {}{memo}",
expected.amount, expected.recipient
),
}));
};
used[index] = true;
matched_memos.push(transfers[index].memo);
}
Ok(matched_memos)
}