use alloy::primitives::{Address, U256};
use super::charge::{parse_memo_bytes_checked, parse_split_memo_bytes, TempoChargeExt};
use super::types::Split;
use crate::error::MppError;
use crate::evm::{parse_address, parse_amount};
use crate::protocol::intents::ChargeRequest;
pub const MAX_SPLITS: usize = 10;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Transfer {
pub amount: U256,
pub recipient: Address,
pub memo: Option<[u8; 32]>,
}
pub fn get_request_transfers(charge: &ChargeRequest) -> Result<Vec<Transfer>, MppError> {
let recipient = charge.recipient_address()?;
let amount = charge.amount_u256()?;
let details = charge.tempo_method_details()?;
let memo = parse_memo_bytes_checked(details.memo.as_deref())?;
get_transfers(amount, recipient, memo, details.splits.as_deref())
}
pub fn get_transfers(
total_amount: U256,
primary_recipient: Address,
primary_memo: Option<[u8; 32]>,
splits: Option<&[Split]>,
) -> Result<Vec<Transfer>, MppError> {
let splits = match splits {
Some([]) => {
return Err(MppError::invalid_challenge_reason(
"Splits must not be empty".to_string(),
));
}
Some(s) => s,
None => {
return Ok(vec![Transfer {
amount: total_amount,
recipient: primary_recipient,
memo: primary_memo,
}]);
}
};
if splits.len() > MAX_SPLITS {
return Err(MppError::invalid_challenge_reason(format!(
"Too many splits: {} (max {})",
splits.len(),
MAX_SPLITS
)));
}
let mut split_sum = U256::ZERO;
let mut split_transfers = Vec::with_capacity(splits.len());
for split in splits {
let amount = parse_amount(&split.amount).map_err(|_| {
MppError::invalid_challenge_reason(format!("Invalid split amount: {}", split.amount))
})?;
if amount.is_zero() {
return Err(MppError::invalid_challenge_reason(
"Split amount must be greater than zero".to_string(),
));
}
let recipient = parse_address(&split.recipient)?;
let memo = parse_split_memo_bytes(split.memo.as_deref())?;
split_sum = split_sum.checked_add(amount).ok_or_else(|| {
MppError::invalid_challenge_reason("Split amounts overflow".to_string())
})?;
split_transfers.push(Transfer {
amount,
recipient,
memo,
});
}
if split_sum >= total_amount {
return Err(MppError::invalid_challenge_reason(format!(
"Sum of splits ({}) must be less than total amount ({})",
split_sum, total_amount
)));
}
let primary_amount = total_amount - split_sum;
let mut transfers = Vec::with_capacity(1 + split_transfers.len());
transfers.push(Transfer {
amount: primary_amount,
recipient: primary_recipient,
memo: primary_memo,
});
transfers.extend(split_transfers);
Ok(transfers)
}
#[cfg(test)]
mod tests {
use super::*;
fn addr(byte: u8) -> Address {
Address::repeat_byte(byte)
}
#[test]
fn test_no_splits_returns_single_transfer() {
let transfers = get_transfers(U256::from(1_000_000u64), addr(0x01), None, None).unwrap();
assert_eq!(transfers.len(), 1);
assert_eq!(transfers[0].amount, U256::from(1_000_000u64));
assert_eq!(transfers[0].recipient, addr(0x01));
assert!(transfers[0].memo.is_none());
}
#[test]
fn test_empty_splits_rejected() {
let error =
get_transfers(U256::from(1_000_000u64), addr(0x01), None, Some(&[])).unwrap_err();
assert!(error.to_string().contains("Splits must not be empty"));
}
#[test]
fn test_single_split() {
let splits = vec![Split {
amount: "300000".to_string(),
recipient: format!("{:#x}", addr(0x02)),
memo: None,
}];
let transfers =
get_transfers(U256::from(1_000_000u64), addr(0x01), None, Some(&splits)).unwrap();
assert_eq!(transfers.len(), 2);
assert_eq!(transfers[0].amount, U256::from(700_000u64));
assert_eq!(transfers[0].recipient, addr(0x01));
assert_eq!(transfers[1].amount, U256::from(300_000u64));
assert_eq!(transfers[1].recipient, addr(0x02));
}
#[test]
fn test_primary_inherits_memo() {
let memo = [0xABu8; 32];
let splits = vec![Split {
amount: "100000".to_string(),
recipient: format!("{:#x}", addr(0x02)),
memo: None,
}];
let transfers = get_transfers(
U256::from(1_000_000u64),
addr(0x01),
Some(memo),
Some(&splits),
)
.unwrap();
assert_eq!(transfers[0].memo, Some(memo));
assert!(transfers[1].memo.is_none());
}
#[test]
fn test_split_with_memo() {
let split_memo_hex =
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string();
let splits = vec![Split {
amount: "100000".to_string(),
recipient: format!("{:#x}", addr(0x02)),
memo: Some(split_memo_hex),
}];
let transfers =
get_transfers(U256::from(1_000_000u64), addr(0x01), None, Some(&splits)).unwrap();
assert!(transfers[1].memo.is_some());
assert_eq!(transfers[1].memo.unwrap()[0], 0x12);
}
#[test]
fn test_rejects_invalid_split_memo() {
let splits = vec![Split {
amount: "100000".to_string(),
recipient: format!("{:#x}", addr(0x02)),
memo: Some("0x1234".to_string()),
}];
let error =
get_transfers(U256::from(1_000_000u64), addr(0x01), None, Some(&splits)).unwrap_err();
assert!(error.to_string().contains("Invalid split memo"));
}
#[test]
fn test_multiple_splits_preserve_order() {
let splits = vec![
Split {
amount: "100000".to_string(),
recipient: format!("{:#x}", addr(0x02)),
memo: None,
},
Split {
amount: "200000".to_string(),
recipient: format!("{:#x}", addr(0x03)),
memo: None,
},
Split {
amount: "50000".to_string(),
recipient: format!("{:#x}", addr(0x04)),
memo: None,
},
];
let transfers =
get_transfers(U256::from(1_000_000u64), addr(0x01), None, Some(&splits)).unwrap();
assert_eq!(transfers.len(), 4);
assert_eq!(transfers[0].amount, U256::from(650_000u64)); assert_eq!(transfers[1].amount, U256::from(100_000u64));
assert_eq!(transfers[1].recipient, addr(0x02));
assert_eq!(transfers[2].amount, U256::from(200_000u64));
assert_eq!(transfers[2].recipient, addr(0x03));
assert_eq!(transfers[3].amount, U256::from(50_000u64));
assert_eq!(transfers[3].recipient, addr(0x04));
}
#[test]
fn test_rejects_sum_equals_total() {
let splits = vec![Split {
amount: "1000000".to_string(),
recipient: format!("{:#x}", addr(0x02)),
memo: None,
}];
let result = get_transfers(U256::from(1_000_000u64), addr(0x01), None, Some(&splits));
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("must be less than"));
}
#[test]
fn test_rejects_sum_exceeds_total() {
let splits = vec![Split {
amount: "1500000".to_string(),
recipient: format!("{:#x}", addr(0x02)),
memo: None,
}];
let result = get_transfers(U256::from(1_000_000u64), addr(0x01), None, Some(&splits));
assert!(result.is_err());
}
#[test]
fn test_rejects_zero_split_amount() {
let splits = vec![Split {
amount: "0".to_string(),
recipient: format!("{:#x}", addr(0x02)),
memo: None,
}];
let result = get_transfers(U256::from(1_000_000u64), addr(0x01), None, Some(&splits));
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("greater than zero"));
}
#[test]
fn test_rejects_too_many_splits() {
let splits: Vec<Split> = (0..11)
.map(|i| Split {
amount: "1000".to_string(),
recipient: format!("{:#x}", addr(i as u8 + 2)),
memo: None,
})
.collect();
let result = get_transfers(U256::from(1_000_000u64), addr(0x01), None, Some(&splits));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Too many splits"));
}
#[test]
fn test_max_splits_allowed() {
let splits: Vec<Split> = (0..10)
.map(|i| Split {
amount: "1000".to_string(),
recipient: format!("{:#x}", addr(i as u8 + 2)),
memo: None,
})
.collect();
let transfers =
get_transfers(U256::from(1_000_000u64), addr(0x01), None, Some(&splits)).unwrap();
assert_eq!(transfers.len(), 11);
assert_eq!(transfers[0].amount, U256::from(990_000u64));
}
#[test]
fn test_get_request_transfers_rejects_invalid_top_level_memo() {
let request = ChargeRequest {
amount: "1000000".to_string(),
currency: format!("{:#x}", addr(0x20)),
recipient: Some(format!("{:#x}", addr(0x01))),
method_details: Some(serde_json::json!({
"memo": "0x1234",
})),
..Default::default()
};
let error = get_request_transfers(&request).unwrap_err();
assert!(error.to_string().contains("Invalid memo"));
}
#[test]
fn test_get_request_transfers_rejects_empty_splits() {
let request = ChargeRequest {
amount: "1000000".to_string(),
currency: format!("{:#x}", addr(0x20)),
recipient: Some(format!("{:#x}", addr(0x01))),
method_details: Some(serde_json::json!({
"splits": [],
})),
..Default::default()
};
let error = get_request_transfers(&request).unwrap_err();
assert!(error.to_string().contains("Splits must not be empty"));
}
}