Skip to main content

cow_orderbook/
quote_amounts.rs

1//! Quote amount calculations: fee breakdown, slippage, and cost stages.
2//!
3//! Ports the `quoteAmountsAndCosts` package from the `TypeScript` SDK.
4//! All arithmetic uses [`U256`] to avoid overflow on token amounts.
5//!
6//! # Fee stages
7//!
8//! The `/quote` API returns amounts where protocol fee and network costs are
9//! already baked in. This module reconstructs every intermediate stage:
10//!
11//! 1. **Before all fees** — spot-price amounts.
12//! 2. **After protocol fees** (= before network costs).
13//! 3. **After network costs**.
14//! 4. **After partner fees**.
15//! 5. **After slippage** — the minimum the user signs for.
16//!
17//! # Key functions
18//!
19//! | Function | Purpose |
20//! |---|---|
21//! | [`get_quote_amounts_and_costs`] | Full breakdown from `/quote` response |
22//! | [`get_protocol_fee_amount`] | Reverse-engineer the protocol fee |
23//! | [`get_quote_amounts_after_partner_fee`] | Apply partner fee to amounts |
24//! | [`get_quote_amounts_after_slippage`] | Apply slippage tolerance |
25//! | [`transform_order`] | Enrich an `Order` with `total_fee` and EthFlow fields |
26
27use alloy_primitives::U256;
28use cow_primitives::{HUNDRED_THOUSANDS, ONE_HUNDRED_BPS};
29use cow_types::OrderKind;
30
31// ── Core types ───────────────────────────────────────────────────────────────
32
33/// Sell and buy amounts at a particular fee stage.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct QuoteAmounts {
36    /// Amount of the sell token (in atoms).
37    pub sell_amount: U256,
38    /// Amount of the buy token (in atoms).
39    pub buy_amount: U256,
40}
41
42/// Network fee expressed in both sell and buy currency.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct QuoteNetworkFee {
45    /// Network fee denominated in the sell token.
46    pub amount_in_sell_currency: U256,
47    /// Network fee denominated in the buy token.
48    pub amount_in_buy_currency: U256,
49}
50
51/// A single fee component: absolute amount and its BPS rate.
52#[derive(Debug, Clone, Copy, PartialEq)]
53pub struct QuoteFeeComponent {
54    /// Absolute fee amount (in the surplus token's atoms).
55    pub amount: U256,
56    /// Fee rate in basis points.
57    pub bps: f64,
58}
59
60/// All cost components of a quote.
61#[derive(Debug, Clone, Copy, PartialEq)]
62pub struct QuoteCosts {
63    /// Gas / network fee covering on-chain settlement.
64    pub network_fee: QuoteNetworkFee,
65    /// Partner fee (revenue share).
66    pub partner_fee: QuoteFeeComponent,
67    /// Protocol fee collected by `CoW` Protocol.
68    pub protocol_fee: QuoteFeeComponent,
69}
70
71/// Complete breakdown of quote amounts at every fee stage.
72///
73/// See the `TypeScript` SDK's `QuoteAmountsAndCosts` for the canonical
74/// description of each stage and how fees are layered.
75#[derive(Debug, Clone, Copy, PartialEq)]
76pub struct QuoteAmountsAndCostsResult {
77    /// Whether the underlying order is a sell order.
78    pub is_sell: bool,
79    /// Cost breakdown.
80    pub costs: QuoteCosts,
81    /// Spot-price amounts before any fees.
82    pub before_all_fees: QuoteAmounts,
83    /// Amounts before network costs (same as `after_protocol_fees`).
84    pub before_network_costs: QuoteAmounts,
85    /// Amounts after protocol fees have been applied.
86    pub after_protocol_fees: QuoteAmounts,
87    /// Amounts after network costs have been applied.
88    pub after_network_costs: QuoteAmounts,
89    /// Amounts after partner fees have been applied.
90    pub after_partner_fees: QuoteAmounts,
91    /// Amounts after slippage tolerance has been applied.
92    pub after_slippage: QuoteAmounts,
93    /// Final amounts to sign in the order.
94    pub amounts_to_sign: QuoteAmounts,
95}
96
97// ── Input types ──────────────────────────────────────────────────────────────
98
99/// Order parameters needed by the quote-amount calculations.
100///
101/// These map to the fields returned by `POST /api/v1/quote`.
102#[derive(Debug, Clone)]
103pub struct QuoteOrderParams {
104    /// Sell or buy.
105    pub kind: OrderKind,
106    /// Sell amount (after network costs for sell orders).
107    pub sell_amount: U256,
108    /// Buy amount (minimum for sell orders, exact for buy orders).
109    pub buy_amount: U256,
110    /// Network fee / gas cost amount.
111    pub fee_amount: U256,
112}
113
114/// Parameters for [`get_quote_amounts_and_costs`].
115#[derive(Debug, Clone)]
116pub struct QuoteAmountsAndCostsParams {
117    /// The order parameters from the `/quote` response.
118    pub order_params: QuoteOrderParams,
119    /// Protocol fee in BPS (may be fractional, e.g. `0.003`). `None` or `0` means no protocol fee.
120    pub protocol_fee_bps: Option<f64>,
121    /// Partner fee in BPS. `None` or `0` means no partner fee.
122    pub partner_fee_bps: Option<u32>,
123    /// Slippage tolerance in BPS.
124    pub slippage_percent_bps: u32,
125}
126
127/// Parameters for [`get_protocol_fee_amount`].
128#[derive(Debug, Clone)]
129pub struct ProtocolFeeAmountParams {
130    /// The order parameters from the `/quote` response.
131    pub order_params: QuoteOrderParams,
132    /// Protocol fee in BPS (may be fractional).
133    pub protocol_fee_bps: f64,
134}
135
136// ── Protocol fee ─────────────────────────────────────────────────────────────
137
138/// Derive the absolute protocol-fee amount from the quote response.
139///
140/// The `/quote` API returns amounts where the protocol fee is already
141/// baked in. This function reverses the fee to recover the absolute amount.
142///
143/// # Parameters
144///
145/// * `params` — a [`ProtocolFeeAmountParams`] containing the order parameters and the protocol fee
146///   rate in BPS.
147///
148/// For **sell orders** the fee was deducted from `buyAmount`:
149/// ```text
150/// protocolFee = buyAmount * bps / (ONE_HUNDRED_BPS * scale - bps_scaled)
151/// ```
152///
153/// For **buy orders** the fee was added to `sellAmount`:
154/// ```text
155/// protocolFee = (sellAmount + feeAmount) * bps / (ONE_HUNDRED_BPS * scale + bps_scaled)
156/// ```
157#[must_use]
158pub fn get_protocol_fee_amount(params: &ProtocolFeeAmountParams) -> U256 {
159    let ProtocolFeeAmountParams { order_params, protocol_fee_bps } = params;
160
161    if *protocol_fee_bps <= 0.0 {
162        return U256::ZERO;
163    }
164
165    let is_sell = order_params.kind.is_sell();
166    let sell_amount = order_params.sell_amount;
167    let buy_amount = order_params.buy_amount;
168    let fee_amount = order_params.fee_amount;
169
170    let protocol_fee_scale = U256::from(HUNDRED_THOUSANDS);
171    // Keep 5 decimal places of BPS precision while staying in integer domain.
172    let protocol_fee_bps_scaled = (*protocol_fee_bps * HUNDRED_THOUSANDS as f64).round() as u64;
173    let protocol_fee_bps_big = U256::from(protocol_fee_bps_scaled);
174
175    if protocol_fee_bps_big.is_zero() {
176        return U256::ZERO;
177    }
178
179    let one_hundred_bps = U256::from(ONE_HUNDRED_BPS);
180
181    if is_sell {
182        // SELL: protocolFee = buyAmount * bps / (ONE_HUNDRED_BPS * scale - bps)
183        let denominator = one_hundred_bps * protocol_fee_scale - protocol_fee_bps_big;
184        buy_amount * protocol_fee_bps_big / denominator
185    } else {
186        // BUY: protocolFee = (sellAmount + feeAmount) * bps / (ONE_HUNDRED_BPS * scale + bps)
187        let denominator = one_hundred_bps * protocol_fee_scale + protocol_fee_bps_big;
188        (sell_amount + fee_amount) * protocol_fee_bps_big / denominator
189    }
190}
191
192// ── Partner fee ──────────────────────────────────────────────────────────────
193
194/// Result of partner-fee calculation.
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub struct PartnerFeeResult {
197    /// Absolute partner-fee amount.
198    pub partner_fee_amount: U256,
199    /// Amounts after the partner fee has been applied.
200    pub after_partner_fees: QuoteAmounts,
201}
202
203/// Calculate the partner-fee amount and adjust quote amounts accordingly.
204///
205/// The partner fee is computed relative to spot-price amounts
206/// (`before_all_fees`) so it reflects the full trade volume, not volume
207/// already reduced by protocol fee.
208///
209/// - **Sell orders**: partner fee is subtracted from `buyAmount`.
210/// - **Buy orders**: partner fee is added to `sellAmount`.
211///
212/// # Parameters
213///
214/// * `after_network_costs` — amounts after network costs have been applied.
215/// * `before_all_fees` — spot-price amounts (used as the fee base).
216/// * `is_sell` — `true` for sell orders, `false` for buy orders.
217/// * `partner_fee_bps` — partner fee rate in basis points.
218///
219/// # Returns
220///
221/// A [`PartnerFeeResult`] with the absolute fee amount and adjusted
222/// amounts.
223#[must_use]
224pub fn get_quote_amounts_after_partner_fee(
225    after_network_costs: &QuoteAmounts,
226    before_all_fees: &QuoteAmounts,
227    is_sell: bool,
228    partner_fee_bps: u32,
229) -> PartnerFeeResult {
230    let one_hundred_bps = U256::from(ONE_HUNDRED_BPS);
231
232    // Partner fee is based on spot-price amounts (before all fees).
233    let surplus_amount =
234        if is_sell { before_all_fees.buy_amount } else { before_all_fees.sell_amount };
235    let partner_fee_amount = if partner_fee_bps > 0 {
236        surplus_amount * U256::from(partner_fee_bps) / one_hundred_bps
237    } else {
238        U256::ZERO
239    };
240
241    let after_partner_fees = if is_sell {
242        QuoteAmounts {
243            sell_amount: after_network_costs.sell_amount,
244            buy_amount: after_network_costs.buy_amount - partner_fee_amount,
245        }
246    } else {
247        QuoteAmounts {
248            sell_amount: after_network_costs.sell_amount + partner_fee_amount,
249            buy_amount: after_network_costs.buy_amount,
250        }
251    };
252
253    PartnerFeeResult { partner_fee_amount, after_partner_fees }
254}
255
256// ── Slippage ─────────────────────────────────────────────────────────────────
257
258/// Apply slippage tolerance to amounts after all fees.
259///
260/// Slippage protects the user from price movements between quoting and
261/// execution. It is always applied to the surplus (non-fixed) side:
262///
263/// - **Sell orders**: slippage reduces `buyAmount` (user accepts less).
264/// - **Buy orders**: slippage increases `sellAmount` (user accepts more).
265///
266/// # Parameters
267///
268/// * `after_partner_fees` — amounts after partner fees have been applied.
269/// * `is_sell` — `true` for sell orders, `false` for buy orders.
270/// * `slippage_bps` — slippage tolerance in basis points.
271///
272/// # Returns
273///
274/// A [`QuoteAmounts`] with slippage applied to the surplus side.
275#[must_use]
276pub fn get_quote_amounts_after_slippage(
277    after_partner_fees: &QuoteAmounts,
278    is_sell: bool,
279    slippage_bps: u32,
280) -> QuoteAmounts {
281    let one_hundred_bps = U256::from(ONE_HUNDRED_BPS);
282    let slippage_bps_big = U256::from(slippage_bps);
283
284    let slippage = |amount: U256| -> U256 { amount * slippage_bps_big / one_hundred_bps };
285
286    if is_sell {
287        QuoteAmounts {
288            sell_amount: after_partner_fees.sell_amount,
289            buy_amount: after_partner_fees.buy_amount - slippage(after_partner_fees.buy_amount),
290        }
291    } else {
292        QuoteAmounts {
293            sell_amount: after_partner_fees.sell_amount + slippage(after_partner_fees.sell_amount),
294            buy_amount: after_partner_fees.buy_amount,
295        }
296    }
297}
298
299// ── Main entry point ─────────────────────────────────────────────────────────
300
301/// Calculate all quote-amount stages and costs from a `/quote` API
302/// response.
303///
304/// Takes the raw order parameters (where protocol fee and network costs
305/// are already baked in) and reconstructs every intermediate amount stage:
306///
307/// 1. **Before all fees** — spot-price amounts.
308/// 2. **After protocol fees** (= before network costs).
309/// 3. **After network costs**.
310/// 4. **After partner fees**.
311/// 5. **After slippage** — the minimum the user signs for.
312///
313/// The returned [`QuoteAmountsAndCostsResult`] includes `amounts_to_sign`,
314/// the final sell/buy amounts that should be placed in the signed order.
315///
316/// # Parameters
317///
318/// * `params` — a [`QuoteAmountsAndCostsParams`] containing the order parameters from the `/quote`
319///   response, protocol/partner fee rates, and slippage tolerance.
320///
321/// # Returns
322///
323/// A [`QuoteAmountsAndCostsResult`] with amounts at every fee stage and
324/// the complete cost breakdown.
325#[must_use]
326pub fn get_quote_amounts_and_costs(
327    params: &QuoteAmountsAndCostsParams,
328) -> QuoteAmountsAndCostsResult {
329    let QuoteAmountsAndCostsParams {
330        order_params,
331        protocol_fee_bps,
332        partner_fee_bps,
333        slippage_percent_bps,
334    } = params;
335
336    let partner_fee = partner_fee_bps.map_or(0, |v| v);
337    let protocol_fee = protocol_fee_bps.map_or(0.0, |v| v);
338    let is_sell = order_params.kind.is_sell();
339
340    let sell_amount = order_params.sell_amount;
341    let buy_amount = order_params.buy_amount;
342    let network_cost_amount = order_params.fee_amount;
343
344    // Avoid division by zero when sell_amount is 0.
345    let network_cost_in_buy = if sell_amount.is_zero() {
346        U256::ZERO
347    } else {
348        buy_amount * network_cost_amount / sell_amount
349    };
350
351    // Reconstruct the protocol fee amount from the baked-in quote.
352    let protocol_fee_amount = get_protocol_fee_amount(&ProtocolFeeAmountParams {
353        order_params: order_params.clone(),
354        protocol_fee_bps: protocol_fee,
355    });
356
357    // Stage 0: before all fees (spot price).
358    let before_all_fees = if is_sell {
359        QuoteAmounts {
360            sell_amount: sell_amount + network_cost_amount,
361            buy_amount: buy_amount + network_cost_in_buy + protocol_fee_amount,
362        }
363    } else {
364        QuoteAmounts { sell_amount: sell_amount - protocol_fee_amount, buy_amount }
365    };
366
367    // Stage 1: after protocol fees (= before network costs).
368    let after_protocol_fees = if is_sell {
369        QuoteAmounts {
370            sell_amount: before_all_fees.sell_amount,
371            buy_amount: before_all_fees.buy_amount - protocol_fee_amount,
372        }
373    } else {
374        QuoteAmounts { sell_amount, buy_amount: before_all_fees.buy_amount }
375    };
376
377    // Stage 2: after network costs.
378    let after_network_costs = if is_sell {
379        QuoteAmounts { sell_amount, buy_amount }
380    } else {
381        QuoteAmounts {
382            sell_amount: sell_amount + network_cost_amount,
383            buy_amount: after_protocol_fees.buy_amount,
384        }
385    };
386
387    // Stage 3: after partner fees.
388    let PartnerFeeResult { partner_fee_amount, after_partner_fees } =
389        get_quote_amounts_after_partner_fee(
390            &after_network_costs,
391            &before_all_fees,
392            is_sell,
393            partner_fee,
394        );
395
396    // Stage 4: after slippage.
397    let after_slippage =
398        get_quote_amounts_after_slippage(&after_partner_fees, is_sell, *slippage_percent_bps);
399
400    // Final amounts to sign.
401    let amounts_to_sign = if is_sell {
402        QuoteAmounts {
403            sell_amount: before_all_fees.sell_amount,
404            buy_amount: after_slippage.buy_amount,
405        }
406    } else {
407        QuoteAmounts {
408            sell_amount: after_slippage.sell_amount,
409            buy_amount: before_all_fees.buy_amount,
410        }
411    };
412
413    QuoteAmountsAndCostsResult {
414        is_sell,
415        costs: QuoteCosts {
416            network_fee: QuoteNetworkFee {
417                amount_in_sell_currency: network_cost_amount,
418                amount_in_buy_currency: network_cost_in_buy,
419            },
420            partner_fee: QuoteFeeComponent { amount: partner_fee_amount, bps: partner_fee as f64 },
421            protocol_fee: QuoteFeeComponent { amount: protocol_fee_amount, bps: protocol_fee },
422        },
423        before_all_fees,
424        before_network_costs: after_protocol_fees,
425        after_protocol_fees,
426        after_network_costs,
427        after_partner_fees,
428        after_slippage,
429        amounts_to_sign,
430    }
431}
432
433// ── Transform order ──────────────────────────────────────────────────────────
434
435/// Enrich an [`Order`](super::Order) by computing its `total_fee` field.
436///
437/// The total fee is the sum of `executed_fee_amount` and `executed_fee`
438/// (both may be zero or non-zero independently). After computing the total
439/// fee, `EthFlow` transformations are applied if applicable:
440///
441/// - `valid_to` is overwritten with the user's original validity.
442/// - `owner` is overwritten with the on-chain user address.
443/// - `sell_token` is replaced with
444///   [`NATIVE_CURRENCY_ADDRESS`](cow_chains::NATIVE_CURRENCY_ADDRESS).
445///
446/// Mirrors `transformOrder` from the `TypeScript` SDK.
447///
448/// # Parameters
449///
450/// * `order` — the [`Order`](super::Order) to enrich (consumed and returned).
451///
452/// # Returns
453///
454/// The enriched [`Order`](super::Order) with `total_fee` set.
455#[must_use]
456pub fn transform_order(mut order: super::Order) -> super::Order {
457    // Compute total fee.
458    let executed_fee_amount: U256 = order.executed_fee_amount.parse().map_or(U256::ZERO, |v| v);
459    let executed_fee: U256 =
460        order.executed_fee.as_deref().and_then(|s| s.parse().ok()).map_or(U256::ZERO, |v| v);
461    order.total_fee = Some((executed_fee_amount + executed_fee).to_string());
462
463    // Apply EthFlow transformations (no-op for regular orders).
464    if let Some(ref ethflow_data) = order.ethflow_data {
465        order.valid_to = ethflow_data.user_valid_to;
466        if let Some(user) = order.onchain_user {
467            order.owner = user;
468        }
469        order.sell_token = cow_chains::NATIVE_CURRENCY_ADDRESS;
470    }
471
472    order
473}
474
475// ── Tests ────────────────────────────────────────────────────────────────────
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    // Test fixtures from the TypeScript SDK test suite.
482    fn sell_order() -> QuoteOrderParams {
483        QuoteOrderParams {
484            kind: OrderKind::Sell,
485            sell_amount: U256::from_str_radix("156144455961718918", 10).unwrap(),
486            fee_amount: U256::from_str_radix("3855544038281082", 10).unwrap(),
487            buy_amount: U256::from_str_radix("18632013982", 10).unwrap(),
488        }
489    }
490
491    fn buy_order() -> QuoteOrderParams {
492        QuoteOrderParams {
493            kind: OrderKind::Buy,
494            sell_amount: U256::from_str_radix("168970833896526983", 10).unwrap(),
495            fee_amount: U256::from_str_radix("2947344072902629", 10).unwrap(),
496            buy_amount: U256::from_str_radix("2000000000", 10).unwrap(),
497        }
498    }
499
500    // ── Network costs ────────────────────────────────────────────────────────
501
502    #[test]
503    fn sell_after_network_costs_equals_api_sell_amount() {
504        let order_params = sell_order();
505        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
506            order_params: order_params.clone(),
507            slippage_percent_bps: 0,
508            partner_fee_bps: None,
509            protocol_fee_bps: None,
510        });
511        assert_eq!(result.after_network_costs.sell_amount, order_params.sell_amount);
512    }
513
514    #[test]
515    fn sell_before_network_costs_is_sell_plus_fee() {
516        let order_params = sell_order();
517        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
518            order_params: order_params.clone(),
519            slippage_percent_bps: 0,
520            partner_fee_bps: None,
521            protocol_fee_bps: None,
522        });
523        assert_eq!(
524            result.before_network_costs.sell_amount,
525            order_params.sell_amount + order_params.fee_amount
526        );
527    }
528
529    #[test]
530    fn buy_after_network_costs_is_sell_plus_fee() {
531        let order_params = buy_order();
532        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
533            order_params: order_params.clone(),
534            slippage_percent_bps: 0,
535            partner_fee_bps: None,
536            protocol_fee_bps: None,
537        });
538        assert_eq!(
539            result.after_network_costs.sell_amount,
540            order_params.sell_amount + order_params.fee_amount
541        );
542    }
543
544    #[test]
545    fn buy_before_network_costs_is_raw_sell() {
546        let order_params = buy_order();
547        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
548            order_params: order_params.clone(),
549            slippage_percent_bps: 0,
550            partner_fee_bps: None,
551            protocol_fee_bps: None,
552        });
553        assert_eq!(result.before_network_costs.sell_amount, order_params.sell_amount);
554    }
555
556    #[test]
557    fn sell_buy_amount_includes_network_cost_in_buy_currency() {
558        let order_params = sell_order();
559        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
560            order_params: order_params.clone(),
561            slippage_percent_bps: 0,
562            partner_fee_bps: None,
563            protocol_fee_bps: None,
564        });
565        let network_cost_in_buy =
566            order_params.buy_amount * order_params.fee_amount / order_params.sell_amount;
567        assert_eq!(
568            result.before_network_costs.buy_amount,
569            order_params.buy_amount + network_cost_in_buy
570        );
571        assert_eq!(result.after_network_costs.buy_amount, order_params.buy_amount);
572    }
573
574    #[test]
575    fn buy_buy_amount_unchanged_by_network_costs() {
576        let order_params = buy_order();
577        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
578            order_params,
579            slippage_percent_bps: 0,
580            partner_fee_bps: None,
581            protocol_fee_bps: None,
582        });
583        assert_eq!(result.after_network_costs.buy_amount, result.before_network_costs.buy_amount);
584    }
585
586    // ── Partner fee ──────────────────────────────────────────────────────────
587
588    #[test]
589    fn sell_partner_fee_subtracted_from_buy() {
590        let order_params = sell_order();
591        let partner_fee_bps = 100u32;
592        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
593            order_params: order_params.clone(),
594            slippage_percent_bps: 0,
595            partner_fee_bps: Some(partner_fee_bps),
596            protocol_fee_bps: None,
597        });
598        let network_cost_in_buy =
599            order_params.buy_amount * order_params.fee_amount / order_params.sell_amount;
600        let buy_before_all_fees = order_params.buy_amount + network_cost_in_buy;
601        let expected =
602            buy_before_all_fees * U256::from(partner_fee_bps) / U256::from(ONE_HUNDRED_BPS);
603        assert_eq!(result.costs.partner_fee.amount, expected);
604    }
605
606    #[test]
607    fn buy_partner_fee_added_to_sell() {
608        let order_params = buy_order();
609        let partner_fee_bps = 100u32;
610        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
611            order_params: order_params.clone(),
612            slippage_percent_bps: 0,
613            partner_fee_bps: Some(partner_fee_bps),
614            protocol_fee_bps: None,
615        });
616        let expected =
617            order_params.sell_amount * U256::from(partner_fee_bps) / U256::from(ONE_HUNDRED_BPS);
618        assert_eq!(result.costs.partner_fee.amount, expected);
619    }
620
621    // ── Slippage ─────────────────────────────────────────────────────────────
622
623    #[test]
624    fn sell_slippage_subtracted_from_buy() {
625        let order_params = sell_order();
626        let slippage_bps = 200u32;
627        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
628            order_params: order_params.clone(),
629            slippage_percent_bps: slippage_bps,
630            partner_fee_bps: None,
631            protocol_fee_bps: None,
632        });
633        let buy_after_network = order_params.buy_amount;
634        let slippage = buy_after_network * U256::from(slippage_bps) / U256::from(ONE_HUNDRED_BPS);
635        assert_eq!(result.after_slippage.buy_amount, buy_after_network - slippage);
636    }
637
638    #[test]
639    fn buy_slippage_added_to_sell() {
640        let order_params = buy_order();
641        let slippage_bps = 200u32;
642        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
643            order_params: order_params.clone(),
644            slippage_percent_bps: slippage_bps,
645            partner_fee_bps: None,
646            protocol_fee_bps: None,
647        });
648        let sell_after_network = order_params.sell_amount + order_params.fee_amount;
649        let slippage = sell_after_network * U256::from(slippage_bps) / U256::from(ONE_HUNDRED_BPS);
650        assert_eq!(result.after_slippage.sell_amount, sell_after_network + slippage);
651    }
652
653    // ── Protocol fee ─────────────────────────────────────────────────────────
654
655    #[test]
656    fn sell_protocol_fee_calculated_correctly() {
657        let order_params = sell_order();
658        let protocol_fee_bps = 20.0;
659        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
660            order_params: order_params.clone(),
661            slippage_percent_bps: 0,
662            partner_fee_bps: None,
663            protocol_fee_bps: Some(protocol_fee_bps),
664        });
665        let bps = U256::from(protocol_fee_bps as u64);
666        let denominator = U256::from(ONE_HUNDRED_BPS) - bps;
667        let expected = order_params.buy_amount * bps / denominator;
668        assert_eq!(result.costs.protocol_fee.amount, expected);
669    }
670
671    #[test]
672    fn buy_protocol_fee_calculated_correctly() {
673        let order_params = buy_order();
674        let protocol_fee_bps = 20.0;
675        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
676            order_params: order_params.clone(),
677            slippage_percent_bps: 0,
678            partner_fee_bps: None,
679            protocol_fee_bps: Some(protocol_fee_bps),
680        });
681        let sell_after_network = order_params.sell_amount + order_params.fee_amount;
682        let bps = U256::from(protocol_fee_bps as u64);
683        let denominator = U256::from(ONE_HUNDRED_BPS) + bps;
684        let expected = sell_after_network * bps / denominator;
685        assert_eq!(result.costs.protocol_fee.amount, expected);
686    }
687
688    #[test]
689    fn sell_before_all_fees_includes_protocol_fee_once() {
690        let order_params = sell_order();
691        let protocol_fee_bps = 20.0;
692        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
693            order_params,
694            slippage_percent_bps: 0,
695            partner_fee_bps: None,
696            protocol_fee_bps: Some(protocol_fee_bps),
697        });
698        assert_eq!(
699            result.before_all_fees.buy_amount,
700            result.before_network_costs.buy_amount + result.costs.protocol_fee.amount
701        );
702    }
703
704    #[test]
705    fn buy_before_all_fees_includes_protocol_fee_once() {
706        let order_params = buy_order();
707        let protocol_fee_bps = 20.0;
708        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
709            order_params,
710            slippage_percent_bps: 0,
711            partner_fee_bps: None,
712            protocol_fee_bps: Some(protocol_fee_bps),
713        });
714        assert_eq!(
715            result.before_all_fees.sell_amount,
716            result.before_network_costs.sell_amount - result.costs.protocol_fee.amount
717        );
718    }
719
720    #[test]
721    fn sell_fractional_protocol_fee_bps() {
722        let order_params = sell_order();
723        let protocol_fee_bps: f64 = 0.003;
724        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
725            order_params: order_params.clone(),
726            slippage_percent_bps: 0,
727            partner_fee_bps: None,
728            protocol_fee_bps: Some(protocol_fee_bps),
729        });
730        // Match the TypeScript test: bps = BigInt(0.003 * 100_000) = 300
731        let bps = U256::from((protocol_fee_bps * HUNDRED_THOUSANDS as f64) as u64);
732        let denominator = U256::from(ONE_HUNDRED_BPS) * U256::from(HUNDRED_THOUSANDS) - bps;
733        let expected = order_params.buy_amount * bps / denominator;
734        assert_eq!(result.costs.protocol_fee.amount, expected);
735        // The TS test asserts amount == 5589
736        assert_eq!(result.costs.protocol_fee.amount, U256::from(5589u64));
737    }
738
739    #[test]
740    fn decimal_protocol_fee_bps_from_string_like_ts_test() {
741        // Mirrors the TS getQuote.test case: `protocolFeeBps: '0.3'`.
742        // 0.3 * 100_000 = 30000.000000000004 — the round() guards against the
743        // float-precision error that previously broke BigInt conversion.
744        let order_params = sell_order();
745        let protocol_fee_bps: f64 = "0.3".parse().expect("parse fee bps");
746        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
747            order_params: order_params.clone(),
748            slippage_percent_bps: 0,
749            partner_fee_bps: None,
750            protocol_fee_bps: Some(protocol_fee_bps),
751        });
752        let bps = U256::from((protocol_fee_bps * HUNDRED_THOUSANDS as f64).round() as u64);
753        assert_eq!(bps, U256::from(30_000u64));
754        let denominator = U256::from(ONE_HUNDRED_BPS) * U256::from(HUNDRED_THOUSANDS) - bps;
755        let expected = order_params.buy_amount * bps / denominator;
756        assert_eq!(result.costs.protocol_fee.amount, expected);
757    }
758
759    #[test]
760    fn zero_or_negative_protocol_fee_bps_returns_zero() {
761        let order_params = sell_order();
762        for bps in [0.0, -0.1, -5.0] {
763            let fee = get_protocol_fee_amount(&ProtocolFeeAmountParams {
764                order_params: order_params.clone(),
765                protocol_fee_bps: bps,
766            });
767            assert_eq!(fee, U256::ZERO, "expected 0 for bps={bps}");
768        }
769    }
770
771    #[test]
772    fn buy_fractional_protocol_fee_bps() {
773        let order_params = buy_order();
774        let protocol_fee_bps: f64 = 0.00071;
775        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
776            order_params,
777            slippage_percent_bps: 0,
778            partner_fee_bps: None,
779            protocol_fee_bps: Some(protocol_fee_bps),
780        });
781        // The TS test asserts amount == 12206189769
782        assert_eq!(result.costs.protocol_fee.amount, U256::from(12_206_189_769u64));
783    }
784
785    #[test]
786    fn sell_partner_fee_with_protocol_fee() {
787        let order_params = sell_order();
788        let protocol_fee_bps = 20.0;
789        let partner_fee_bps = 100u32;
790        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
791            order_params: order_params.clone(),
792            slippage_percent_bps: 0,
793            partner_fee_bps: Some(partner_fee_bps),
794            protocol_fee_bps: Some(protocol_fee_bps),
795        });
796
797        let buy_after = order_params.buy_amount;
798        let protocol_bps = U256::from(protocol_fee_bps as u64);
799        let protocol_denom = U256::from(ONE_HUNDRED_BPS) - protocol_bps;
800        let protocol_fee = buy_after * protocol_bps / protocol_denom;
801
802        let network_cost_in_buy = buy_after * order_params.fee_amount / order_params.sell_amount;
803        let buy_before_all_fees = buy_after + network_cost_in_buy + protocol_fee;
804        let expected_partner =
805            buy_before_all_fees * U256::from(partner_fee_bps) / U256::from(ONE_HUNDRED_BPS);
806        assert_eq!(result.costs.partner_fee.amount, expected_partner);
807        assert_eq!(result.after_partner_fees.buy_amount, buy_after - expected_partner);
808    }
809
810    #[test]
811    fn buy_partner_fee_with_protocol_fee() {
812        let order_params = buy_order();
813        let protocol_fee_bps = 20.0;
814        let partner_fee_bps = 100u32;
815        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
816            order_params: order_params.clone(),
817            slippage_percent_bps: 0,
818            partner_fee_bps: Some(partner_fee_bps),
819            protocol_fee_bps: Some(protocol_fee_bps),
820        });
821
822        let sell_amount = order_params.sell_amount;
823        let fee_amount = order_params.fee_amount;
824        let sell_after_network = sell_amount + fee_amount;
825
826        let protocol_bps = U256::from(protocol_fee_bps as u64);
827        let protocol_denom = U256::from(ONE_HUNDRED_BPS) + protocol_bps;
828        let protocol_fee = sell_after_network * protocol_bps / protocol_denom;
829
830        let sell_before_all_fees = sell_amount - protocol_fee;
831        let expected_partner =
832            sell_before_all_fees * U256::from(partner_fee_bps) / U256::from(ONE_HUNDRED_BPS);
833        assert_eq!(result.costs.partner_fee.amount, expected_partner);
834        assert_eq!(result.after_partner_fees.sell_amount, sell_after_network + expected_partner);
835    }
836
837    // ── Zero protocol fee ───────────────────────────────────────────────────
838
839    #[test]
840    fn sell_protocol_fee_zero_bps() {
841        let order_params = sell_order();
842        let amount = get_protocol_fee_amount(&ProtocolFeeAmountParams {
843            order_params,
844            protocol_fee_bps: 0.0,
845        });
846        assert_eq!(amount, U256::ZERO);
847    }
848
849    #[test]
850    fn sell_protocol_fee_negative_bps() {
851        let order_params = sell_order();
852        let amount = get_protocol_fee_amount(&ProtocolFeeAmountParams {
853            order_params,
854            protocol_fee_bps: -1.0,
855        });
856        assert_eq!(amount, U256::ZERO);
857    }
858
859    // ── Zero sell amount (division by zero guard) ───────────────────────────
860
861    #[test]
862    fn sell_order_zero_sell_amount() {
863        let order_params = QuoteOrderParams {
864            kind: OrderKind::Sell,
865            sell_amount: U256::ZERO,
866            buy_amount: U256::from(1000u64),
867            fee_amount: U256::from(100u64),
868        };
869        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
870            order_params,
871            slippage_percent_bps: 50,
872            partner_fee_bps: None,
873            protocol_fee_bps: None,
874        });
875        assert_eq!(result.costs.network_fee.amount_in_buy_currency, U256::ZERO);
876    }
877
878    // ── Partner fee zero bps ────────────────────────────────────────────────
879
880    #[test]
881    fn partner_fee_zero_bps() {
882        let amounts =
883            QuoteAmounts { sell_amount: U256::from(1000u64), buy_amount: U256::from(500u64) };
884        let result = get_quote_amounts_after_partner_fee(&amounts, &amounts, true, 0);
885        assert_eq!(result.partner_fee_amount, U256::ZERO);
886        assert_eq!(result.after_partner_fees.buy_amount, U256::from(500u64));
887    }
888
889    // ── Slippage ────────────────────────────────────────────────────────────
890
891    #[test]
892    fn sell_slippage_reduces_buy_amount() {
893        let amounts =
894            QuoteAmounts { sell_amount: U256::from(1000u64), buy_amount: U256::from(10_000u64) };
895        let result = get_quote_amounts_after_slippage(&amounts, true, 100); // 1% slippage
896        assert_eq!(result.sell_amount, U256::from(1000u64));
897        assert!(result.buy_amount < U256::from(10_000u64));
898    }
899
900    #[test]
901    fn buy_slippage_increases_sell_amount() {
902        let amounts =
903            QuoteAmounts { sell_amount: U256::from(10_000u64), buy_amount: U256::from(1000u64) };
904        let result = get_quote_amounts_after_slippage(&amounts, false, 100); // 1% slippage
905        assert!(result.sell_amount > U256::from(10_000u64));
906        assert_eq!(result.buy_amount, U256::from(1000u64));
907    }
908
909    // ── Buy order full pipeline ─────────────────────────────────────────
910
911    #[test]
912    fn buy_order_amounts_to_sign_correct() {
913        let order_params = buy_order();
914        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
915            order_params,
916            slippage_percent_bps: 50,
917            partner_fee_bps: Some(50),
918            protocol_fee_bps: Some(10.0),
919        });
920        // For buy orders, amounts_to_sign.sell_amount = after_slippage.sell_amount
921        assert_eq!(result.amounts_to_sign.sell_amount, result.after_slippage.sell_amount);
922        // For buy orders, amounts_to_sign.buy_amount = before_all_fees.buy_amount
923        assert_eq!(result.amounts_to_sign.buy_amount, result.before_all_fees.buy_amount);
924        assert!(!result.is_sell);
925    }
926
927    #[test]
928    fn sell_order_amounts_to_sign_correct() {
929        let order_params = sell_order();
930        let result = get_quote_amounts_and_costs(&QuoteAmountsAndCostsParams {
931            order_params,
932            slippage_percent_bps: 50,
933            partner_fee_bps: Some(50),
934            protocol_fee_bps: Some(10.0),
935        });
936        // For sell orders, amounts_to_sign.sell_amount = before_all_fees.sell_amount
937        assert_eq!(result.amounts_to_sign.sell_amount, result.before_all_fees.sell_amount);
938        // For sell orders, amounts_to_sign.buy_amount = after_slippage.buy_amount
939        assert_eq!(result.amounts_to_sign.buy_amount, result.after_slippage.buy_amount);
940        assert!(result.is_sell);
941    }
942
943    // ── Transform order ─────────────────────────────────────────────────
944
945    #[test]
946    fn protocol_fee_bps_below_scaling_resolution_returns_zero() {
947        // BPS small enough that `(bps * 100_000).round() as u64 == 0` —
948        // i.e. inside the scaling resolution. Drives the early-return
949        // branch that guards against U256-zero division by checking
950        // `protocol_fee_bps_big.is_zero()` after rounding.
951        let result = get_protocol_fee_amount(&ProtocolFeeAmountParams {
952            order_params: sell_order(),
953            protocol_fee_bps: 1e-6,
954        });
955        assert_eq!(result, U256::ZERO);
956    }
957
958    #[test]
959    fn transform_order_with_ethflow_data_overrides_owner_and_sell_token() {
960        // Drives the EthFlow branch of `transform_order`: when an order
961        // carries `ethflow_data` and an `onchain_user`, the `valid_to` is
962        // taken from `ethflow_data.user_valid_to`, the `owner` is replaced
963        // with `onchain_user`, and the `sell_token` is rewritten to the
964        // native-currency sentinel.
965        let onchain_user = "0x2222222222222222222222222222222222222222"
966            .parse::<alloy_primitives::Address>()
967            .unwrap();
968        let json = serde_json::json!({
969            "uid": "0xmockuid",
970            "sellToken": "0xfff9976782d46cc05630d1f6ebab18b2324d6b14",
971            "buyToken": "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
972            "receiver": "0x0000000000000000000000000000000000000000",
973            "sellAmount": "1000",
974            "buyAmount": "500",
975            "validTo": 1_700_000_000u32,
976            "appData": "0x0000000000000000000000000000000000000000000000000000000000000000",
977            "feeAmount": "10",
978            "kind": "sell",
979            "partiallyFillable": false,
980            "creationDate": "2024-01-01T00:00:00Z",
981            "owner": "0x0000000000000000000000000000000000000000",
982            "executedSellAmount": "100",
983            "executedSellAmountBeforeFees": "100",
984            "executedBuyAmount": "50",
985            "executedFeeAmount": "5",
986            "invalidated": false,
987            "status": "open",
988            "signingScheme": "eip712",
989            "signature": "0x",
990            "ethflowData": { "userValidTo": 1_999_999_999u32, "isRefundClaimed": false },
991            "onchainUser": format!("{onchain_user:#x}"),
992        });
993        let order: super::super::Order = serde_json::from_value(json).unwrap();
994        let enriched = transform_order(order);
995
996        // EthFlow `valid_to` overrides the on-chain order `valid_to`.
997        assert_eq!(enriched.valid_to, 1_999_999_999);
998        // `owner` is replaced with the real user behind the EthFlow contract.
999        assert_eq!(enriched.owner, onchain_user);
1000        // `sell_token` is rewritten to the native-currency sentinel because
1001        // EthFlow always sells native ETH.
1002        assert_eq!(enriched.sell_token, cow_chains::NATIVE_CURRENCY_ADDRESS);
1003    }
1004
1005    #[test]
1006    fn transform_order_with_ethflow_data_without_onchain_user_keeps_owner() {
1007        // Same EthFlow branch as above but with `onchain_user = None` —
1008        // confirms the inner `if let Some(user)` arm is correctly skipped
1009        // while the `valid_to` and `sell_token` rewrites still happen.
1010        let original_owner = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1011            .parse::<alloy_primitives::Address>()
1012            .unwrap();
1013        let json = serde_json::json!({
1014            "uid": "0xmockuid",
1015            "sellToken": "0xfff9976782d46cc05630d1f6ebab18b2324d6b14",
1016            "buyToken": "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
1017            "receiver": "0x0000000000000000000000000000000000000000",
1018            "sellAmount": "1000",
1019            "buyAmount": "500",
1020            "validTo": 1_700_000_000u32,
1021            "appData": "0x0000000000000000000000000000000000000000000000000000000000000000",
1022            "feeAmount": "10",
1023            "kind": "sell",
1024            "partiallyFillable": false,
1025            "creationDate": "2024-01-01T00:00:00Z",
1026            "owner": format!("{original_owner:#x}"),
1027            "executedSellAmount": "100",
1028            "executedSellAmountBeforeFees": "100",
1029            "executedBuyAmount": "50",
1030            "executedFeeAmount": "5",
1031            "invalidated": false,
1032            "status": "open",
1033            "signingScheme": "eip712",
1034            "signature": "0x",
1035            "ethflowData": { "userValidTo": 42, "isRefundClaimed": true },
1036        });
1037        let order: super::super::Order = serde_json::from_value(json).unwrap();
1038        let enriched = transform_order(order);
1039        assert_eq!(enriched.valid_to, 42);
1040        assert_eq!(enriched.owner, original_owner); // unchanged — no onchain_user
1041        assert_eq!(enriched.sell_token, cow_chains::NATIVE_CURRENCY_ADDRESS);
1042    }
1043
1044    #[test]
1045    fn transform_order_sets_total_fee() {
1046        let json = serde_json::json!({
1047            "uid": "0xmockuid",
1048            "sellToken": "0xfff9976782d46cc05630d1f6ebab18b2324d6b14",
1049            "buyToken": "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
1050            "receiver": "0x0000000000000000000000000000000000000000",
1051            "sellAmount": "1000",
1052            "buyAmount": "500",
1053            "validTo": 1700000000u32,
1054            "appData": "0x0000000000000000000000000000000000000000000000000000000000000000",
1055            "feeAmount": "10",
1056            "kind": "sell",
1057            "partiallyFillable": false,
1058            "creationDate": "2024-01-01T00:00:00Z",
1059            "owner": "0x0000000000000000000000000000000000000000",
1060            "executedSellAmount": "100",
1061            "executedSellAmountBeforeFees": "100",
1062            "executedBuyAmount": "50",
1063            "executedFeeAmount": "5",
1064            "invalidated": false,
1065            "status": "open",
1066            "signingScheme": "eip712",
1067            "signature": "0x",
1068        });
1069        let order: super::super::Order = serde_json::from_value(json).unwrap();
1070        let enriched = transform_order(order);
1071        assert!(enriched.total_fee.is_some());
1072        assert_eq!(enriched.total_fee.unwrap(), "5");
1073    }
1074}