cow_settlement/
refunds.rs1use std::fmt;
8
9use alloy_primitives::{U256, keccak256};
10use cow_errors::CowError;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct OrderRefund {
27 pub order_uid: String,
29 pub refund_type: RefundType,
31}
32
33impl OrderRefund {
34 #[must_use]
45 pub const fn new(order_uid: String, refund_type: RefundType) -> Self {
46 Self { order_uid, refund_type }
47 }
48}
49
50impl fmt::Display for OrderRefund {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 write!(f, "Refund({}, {:?})", self.order_uid, self.refund_type)
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum RefundType {
59 Settlement,
61 EthFlow,
63}
64
65impl RefundType {
66 #[must_use]
72 pub const fn is_settlement(&self) -> bool {
73 matches!(self, Self::Settlement)
74 }
75
76 #[must_use]
82 pub const fn is_eth_flow(&self) -> bool {
83 matches!(self, Self::EthFlow)
84 }
85}
86
87impl fmt::Display for RefundType {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 match self {
90 Self::Settlement => write!(f, "Settlement"),
91 Self::EthFlow => write!(f, "EthFlow"),
92 }
93 }
94}
95
96pub fn settlement_refund_calldata(order_uid: &str) -> Result<Vec<u8>, CowError> {
124 let uid_bytes = decode_uid(order_uid)?;
125 let sel = selector("freeFilledAmountStorage(bytes)");
126
127 let padded_len = padded32(uid_bytes.len());
128 let mut buf = Vec::with_capacity(4 + 32 + 32 + padded_len);
129 buf.extend_from_slice(&sel);
130 buf.extend_from_slice(&u256_be(32));
132 buf.extend_from_slice(&u256_be(uid_bytes.len() as u64));
134 buf.extend_from_slice(&uid_bytes);
136 pad_to(&mut buf, uid_bytes.len());
138 Ok(buf)
139}
140
141pub fn ethflow_refund_calldata(order_uid: &str) -> Result<Vec<u8>, CowError> {
168 let uid_bytes = decode_uid(order_uid)?;
169 let sel = selector("invalidateOrder(bytes)");
170
171 let padded_len = padded32(uid_bytes.len());
172 let mut buf = Vec::with_capacity(4 + 32 + 32 + padded_len);
173 buf.extend_from_slice(&sel);
174 buf.extend_from_slice(&u256_be(32));
176 buf.extend_from_slice(&u256_be(uid_bytes.len() as u64));
178 buf.extend_from_slice(&uid_bytes);
180 pad_to(&mut buf, uid_bytes.len());
182 Ok(buf)
183}
184
185#[must_use]
210pub fn is_refundable(filled_amount: U256, total_amount: U256) -> bool {
211 filled_amount < total_amount
212}
213
214#[must_use]
238pub const fn refund_amount(filled_amount: U256, total_amount: U256) -> U256 {
239 total_amount.saturating_sub(filled_amount)
240}
241
242fn selector(sig: &str) -> [u8; 4] {
246 let h = keccak256(sig.as_bytes());
247 [h[0], h[1], h[2], h[3]]
248}
249
250fn u256_be(v: u64) -> [u8; 32] {
252 let mut out = [0u8; 32];
253 out[24..].copy_from_slice(&v.to_be_bytes());
254 out
255}
256
257fn pad_to(buf: &mut Vec<u8>, written: usize) {
259 let rem = written % 32;
260 if rem != 0 {
261 buf.resize(buf.len() + (32 - rem), 0);
262 }
263}
264
265const fn padded32(n: usize) -> usize {
267 if n.is_multiple_of(32) { n } else { n + (32 - n % 32) }
268}
269
270fn decode_uid(uid: &str) -> Result<Vec<u8>, CowError> {
272 let stripped = uid.trim_start_matches("0x");
273 alloy_primitives::hex::decode(stripped)
274 .map_err(|_e| CowError::Api { status: 0, body: format!("invalid orderUid: {uid}") })
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 fn dummy_uid_56() -> String {
282 "0x".to_owned() + &"ab".repeat(56)
283 }
284
285 #[test]
288 fn order_refund_new() {
289 let refund = OrderRefund::new("0xdead".to_owned(), RefundType::Settlement);
290 assert_eq!(refund.order_uid, "0xdead");
291 assert_eq!(refund.refund_type, RefundType::Settlement);
292 }
293
294 #[test]
295 fn order_refund_display() {
296 let refund = OrderRefund::new("0xbeef".to_owned(), RefundType::EthFlow);
297 let s = format!("{refund}");
298 assert!(s.contains("0xbeef"));
299 assert!(s.contains("EthFlow"));
300 }
301
302 #[test]
303 fn order_refund_clone_eq() {
304 let a = OrderRefund::new("0xaa".to_owned(), RefundType::Settlement);
305 let b = a.clone();
306 assert_eq!(a, b);
307 }
308
309 #[test]
312 fn refund_type_is_settlement() {
313 assert!(RefundType::Settlement.is_settlement());
314 assert!(!RefundType::Settlement.is_eth_flow());
315 }
316
317 #[test]
318 fn refund_type_is_eth_flow() {
319 assert!(RefundType::EthFlow.is_eth_flow());
320 assert!(!RefundType::EthFlow.is_settlement());
321 }
322
323 #[test]
324 fn refund_type_display() {
325 assert_eq!(format!("{}", RefundType::Settlement), "Settlement");
326 assert_eq!(format!("{}", RefundType::EthFlow), "EthFlow");
327 }
328
329 #[test]
330 fn refund_type_copy() {
331 let a = RefundType::Settlement;
332 let b = a;
333 assert_eq!(a, b);
334 }
335
336 #[test]
339 fn settlement_refund_calldata_valid() {
340 let uid = dummy_uid_56();
341 let data = settlement_refund_calldata(&uid).unwrap();
342 assert_eq!(data.len(), 132);
344 assert_eq!(&data[..4], &selector("freeFilledAmountStorage(bytes)"));
345 }
346
347 #[test]
348 fn settlement_refund_calldata_invalid_hex() {
349 assert!(settlement_refund_calldata("0xZZZZ").is_err());
350 }
351
352 #[test]
353 fn settlement_refund_calldata_without_prefix() {
354 let uid = "ab".repeat(56);
355 let data = settlement_refund_calldata(&uid).unwrap();
356 assert_eq!(data.len(), 132);
357 }
358
359 #[test]
360 fn settlement_refund_calldata_empty_uid() {
361 let data = settlement_refund_calldata("0x").unwrap();
362 assert_eq!(data.len(), 68);
364 }
365
366 #[test]
369 fn ethflow_refund_calldata_valid() {
370 let uid = dummy_uid_56();
371 let data = ethflow_refund_calldata(&uid).unwrap();
372 assert_eq!(data.len(), 132);
373 assert_eq!(&data[..4], &selector("invalidateOrder(bytes)"));
374 }
375
376 #[test]
377 fn ethflow_refund_calldata_invalid_hex() {
378 assert!(ethflow_refund_calldata("not_hex_gg").is_err());
379 }
380
381 #[test]
382 fn ethflow_refund_calldata_without_prefix() {
383 let uid = "cd".repeat(56);
384 let data = ethflow_refund_calldata(&uid).unwrap();
385 assert_eq!(data.len(), 132);
386 }
387
388 #[test]
391 fn is_refundable_zero_filled() {
392 assert!(is_refundable(U256::ZERO, U256::from(1000)));
393 }
394
395 #[test]
396 fn is_refundable_partial_filled() {
397 assert!(is_refundable(U256::from(500), U256::from(1000)));
398 }
399
400 #[test]
401 fn is_refundable_fully_filled() {
402 assert!(!is_refundable(U256::from(1000), U256::from(1000)));
403 }
404
405 #[test]
406 fn is_refundable_zero_total() {
407 assert!(!is_refundable(U256::ZERO, U256::ZERO));
408 }
409
410 #[test]
413 fn refund_amount_partial() {
414 assert_eq!(refund_amount(U256::from(300), U256::from(1000)), U256::from(700));
415 }
416
417 #[test]
418 fn refund_amount_fully_filled() {
419 assert_eq!(refund_amount(U256::from(1000), U256::from(1000)), U256::ZERO);
420 }
421
422 #[test]
423 fn refund_amount_zero_filled() {
424 assert_eq!(refund_amount(U256::ZERO, U256::from(500)), U256::from(500));
425 }
426
427 #[test]
428 fn refund_amount_overfilled_saturates() {
429 assert_eq!(refund_amount(U256::from(2000), U256::from(1000)), U256::ZERO);
431 }
432
433 #[test]
436 fn padded32_rounds_up() {
437 assert_eq!(padded32(0), 0);
438 assert_eq!(padded32(1), 32);
439 assert_eq!(padded32(31), 32);
440 assert_eq!(padded32(32), 32);
441 assert_eq!(padded32(33), 64);
442 }
443}