Skip to main content

cow_trading/
types.rs

1//! High-level trading types: parameters, fee breakdown, and quote results.
2
3use std::fmt;
4
5use alloy_primitives::{Address, U256};
6use cow_orderbook::types::OrderQuoteResponse;
7use cow_signing::types::{OrderTypedData, UnsignedOrder};
8use cow_types::OrderKind;
9
10/// Amounts at a specific stage of the fee pipeline.
11#[derive(Debug, Clone, Copy, Default)]
12pub struct Amounts {
13    /// Sell-token amount at this stage (in atoms).
14    pub sell_amount: U256,
15    /// Buy-token amount at this stage (in atoms).
16    pub buy_amount: U256,
17}
18
19impl Amounts {
20    /// Construct an [`Amounts`] from sell and buy amounts.
21    ///
22    /// # Arguments
23    ///
24    /// * `sell_amount` — sell-token amount in atoms.
25    /// * `buy_amount` — buy-token amount in atoms.
26    ///
27    /// # Returns
28    ///
29    /// A new [`Amounts`] instance.
30    #[must_use]
31    pub const fn new(sell_amount: U256, buy_amount: U256) -> Self {
32        Self { sell_amount, buy_amount }
33    }
34
35    /// Returns `true` if both sell and buy amounts are zero.
36    ///
37    /// # Returns
38    ///
39    /// `true` when `sell_amount` and `buy_amount` are both `U256::ZERO`.
40    #[must_use]
41    pub fn is_zero(&self) -> bool {
42        self.sell_amount.is_zero() && self.buy_amount.is_zero()
43    }
44
45    /// Total token amount: `sell_amount + buy_amount` (saturating).
46    ///
47    /// ```
48    /// use alloy_primitives::U256;
49    /// use cow_trading::Amounts;
50    ///
51    /// let a = Amounts::new(U256::from(100u32), U256::from(90u32));
52    /// assert_eq!(a.total(), U256::from(190u32));
53    /// ```
54    #[must_use]
55    pub const fn total(&self) -> U256 {
56        self.sell_amount.saturating_add(self.buy_amount)
57    }
58}
59
60impl fmt::Display for Amounts {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        write!(f, "sell {} → buy {}", self.sell_amount, self.buy_amount)
63    }
64}
65
66/// Network fee expressed in both currencies.
67#[derive(Debug, Clone, Copy, Default)]
68pub struct NetworkFee {
69    /// Fee denominated in `sell_token` atoms.
70    pub amount_in_sell_currency: U256,
71    /// Fee denominated in `buy_token` atoms (estimated).
72    pub amount_in_buy_currency: U256,
73}
74
75impl NetworkFee {
76    /// Construct a [`NetworkFee`] from sell-currency and buy-currency amounts.
77    ///
78    /// # Arguments
79    ///
80    /// * `amount_in_sell_currency` — fee denominated in sell-token atoms.
81    /// * `amount_in_buy_currency` — fee denominated in buy-token atoms (estimated).
82    ///
83    /// # Returns
84    ///
85    /// A new [`NetworkFee`] instance.
86    #[must_use]
87    pub const fn new(amount_in_sell_currency: U256, amount_in_buy_currency: U256) -> Self {
88        Self { amount_in_sell_currency, amount_in_buy_currency }
89    }
90
91    /// Returns `true` if both fee components are zero.
92    ///
93    /// # Returns
94    ///
95    /// `true` when both `amount_in_sell_currency` and `amount_in_buy_currency` are `U256::ZERO`.
96    #[must_use]
97    pub fn is_zero(&self) -> bool {
98        self.amount_in_sell_currency.is_zero() && self.amount_in_buy_currency.is_zero()
99    }
100
101    /// Total fee: `sell_currency + buy_currency` (saturating).
102    ///
103    /// # Returns
104    ///
105    /// The saturating sum of both fee components.
106    #[must_use]
107    pub const fn total_atoms(&self) -> U256 {
108        self.amount_in_sell_currency.saturating_add(self.amount_in_buy_currency)
109    }
110}
111
112impl fmt::Display for NetworkFee {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        write!(
115            f,
116            "network-fee sell={} buy={}",
117            self.amount_in_sell_currency, self.amount_in_buy_currency,
118        )
119    }
120}
121
122/// Partner fee cost component.
123///
124/// Mirrors the `costs.partnerFee` field in the `TypeScript` SDK's `QuoteAmountsAndCosts`.
125#[derive(Debug, Clone, Copy, Default)]
126pub struct PartnerFeeCost {
127    /// Fee amount deducted from the output token (in atoms).
128    pub amount: U256,
129    /// Fee rate in basis points.
130    pub bps: u32,
131}
132
133impl PartnerFeeCost {
134    /// Construct a [`PartnerFeeCost`] from fee amount and basis points.
135    ///
136    /// # Arguments
137    ///
138    /// * `amount` — fee amount deducted from the output token (in atoms).
139    /// * `bps` — fee rate in basis points.
140    ///
141    /// # Returns
142    ///
143    /// A new [`PartnerFeeCost`] instance.
144    #[must_use]
145    pub const fn new(amount: U256, bps: u32) -> Self {
146        Self { amount, bps }
147    }
148
149    /// Returns `true` if the fee amount is zero and the rate is 0 bps.
150    ///
151    /// # Returns
152    ///
153    /// `true` when both `amount` is `U256::ZERO` and `bps` is `0`.
154    #[must_use]
155    pub fn is_zero(&self) -> bool {
156        self.amount.is_zero() && self.bps == 0
157    }
158
159    /// Returns `true` if the fee rate is non-zero.
160    ///
161    /// # Returns
162    ///
163    /// `true` when `bps > 0`.
164    #[must_use]
165    pub const fn has_bps(&self) -> bool {
166        self.bps > 0
167    }
168}
169
170impl fmt::Display for PartnerFeeCost {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        write!(f, "partner-fee {}bps {}", self.bps, self.amount)
173    }
174}
175
176/// Protocol fee cost component.
177///
178/// Mirrors the `costs.protocolFee` field in the `TypeScript` SDK's `QuoteAmountsAndCosts`.
179#[derive(Debug, Clone, Copy, Default)]
180pub struct ProtocolFeeCost {
181    /// Fee amount (in atoms).
182    pub amount: U256,
183    /// Fee rate in basis points.
184    pub bps: u32,
185}
186
187impl ProtocolFeeCost {
188    /// Construct a [`ProtocolFeeCost`] from fee amount and basis points.
189    ///
190    /// # Arguments
191    ///
192    /// * `amount` — fee amount in atoms.
193    /// * `bps` — fee rate in basis points.
194    ///
195    /// # Returns
196    ///
197    /// A new [`ProtocolFeeCost`] instance.
198    #[must_use]
199    pub const fn new(amount: U256, bps: u32) -> Self {
200        Self { amount, bps }
201    }
202
203    /// Returns `true` if the fee amount is zero and the rate is 0 bps.
204    ///
205    /// # Returns
206    ///
207    /// `true` when both `amount` is `U256::ZERO` and `bps` is `0`.
208    #[must_use]
209    pub fn is_zero(&self) -> bool {
210        self.amount.is_zero() && self.bps == 0
211    }
212
213    /// Returns `true` if the fee rate is non-zero.
214    ///
215    /// # Returns
216    ///
217    /// `true` when `bps > 0`.
218    #[must_use]
219    pub const fn has_bps(&self) -> bool {
220        self.bps > 0
221    }
222}
223
224impl fmt::Display for ProtocolFeeCost {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        write!(f, "protocol-fee {}bps {}", self.bps, self.amount)
227    }
228}
229
230/// Full fee and amount breakdown for a quoted trade, mirroring the `CoW` SDK's
231/// `QuoteAmountsAndCosts` structure.
232#[derive(Debug, Clone, Copy)]
233pub struct QuoteAmountsAndCosts {
234    /// `true` for sell orders, `false` for buy orders.
235    pub is_sell: bool,
236    /// Gross amounts before **all** fees (network + partner + protocol).
237    pub before_all_fees: Amounts,
238    /// Gross amounts before the network fee (may reflect protocol fee adjustments).
239    pub before_network_costs: Amounts,
240    /// Amounts after deducting the network (protocol) fee.
241    pub after_network_costs: Amounts,
242    /// Amounts after deducting the partner fee.
243    pub after_partner_fees: Amounts,
244    /// Amounts after applying the requested slippage tolerance.
245    pub after_slippage: Amounts,
246    /// The network fee component.
247    pub network_fee: NetworkFee,
248    /// The partner fee component (zero when no partner fee is configured).
249    pub partner_fee: PartnerFeeCost,
250    /// The protocol fee component (zero when not applicable).
251    pub protocol_fee: ProtocolFeeCost,
252}
253
254/// App-data document and its `keccak256` hash.
255///
256/// Mirrors `TradingAppDataInfo` from the `TypeScript` SDK.
257#[derive(Debug, Clone)]
258pub struct TradingAppDataInfo {
259    /// The canonical JSON app-data document (full UTF-8 string).
260    pub full_app_data: String,
261    /// `keccak256(full_app_data)` as a `0x`-prefixed 32-byte hex string.
262    pub app_data_keccak256: String,
263}
264
265impl TradingAppDataInfo {
266    /// Construct a [`TradingAppDataInfo`] from raw content and its hash.
267    ///
268    /// # Arguments
269    ///
270    /// * `full_app_data` — the canonical JSON app-data document.
271    /// * `app_data_keccak256` — `keccak256(full_app_data)` as a `0x`-prefixed hex string.
272    ///
273    /// # Returns
274    ///
275    /// A new [`TradingAppDataInfo`] instance.
276    #[must_use]
277    pub fn new(full_app_data: impl Into<String>, app_data_keccak256: impl Into<String>) -> Self {
278        Self { full_app_data: full_app_data.into(), app_data_keccak256: app_data_keccak256.into() }
279    }
280
281    /// Returns `true` if the full app-data document is non-empty.
282    ///
283    /// # Returns
284    ///
285    /// `true` when `full_app_data` is a non-empty string.
286    #[must_use]
287    pub const fn has_full_app_data(&self) -> bool {
288        !self.full_app_data.is_empty()
289    }
290
291    /// Returns the full app-data JSON document as a string slice.
292    ///
293    /// # Returns
294    ///
295    /// A `&str` reference to the full app-data JSON content.
296    ///
297    /// ```
298    /// use cow_trading::TradingAppDataInfo;
299    ///
300    /// let info = TradingAppDataInfo::new("{}", "0xabc");
301    /// assert_eq!(info.full_app_data_ref(), "{}");
302    /// ```
303    #[must_use]
304    pub fn full_app_data_ref(&self) -> &str {
305        &self.full_app_data
306    }
307
308    /// Returns the `keccak256` hash of the app-data as a `0x`-prefixed hex string slice.
309    ///
310    /// # Returns
311    ///
312    /// A `&str` reference to the `0x`-prefixed 32-byte hex hash.
313    ///
314    /// ```
315    /// use cow_trading::TradingAppDataInfo;
316    ///
317    /// let info = TradingAppDataInfo::new("{}", "0xdeadbeef");
318    /// assert_eq!(info.keccak256_ref(), "0xdeadbeef");
319    /// ```
320    #[must_use]
321    pub fn keccak256_ref(&self) -> &str {
322        &self.app_data_keccak256
323    }
324}
325
326impl fmt::Display for TradingAppDataInfo {
327    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328        write!(f, "app-data({})", self.app_data_keccak256)
329    }
330}
331
332/// A raw Ethereum transaction object produced by the `CoW` SDK on-chain helpers.
333///
334/// Mirrors `TradingTransactionParams` from the `TypeScript` SDK.
335/// Used by calldata-returning functions such as `getPreSignTransaction` and
336/// `getEthFlowTransaction`.
337#[derive(Debug, Clone)]
338pub struct TradingTransactionParams {
339    /// ABI-encoded calldata (`0x`-prefixed hex string).
340    pub data: Vec<u8>,
341    /// Target contract address.
342    pub to: Address,
343    /// Gas limit for the transaction.
344    pub gas_limit: u64,
345    /// ETH value to send in wei (usually zero for token swaps).
346    pub value: U256,
347}
348
349impl TradingTransactionParams {
350    /// Construct a [`TradingTransactionParams`] from its four core fields.
351    ///
352    /// # Arguments
353    ///
354    /// * `data` — ABI-encoded calldata bytes.
355    /// * `to` — target contract address.
356    /// * `gas_limit` — gas limit for the transaction.
357    /// * `value` — ETH value to send in wei.
358    ///
359    /// # Returns
360    ///
361    /// A new [`TradingTransactionParams`] instance.
362    #[must_use]
363    pub const fn new(data: Vec<u8>, to: Address, gas_limit: u64, value: U256) -> Self {
364        Self { data, to, gas_limit, value }
365    }
366
367    /// Override the ABI-encoded calldata.
368    ///
369    /// # Arguments
370    ///
371    /// * `data` — replacement calldata bytes.
372    ///
373    /// # Returns
374    ///
375    /// The modified [`TradingTransactionParams`] with the new calldata.
376    #[must_use]
377    pub fn with_data(mut self, data: Vec<u8>) -> Self {
378        self.data = data;
379        self
380    }
381
382    /// Override the target contract address.
383    ///
384    /// # Arguments
385    ///
386    /// * `to` — replacement target contract address.
387    ///
388    /// # Returns
389    ///
390    /// The modified [`TradingTransactionParams`] with the new target.
391    #[must_use]
392    pub const fn with_to(mut self, to: Address) -> Self {
393        self.to = to;
394        self
395    }
396
397    /// Override the gas limit.
398    ///
399    /// # Arguments
400    ///
401    /// * `gas_limit` — replacement gas limit.
402    ///
403    /// # Returns
404    ///
405    /// The modified [`TradingTransactionParams`] with the new gas limit.
406    #[must_use]
407    pub const fn with_gas_limit(mut self, gas_limit: u64) -> Self {
408        self.gas_limit = gas_limit;
409        self
410    }
411
412    /// Override the ETH value to send (in wei).
413    ///
414    /// # Arguments
415    ///
416    /// * `value` — replacement ETH value in wei.
417    ///
418    /// # Returns
419    ///
420    /// The modified [`TradingTransactionParams`] with the new value.
421    #[must_use]
422    pub const fn with_value(mut self, value: U256) -> Self {
423        self.value = value;
424        self
425    }
426
427    /// Returns the length of the calldata in bytes.
428    ///
429    /// # Returns
430    ///
431    /// The byte length of `data`.
432    #[must_use]
433    pub const fn data_len(&self) -> usize {
434        self.data.len()
435    }
436
437    /// Returns `true` if the ETH value to send is non-zero.
438    ///
439    /// # Returns
440    ///
441    /// `true` when `value` is not `U256::ZERO`.
442    #[must_use]
443    pub fn has_value(&self) -> bool {
444        !self.value.is_zero()
445    }
446}
447
448impl fmt::Display for TradingTransactionParams {
449    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
450        write!(f, "tx to={:#x} gas={}", self.to, self.gas_limit)
451    }
452}
453
454/// Additional parameters for posting orders.
455///
456/// All fields are optional; the SDK applies sensible defaults when absent.
457/// Mirrors `PostTradeAdditionalParams` from the `TypeScript` SDK.
458#[derive(Debug, Clone, Default)]
459pub struct PostTradeAdditionalParams {
460    /// Override for the order signing scheme.
461    ///
462    /// Defaults to [`EcdsaSigningScheme::Eip712`](cow_types::EcdsaSigningScheme::Eip712)
463    /// for EOA wallets.  Use
464    /// [`SigningScheme::PreSign`](cow_types::SigningScheme::PreSign) for
465    /// smart-contract wallets.
466    pub signing_scheme: Option<cow_types::SigningScheme>,
467    /// Network gas cost in wei, expressed as a decimal string.
468    ///
469    /// Used when computing adjusted quote amounts.  Set to `None` to use the
470    /// cost embedded in the quote response.
471    pub network_costs_amount: Option<String>,
472    /// When `Some(false)`, the SDK posts raw caller-supplied amounts without
473    /// adjusting for network costs, slippage, or partner fees.
474    ///
475    /// Defaults to `None` (amounts are adjusted, as for swap orders).
476    pub apply_costs_slippage_and_fees: Option<bool>,
477    /// Optional protocol-fee rate in basis points sourced from the `/quote`
478    /// response (`OrderQuoteResponse::protocol_fee_bps`).
479    ///
480    /// Required for orders that have BOTH a protocol fee AND a partner fee:
481    /// without it, the partner-fee base is computed against the wrong
482    /// `before_all_fees` and the final `buy_amount` is overstated.
483    /// Mirrors `protocolFeeBps` added to `PostTradeAdditionalParams` in
484    /// `cow-sdk` PR #867.
485    pub protocol_fee_bps: Option<f64>,
486}
487
488impl PostTradeAdditionalParams {
489    /// Set the signing scheme override.
490    ///
491    /// # Arguments
492    ///
493    /// * `scheme` — the [`SigningScheme`](cow_types::SigningScheme) to use for this order.
494    ///
495    /// # Returns
496    ///
497    /// The modified [`PostTradeAdditionalParams`] with the signing scheme set.
498    #[must_use]
499    pub const fn with_signing_scheme(mut self, scheme: cow_types::SigningScheme) -> Self {
500        self.signing_scheme = Some(scheme);
501        self
502    }
503
504    /// Set the network cost amount override (decimal atom string).
505    ///
506    /// # Arguments
507    ///
508    /// * `amount` — network gas cost in wei as a decimal string.
509    ///
510    /// # Returns
511    ///
512    /// The modified [`PostTradeAdditionalParams`] with the network cost set.
513    #[must_use]
514    pub fn with_network_costs_amount(mut self, amount: impl Into<String>) -> Self {
515        self.network_costs_amount = Some(amount.into());
516        self
517    }
518
519    /// Override whether the SDK adjusts amounts for costs, slippage, and fees.
520    ///
521    /// # Arguments
522    ///
523    /// * `apply` — `true` to let the SDK adjust amounts; `false` to post raw amounts.
524    ///
525    /// # Returns
526    ///
527    /// The modified [`PostTradeAdditionalParams`] with the flag set.
528    #[must_use]
529    pub const fn with_apply_costs_slippage_and_fees(mut self, apply: bool) -> Self {
530        self.apply_costs_slippage_and_fees = Some(apply);
531        self
532    }
533
534    /// Returns `true` if a signing scheme override is set.
535    ///
536    /// # Returns
537    ///
538    /// `true` when `signing_scheme` is `Some`.
539    #[must_use]
540    pub const fn has_signing_scheme(&self) -> bool {
541        self.signing_scheme.is_some()
542    }
543
544    /// Returns `true` if a network costs amount override is set.
545    ///
546    /// # Returns
547    ///
548    /// `true` when `network_costs_amount` is `Some`.
549    #[must_use]
550    pub const fn has_network_costs(&self) -> bool {
551        self.network_costs_amount.is_some()
552    }
553
554    /// Returns `true` if `apply_costs_slippage_and_fees` is explicitly set to `true`.
555    ///
556    /// # Returns
557    ///
558    /// `true` only when the inner value is `Some(true)`.
559    #[must_use]
560    pub const fn should_apply_costs(&self) -> bool {
561        matches!(self.apply_costs_slippage_and_fees, Some(true))
562    }
563
564    /// Override the protocol-fee rate sourced from the `/quote` response.
565    ///
566    /// # Arguments
567    ///
568    /// * `bps` — protocol-fee rate in basis points (may be fractional, e.g. `0.3`).
569    ///
570    /// # Returns
571    ///
572    /// The modified [`PostTradeAdditionalParams`] with the protocol-fee rate set.
573    #[must_use]
574    pub const fn with_protocol_fee_bps(mut self, bps: f64) -> Self {
575        self.protocol_fee_bps = Some(bps);
576        self
577    }
578
579    /// Returns `true` if a protocol-fee rate is set.
580    #[must_use]
581    pub const fn has_protocol_fee_bps(&self) -> bool {
582        self.protocol_fee_bps.is_some()
583    }
584}
585
586impl fmt::Display for PostTradeAdditionalParams {
587    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
588        f.write_str("post-trade-params")
589    }
590}
591
592/// Advanced overrides for swap and quote operations.
593///
594/// Mirrors `SwapAdvancedSettings` from the `TypeScript` SDK.
595#[derive(Debug, Clone, Default)]
596pub struct SwapAdvancedSettings {
597    /// Custom app-data fields to merge into the auto-generated document.
598    pub app_data: Option<serde_json::Value>,
599    /// Override for the slippage tolerance in basis points.
600    ///
601    /// Takes precedence over [`TradeParameters::slippage_bps`] and the
602    /// SDK-level [`crate::TradingSdkConfig::slippage_bps`].
603    pub slippage_bps: Option<u32>,
604    /// Override for the partner fee.
605    ///
606    /// Takes precedence over [`TradeParameters::partner_fee`] and the
607    /// SDK-level [`crate::TradingSdkConfig::partner_fee`].
608    pub partner_fee: Option<cow_app_data::types::PartnerFee>,
609}
610
611impl SwapAdvancedSettings {
612    /// Set custom app-data fields to merge into the auto-generated document.
613    ///
614    /// # Arguments
615    ///
616    /// * `app_data` — JSON value containing custom app-data fields.
617    ///
618    /// # Returns
619    ///
620    /// The modified [`SwapAdvancedSettings`] with the app-data set.
621    #[must_use]
622    pub fn with_app_data(mut self, app_data: serde_json::Value) -> Self {
623        self.app_data = Some(app_data);
624        self
625    }
626
627    /// Override the slippage tolerance in basis points for this swap.
628    ///
629    /// # Arguments
630    ///
631    /// * `bps` — slippage tolerance in basis points (e.g. `50` for 0.5%).
632    ///
633    /// # Returns
634    ///
635    /// The modified [`SwapAdvancedSettings`] with the slippage override set.
636    #[must_use]
637    pub const fn with_slippage_bps(mut self, bps: u32) -> Self {
638        self.slippage_bps = Some(bps);
639        self
640    }
641
642    /// Override the partner fee for this swap.
643    ///
644    /// # Arguments
645    ///
646    /// * `fee` — the [`PartnerFee`](cow_app_data::types::PartnerFee) to apply.
647    ///
648    /// # Returns
649    ///
650    /// The modified [`SwapAdvancedSettings`] with the partner fee set.
651    #[must_use]
652    pub fn with_partner_fee(mut self, fee: cow_app_data::types::PartnerFee) -> Self {
653        self.partner_fee = Some(fee);
654        self
655    }
656
657    /// Returns `true` if custom app-data fields are set.
658    ///
659    /// # Returns
660    ///
661    /// `true` when `app_data` is `Some`.
662    #[must_use]
663    pub const fn has_app_data(&self) -> bool {
664        self.app_data.is_some()
665    }
666
667    /// Returns `true` if a slippage tolerance override is set.
668    ///
669    /// # Returns
670    ///
671    /// `true` when `slippage_bps` is `Some`.
672    #[must_use]
673    pub const fn has_slippage_bps(&self) -> bool {
674        self.slippage_bps.is_some()
675    }
676
677    /// Returns `true` if a partner fee override is set.
678    ///
679    /// # Returns
680    ///
681    /// `true` when `partner_fee` is `Some`.
682    #[must_use]
683    pub const fn has_partner_fee(&self) -> bool {
684        self.partner_fee.is_some()
685    }
686}
687
688impl fmt::Display for SwapAdvancedSettings {
689    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
690        f.write_str("swap-settings")
691    }
692}
693
694/// Advanced overrides for limit order submission.
695///
696/// Applied on top of [`LimitTradeParameters`] via
697/// [`apply_settings_to_limit_trade_parameters`].
698///
699/// Mirrors `LimitOrderAdvancedSettings` from the `TypeScript` SDK.
700#[derive(Debug, Clone, Default)]
701pub struct LimitOrderAdvancedSettings {
702    /// Override for the order receiver.
703    pub receiver: Option<Address>,
704    /// Absolute order expiry timestamp.  Overrides `valid_for` in the params.
705    pub valid_to: Option<u32>,
706    /// Partner fee override (replaces any fee set at the config level).
707    pub partner_fee: Option<cow_app_data::types::PartnerFee>,
708    /// Whether the order may be partially filled.
709    pub partially_fillable: Option<bool>,
710    /// Pre-computed app-data hash override (`0x`-prefixed `bytes32`).
711    pub app_data: Option<String>,
712}
713
714impl LimitOrderAdvancedSettings {
715    /// Override the order receiver address.
716    ///
717    /// # Arguments
718    ///
719    /// * `receiver` — the address that will receive the bought tokens.
720    ///
721    /// # Returns
722    ///
723    /// The modified [`LimitOrderAdvancedSettings`] with the receiver set.
724    #[must_use]
725    pub const fn with_receiver(mut self, receiver: Address) -> Self {
726        self.receiver = Some(receiver);
727        self
728    }
729
730    /// Set an absolute order expiry Unix timestamp.
731    ///
732    /// # Arguments
733    ///
734    /// * `valid_to` — Unix timestamp after which the order expires.
735    ///
736    /// # Returns
737    ///
738    /// The modified [`LimitOrderAdvancedSettings`] with the expiry set.
739    #[must_use]
740    pub const fn with_valid_to(mut self, valid_to: u32) -> Self {
741        self.valid_to = Some(valid_to);
742        self
743    }
744
745    /// Override the partner fee for this limit order.
746    ///
747    /// # Arguments
748    ///
749    /// * `fee` — the [`PartnerFee`](cow_app_data::types::PartnerFee) to apply.
750    ///
751    /// # Returns
752    ///
753    /// The modified [`LimitOrderAdvancedSettings`] with the partner fee set.
754    #[must_use]
755    pub fn with_partner_fee(mut self, fee: cow_app_data::types::PartnerFee) -> Self {
756        self.partner_fee = Some(fee);
757        self
758    }
759
760    /// Override whether the order may be partially filled.
761    ///
762    /// # Arguments
763    ///
764    /// * `partially_fillable` — `true` to allow partial fills.
765    ///
766    /// # Returns
767    ///
768    /// The modified [`LimitOrderAdvancedSettings`] with the flag set.
769    #[must_use]
770    pub const fn with_partially_fillable(mut self, partially_fillable: bool) -> Self {
771        self.partially_fillable = Some(partially_fillable);
772        self
773    }
774
775    /// Override the pre-computed app-data hash (`0x`-prefixed `bytes32`).
776    ///
777    /// # Arguments
778    ///
779    /// * `app_data` — `0x`-prefixed 32-byte hex string of the app-data hash.
780    ///
781    /// # Returns
782    ///
783    /// The modified [`LimitOrderAdvancedSettings`] with the app-data hash set.
784    #[must_use]
785    pub fn with_app_data(mut self, app_data: impl Into<String>) -> Self {
786        self.app_data = Some(app_data.into());
787        self
788    }
789
790    /// Returns `true` if a receiver override is set.
791    ///
792    /// # Returns
793    ///
794    /// `true` when `receiver` is `Some`.
795    #[must_use]
796    pub const fn has_receiver(&self) -> bool {
797        self.receiver.is_some()
798    }
799
800    /// Returns `true` if an absolute expiry timestamp override is set.
801    ///
802    /// # Returns
803    ///
804    /// `true` when `valid_to` is `Some`.
805    #[must_use]
806    pub const fn has_valid_to(&self) -> bool {
807        self.valid_to.is_some()
808    }
809
810    /// Returns `true` if a partner fee override is set.
811    ///
812    /// # Returns
813    ///
814    /// `true` when `partner_fee` is `Some`.
815    #[must_use]
816    pub const fn has_partner_fee(&self) -> bool {
817        self.partner_fee.is_some()
818    }
819
820    /// Returns `true` if a partially-fillable override is set.
821    ///
822    /// # Returns
823    ///
824    /// `true` when `partially_fillable` is `Some`.
825    #[must_use]
826    pub const fn has_partially_fillable(&self) -> bool {
827        self.partially_fillable.is_some()
828    }
829
830    /// Returns `true` if a pre-computed app-data override is set.
831    ///
832    /// # Returns
833    ///
834    /// `true` when `app_data` is `Some`.
835    #[must_use]
836    pub const fn has_app_data(&self) -> bool {
837        self.app_data.is_some()
838    }
839}
840
841impl fmt::Display for LimitOrderAdvancedSettings {
842    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
843        f.write_str("limit-settings")
844    }
845}
846
847/// Apply [`LimitOrderAdvancedSettings`] overrides to limit order parameters.
848///
849/// When `settings` is `None`, `params` is returned unchanged.  Only fields
850/// that are `Some` in `settings` replace the corresponding fields in `params`.
851///
852/// Mirrors `applySettingsToLimitTradeParameters` from the `TypeScript` SDK.
853///
854/// # Arguments
855///
856/// * `params` — the base limit trade parameters to modify.
857/// * `settings` — optional overrides; when `None`, `params` is returned as-is.
858///
859/// # Returns
860///
861/// The updated [`LimitTradeParameters`] with any overrides applied.
862///
863/// # Example
864///
865/// ```no_run
866/// use alloy_primitives::{Address, U256};
867/// use cow_trading::{
868///     LimitOrderAdvancedSettings, LimitTradeParameters, apply_settings_to_limit_trade_parameters,
869/// };
870/// use cow_types::OrderKind;
871///
872/// let params = LimitTradeParameters {
873///     kind: OrderKind::Sell,
874///     sell_token: Address::ZERO,
875///     buy_token: Address::ZERO,
876///     sell_amount: U256::from(1_000u32),
877///     buy_amount: U256::from(900u32),
878///     receiver: None,
879///     valid_for: None,
880///     valid_to: None,
881///     partially_fillable: false,
882///     app_data: None,
883///     partner_fee: None,
884/// };
885///
886/// let settings = LimitOrderAdvancedSettings {
887///     receiver: Some(Address::ZERO),
888///     partially_fillable: Some(true),
889///     ..LimitOrderAdvancedSettings::default()
890/// };
891///
892/// let updated = apply_settings_to_limit_trade_parameters(params, Some(&settings));
893/// assert_eq!(updated.receiver, Some(Address::ZERO));
894/// assert!(updated.partially_fillable);
895/// ```
896#[must_use]
897pub fn apply_settings_to_limit_trade_parameters(
898    mut params: LimitTradeParameters,
899    settings: Option<&LimitOrderAdvancedSettings>,
900) -> LimitTradeParameters {
901    let Some(s) = settings else {
902        return params;
903    };
904    if s.receiver.is_some() {
905        params.receiver = s.receiver;
906    }
907    if s.valid_to.is_some() {
908        params.valid_to = s.valid_to;
909    }
910    if s.partner_fee.is_some() {
911        params.partner_fee = s.partner_fee.clone();
912    }
913    if let Some(pf) = s.partially_fillable {
914        params.partially_fillable = pf;
915    }
916    if s.app_data.is_some() {
917        params.app_data = s.app_data.clone();
918    }
919    params
920}
921
922/// Simplified limit order parameters derived directly from a quote response.
923///
924/// Mirrors `LimitTradeParametersFromQuote` from the `TypeScript` SDK.
925/// Use [`TradingSdk::post_limit_order`](crate::TradingSdk::post_limit_order) with the
926/// full [`LimitTradeParameters`] when you need to set receiver, validity, or other options.
927#[derive(Debug, Clone)]
928pub struct LimitTradeParametersFromQuote {
929    /// Token to sell.
930    pub sell_token: Address,
931    /// Token to buy.
932    pub buy_token: Address,
933    /// Amount to sell (from quote, in atoms).
934    pub sell_amount: U256,
935    /// Amount to buy (from quote, in atoms).
936    pub buy_amount: U256,
937    /// Quote ID returned by the orderbook (for analytics).
938    pub quote_id: Option<i64>,
939}
940
941impl LimitTradeParametersFromQuote {
942    /// Construct from the essential quote fields.
943    ///
944    /// # Arguments
945    ///
946    /// * `sell_token` — address of the token to sell.
947    /// * `buy_token` — address of the token to buy.
948    /// * `sell_amount` — sell amount from the quote (in atoms).
949    /// * `buy_amount` — buy amount from the quote (in atoms).
950    ///
951    /// # Returns
952    ///
953    /// A new [`LimitTradeParametersFromQuote`] with `quote_id` set to `None`.
954    #[must_use]
955    pub const fn new(
956        sell_token: Address,
957        buy_token: Address,
958        sell_amount: U256,
959        buy_amount: U256,
960    ) -> Self {
961        Self { sell_token, buy_token, sell_amount, buy_amount, quote_id: None }
962    }
963
964    /// Attach a quote ID for analytics.
965    ///
966    /// # Arguments
967    ///
968    /// * `quote_id` — the quote identifier returned by the orderbook.
969    ///
970    /// # Returns
971    ///
972    /// The modified [`LimitTradeParametersFromQuote`] with the quote ID set.
973    #[must_use]
974    pub const fn with_quote_id(mut self, quote_id: i64) -> Self {
975        self.quote_id = Some(quote_id);
976        self
977    }
978
979    /// Returns `true` if a quote ID is attached.
980    ///
981    /// # Returns
982    ///
983    /// `true` when `quote_id` is `Some`.
984    #[must_use]
985    pub const fn has_quote_id(&self) -> bool {
986        self.quote_id.is_some()
987    }
988}
989
990impl fmt::Display for LimitTradeParametersFromQuote {
991    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
992        write!(
993            f,
994            "limit-from-quote {:#x} sell={} buy\u{2265}{}",
995            self.sell_token, self.sell_amount, self.buy_amount
996        )
997    }
998}
999
1000/// Parameters for requesting a swap quote.
1001#[derive(Debug, Clone)]
1002pub struct TradeParameters {
1003    /// Sell or buy direction.
1004    pub kind: OrderKind,
1005    /// Token to sell.
1006    pub sell_token: Address,
1007    /// Decimal places of `sell_token`.
1008    pub sell_token_decimals: u8,
1009    /// Token to buy.
1010    pub buy_token: Address,
1011    /// Decimal places of `buy_token`.
1012    pub buy_token_decimals: u8,
1013    /// Amount in atoms: sell amount for `kind = Sell`, buy amount for `kind = Buy`.
1014    pub amount: U256,
1015    /// Slippage tolerance in basis points.  Defaults to 50 (0.5 %).
1016    pub slippage_bps: Option<u32>,
1017    /// Override for the order receiver (defaults to the signer's address).
1018    pub receiver: Option<Address>,
1019    /// Relative order TTL in seconds.  Defaults to 1800 (30 min).
1020    ///
1021    /// Mutually exclusive with `valid_to`. When both are `Some`, `valid_to` takes precedence.
1022    pub valid_for: Option<u32>,
1023    /// Absolute order expiry as a Unix timestamp.
1024    ///
1025    /// When set, overrides `valid_for`. Mirrors `TradeOptionalParameters.validTo` from the TS SDK.
1026    pub valid_to: Option<u32>,
1027    /// Whether the order may be partially filled.
1028    ///
1029    /// Defaults to `false` (fill-or-kill). Mirrors `TradeOptionalParameters.partiallyFillable`.
1030    pub partially_fillable: Option<bool>,
1031    /// Per-trade partner fee override.
1032    ///
1033    /// When set, this fee policy is embedded in the order's app-data for this trade only,
1034    /// overriding any partner fee configured at the [`crate::TradingSdkConfig`] level.
1035    pub partner_fee: Option<cow_app_data::types::PartnerFee>,
1036}
1037
1038impl TradeParameters {
1039    /// Construct a **sell** quote request: sell exactly `amount` of `sell_token`.
1040    ///
1041    /// # Arguments
1042    ///
1043    /// * `sell_token` — address of the token to sell.
1044    /// * `sell_token_decimals` — decimal places of `sell_token`.
1045    /// * `buy_token` — address of the token to buy.
1046    /// * `buy_token_decimals` — decimal places of `buy_token`.
1047    /// * `amount` — exact sell amount in atoms.
1048    ///
1049    /// # Returns
1050    ///
1051    /// A new [`TradeParameters`] configured for a sell order with no optional overrides.
1052    #[must_use]
1053    pub const fn sell(
1054        sell_token: Address,
1055        sell_token_decimals: u8,
1056        buy_token: Address,
1057        buy_token_decimals: u8,
1058        amount: U256,
1059    ) -> Self {
1060        Self {
1061            kind: OrderKind::Sell,
1062            sell_token,
1063            sell_token_decimals,
1064            buy_token,
1065            buy_token_decimals,
1066            amount,
1067            slippage_bps: None,
1068            receiver: None,
1069            valid_for: None,
1070            valid_to: None,
1071            partially_fillable: None,
1072            partner_fee: None,
1073        }
1074    }
1075
1076    /// Construct a **buy** quote request: receive exactly `amount` of `buy_token`.
1077    ///
1078    /// # Arguments
1079    ///
1080    /// * `sell_token` — address of the token to sell.
1081    /// * `sell_token_decimals` — decimal places of `sell_token`.
1082    /// * `buy_token` — address of the token to buy.
1083    /// * `buy_token_decimals` — decimal places of `buy_token`.
1084    /// * `amount` — exact buy amount in atoms.
1085    ///
1086    /// # Returns
1087    ///
1088    /// A new [`TradeParameters`] configured for a buy order with no optional overrides.
1089    #[must_use]
1090    pub const fn buy(
1091        sell_token: Address,
1092        sell_token_decimals: u8,
1093        buy_token: Address,
1094        buy_token_decimals: u8,
1095        amount: U256,
1096    ) -> Self {
1097        Self {
1098            kind: OrderKind::Buy,
1099            sell_token,
1100            sell_token_decimals,
1101            buy_token,
1102            buy_token_decimals,
1103            amount,
1104            slippage_bps: None,
1105            receiver: None,
1106            valid_for: None,
1107            valid_to: None,
1108            partially_fillable: None,
1109            partner_fee: None,
1110        }
1111    }
1112
1113    /// Override the slippage tolerance in basis points.
1114    ///
1115    /// # Arguments
1116    ///
1117    /// * `bps` — slippage tolerance in basis points (e.g. `50` for 0.5%).
1118    ///
1119    /// # Returns
1120    ///
1121    /// The modified [`TradeParameters`] with the slippage override set.
1122    #[must_use]
1123    pub const fn with_slippage_bps(mut self, bps: u32) -> Self {
1124        self.slippage_bps = Some(bps);
1125        self
1126    }
1127
1128    /// Override the order receiver.
1129    ///
1130    /// # Arguments
1131    ///
1132    /// * `receiver` — the address that will receive the bought tokens.
1133    ///
1134    /// # Returns
1135    ///
1136    /// The modified [`TradeParameters`] with the receiver set.
1137    #[must_use]
1138    pub const fn with_receiver(mut self, receiver: Address) -> Self {
1139        self.receiver = Some(receiver);
1140        self
1141    }
1142
1143    /// Set a relative validity window in seconds.
1144    ///
1145    /// # Arguments
1146    ///
1147    /// * `secs` — order time-to-live in seconds.
1148    ///
1149    /// # Returns
1150    ///
1151    /// The modified [`TradeParameters`] with the validity window set.
1152    #[must_use]
1153    pub const fn with_valid_for(mut self, secs: u32) -> Self {
1154        self.valid_for = Some(secs);
1155        self
1156    }
1157
1158    /// Set an absolute expiry Unix timestamp.
1159    ///
1160    /// # Arguments
1161    ///
1162    /// * `ts` — Unix timestamp after which the order expires.
1163    ///
1164    /// # Returns
1165    ///
1166    /// The modified [`TradeParameters`] with the expiry set.
1167    #[must_use]
1168    pub const fn with_valid_to(mut self, ts: u32) -> Self {
1169        self.valid_to = Some(ts);
1170        self
1171    }
1172
1173    /// Allow partial fills.
1174    ///
1175    /// # Returns
1176    ///
1177    /// The modified [`TradeParameters`] with `partially_fillable` set to `true`.
1178    #[must_use]
1179    pub const fn with_partially_fillable(mut self) -> Self {
1180        self.partially_fillable = Some(true);
1181        self
1182    }
1183
1184    /// Returns `true` if this is a sell-direction trade.
1185    ///
1186    /// # Returns
1187    ///
1188    /// `true` when `kind` is [`OrderKind::Sell`].
1189    #[must_use]
1190    pub const fn is_sell(&self) -> bool {
1191        self.kind.is_sell()
1192    }
1193
1194    /// Returns `true` if this is a buy-direction trade.
1195    ///
1196    /// # Returns
1197    ///
1198    /// `true` when `kind` is [`OrderKind::Buy`].
1199    #[must_use]
1200    pub const fn is_buy(&self) -> bool {
1201        self.kind.is_buy()
1202    }
1203
1204    /// Returns `true` if a slippage tolerance override is set.
1205    ///
1206    /// # Returns
1207    ///
1208    /// `true` when `slippage_bps` is `Some`.
1209    #[must_use]
1210    pub const fn has_slippage_bps(&self) -> bool {
1211        self.slippage_bps.is_some()
1212    }
1213
1214    /// Returns `true` if a receiver override is set.
1215    ///
1216    /// # Returns
1217    ///
1218    /// `true` when `receiver` is `Some`.
1219    #[must_use]
1220    pub const fn has_receiver(&self) -> bool {
1221        self.receiver.is_some()
1222    }
1223
1224    /// Returns `true` if a partner fee override is set.
1225    ///
1226    /// # Returns
1227    ///
1228    /// `true` when `partner_fee` is `Some`.
1229    #[must_use]
1230    pub const fn has_partner_fee(&self) -> bool {
1231        self.partner_fee.is_some()
1232    }
1233}
1234
1235impl fmt::Display for TradeParameters {
1236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1237        write!(
1238            f,
1239            "{} {:#x} \u{2192} {:#x} amt={}",
1240            self.kind, self.sell_token, self.buy_token, self.amount
1241        )
1242    }
1243}
1244
1245impl LimitTradeParameters {
1246    /// Construct a limit **sell** order: sell exactly `sell_amount`, receive at least `buy_amount`.
1247    ///
1248    /// # Arguments
1249    ///
1250    /// * `sell_token` — address of the token to sell.
1251    /// * `buy_token` — address of the token to buy.
1252    /// * `sell_amount` — exact sell amount in atoms.
1253    /// * `buy_amount` — minimum buy amount in atoms.
1254    ///
1255    /// # Returns
1256    ///
1257    /// A new [`LimitTradeParameters`] configured for a sell limit order.
1258    #[must_use]
1259    pub const fn sell(
1260        sell_token: Address,
1261        buy_token: Address,
1262        sell_amount: U256,
1263        buy_amount: U256,
1264    ) -> Self {
1265        Self {
1266            kind: cow_types::OrderKind::Sell,
1267            sell_token,
1268            buy_token,
1269            sell_amount,
1270            buy_amount,
1271            receiver: None,
1272            valid_for: None,
1273            valid_to: None,
1274            partially_fillable: false,
1275            app_data: None,
1276            partner_fee: None,
1277        }
1278    }
1279
1280    /// Construct a limit **buy** order: receive exactly `buy_amount`, spend at most `sell_amount`.
1281    ///
1282    /// # Arguments
1283    ///
1284    /// * `sell_token` — address of the token to sell.
1285    /// * `buy_token` — address of the token to buy.
1286    /// * `sell_amount` — maximum sell amount in atoms.
1287    /// * `buy_amount` — exact buy amount in atoms.
1288    ///
1289    /// # Returns
1290    ///
1291    /// A new [`LimitTradeParameters`] configured for a buy limit order.
1292    #[must_use]
1293    pub const fn buy(
1294        sell_token: Address,
1295        buy_token: Address,
1296        sell_amount: U256,
1297        buy_amount: U256,
1298    ) -> Self {
1299        Self {
1300            kind: cow_types::OrderKind::Buy,
1301            sell_token,
1302            buy_token,
1303            sell_amount,
1304            buy_amount,
1305            receiver: None,
1306            valid_for: None,
1307            valid_to: None,
1308            partially_fillable: false,
1309            app_data: None,
1310            partner_fee: None,
1311        }
1312    }
1313
1314    /// Override the order receiver.
1315    ///
1316    /// # Arguments
1317    ///
1318    /// * `receiver` — the address that will receive the bought tokens.
1319    ///
1320    /// # Returns
1321    ///
1322    /// The modified [`LimitTradeParameters`] with the receiver set.
1323    #[must_use]
1324    pub const fn with_receiver(mut self, receiver: Address) -> Self {
1325        self.receiver = Some(receiver);
1326        self
1327    }
1328
1329    /// Set a relative validity window in seconds.
1330    ///
1331    /// # Arguments
1332    ///
1333    /// * `secs` — order time-to-live in seconds.
1334    ///
1335    /// # Returns
1336    ///
1337    /// The modified [`LimitTradeParameters`] with the validity window set.
1338    #[must_use]
1339    pub const fn with_valid_for(mut self, secs: u32) -> Self {
1340        self.valid_for = Some(secs);
1341        self
1342    }
1343
1344    /// Set an absolute expiry Unix timestamp.
1345    ///
1346    /// # Arguments
1347    ///
1348    /// * `ts` — Unix timestamp after which the order expires.
1349    ///
1350    /// # Returns
1351    ///
1352    /// The modified [`LimitTradeParameters`] with the expiry set.
1353    #[must_use]
1354    pub const fn with_valid_to(mut self, ts: u32) -> Self {
1355        self.valid_to = Some(ts);
1356        self
1357    }
1358
1359    /// Allow partial fills.
1360    ///
1361    /// # Returns
1362    ///
1363    /// The modified [`LimitTradeParameters`] with `partially_fillable` set to `true`.
1364    #[must_use]
1365    pub const fn with_partially_fillable(mut self) -> Self {
1366        self.partially_fillable = true;
1367        self
1368    }
1369
1370    /// Returns `true` if this is a sell-direction limit order.
1371    ///
1372    /// # Returns
1373    ///
1374    /// `true` when `kind` is [`OrderKind::Sell`].
1375    #[must_use]
1376    pub const fn is_sell(&self) -> bool {
1377        self.kind.is_sell()
1378    }
1379
1380    /// Returns `true` if this is a buy-direction limit order.
1381    ///
1382    /// # Returns
1383    ///
1384    /// `true` when `kind` is [`OrderKind::Buy`].
1385    #[must_use]
1386    pub const fn is_buy(&self) -> bool {
1387        self.kind.is_buy()
1388    }
1389
1390    /// Returns `true` if a receiver override is set.
1391    ///
1392    /// # Returns
1393    ///
1394    /// `true` when `receiver` is `Some`.
1395    #[must_use]
1396    pub const fn has_receiver(&self) -> bool {
1397        self.receiver.is_some()
1398    }
1399
1400    /// Returns `true` if an absolute expiry timestamp override is set.
1401    ///
1402    /// # Returns
1403    ///
1404    /// `true` when `valid_to` is `Some`.
1405    #[must_use]
1406    pub const fn has_valid_to(&self) -> bool {
1407        self.valid_to.is_some()
1408    }
1409
1410    /// Returns `true` if a relative validity window is set.
1411    ///
1412    /// # Returns
1413    ///
1414    /// `true` when `valid_for` is `Some`.
1415    #[must_use]
1416    pub const fn has_valid_for(&self) -> bool {
1417        self.valid_for.is_some()
1418    }
1419
1420    /// Returns `true` if a pre-computed app-data override is set.
1421    ///
1422    /// # Returns
1423    ///
1424    /// `true` when `app_data` is `Some`.
1425    #[must_use]
1426    pub const fn has_app_data(&self) -> bool {
1427        self.app_data.is_some()
1428    }
1429
1430    /// Returns `true` if a partner fee override is set.
1431    ///
1432    /// # Returns
1433    ///
1434    /// `true` when `partner_fee` is `Some`.
1435    #[must_use]
1436    pub const fn has_partner_fee(&self) -> bool {
1437        self.partner_fee.is_some()
1438    }
1439}
1440
1441impl fmt::Display for LimitTradeParameters {
1442    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1443        write!(
1444            f,
1445            "limit {} {:#x} sell={} buy\u{2265}{}",
1446            self.kind, self.sell_token, self.sell_amount, self.buy_amount
1447        )
1448    }
1449}
1450
1451impl QuoteAmountsAndCosts {
1452    /// Returns `true` for buy orders.
1453    ///
1454    /// This is the logical complement of the `is_sell` field.
1455    ///
1456    /// # Returns
1457    ///
1458    /// `true` when `is_sell` is `false`.
1459    #[must_use]
1460    pub const fn is_buy(&self) -> bool {
1461        !self.is_sell
1462    }
1463
1464    /// Slippage tolerance in buy-token atoms.
1465    ///
1466    /// Computed as the difference between the buy amount before slippage is
1467    /// applied and the final after-slippage buy amount. Returns `U256::ZERO`
1468    /// when `after_partner_fees.buy_amount < after_slippage.buy_amount`
1469    /// (saturating subtract prevents underflow).
1470    ///
1471    /// # Returns
1472    ///
1473    /// The maximum slippage as a `U256` amount in buy-token atoms.
1474    #[must_use]
1475    pub const fn max_slippage_atoms(&self) -> U256 {
1476        self.after_partner_fees.buy_amount.saturating_sub(self.after_slippage.buy_amount)
1477    }
1478
1479    /// Total fees in sell-token atoms (network + partner + protocol).
1480    ///
1481    /// # Returns
1482    ///
1483    /// The saturating sum of network, partner, and protocol fee amounts.
1484    #[must_use]
1485    pub const fn total_fees_atoms(&self) -> U256 {
1486        self.network_fee
1487            .amount_in_sell_currency
1488            .saturating_add(self.partner_fee.amount)
1489            .saturating_add(self.protocol_fee.amount)
1490    }
1491
1492    /// Returns `true` if both network fee components are non-zero.
1493    ///
1494    /// # Returns
1495    ///
1496    /// `true` when at least one component of `network_fee` is non-zero.
1497    #[must_use]
1498    pub fn has_network_fee(&self) -> bool {
1499        !self.network_fee.is_zero()
1500    }
1501
1502    /// Returns `true` if the partner fee is non-zero.
1503    ///
1504    /// # Returns
1505    ///
1506    /// `true` when the partner fee amount or bps is non-zero.
1507    #[must_use]
1508    pub fn has_partner_fee(&self) -> bool {
1509        !self.partner_fee.is_zero()
1510    }
1511
1512    /// Returns `true` if the protocol fee is non-zero.
1513    ///
1514    /// # Returns
1515    ///
1516    /// `true` when the protocol fee amount or bps is non-zero.
1517    #[must_use]
1518    pub fn has_protocol_fee(&self) -> bool {
1519        !self.protocol_fee.is_zero()
1520    }
1521}
1522
1523impl fmt::Display for QuoteAmountsAndCosts {
1524    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1525        let dir = if self.is_sell { "sell" } else { "buy" };
1526        write!(
1527            f,
1528            "{dir} gross={} after-slippage={} [{} / {} / {}]",
1529            self.before_all_fees,
1530            self.after_slippage,
1531            self.network_fee,
1532            self.partner_fee,
1533            self.protocol_fee,
1534        )
1535    }
1536}
1537
1538/// Apply a mapping function to every amount in a [`QuoteAmountsAndCosts`].
1539///
1540/// Useful for converting between token denominators or scaling amounts.
1541/// Mirrors `mapQuoteAmountsAndCosts` from the `TypeScript` SDK.
1542///
1543/// # Arguments
1544///
1545/// * `costs` — the original quote amounts and costs to transform.
1546/// * `f` — a closure applied to each `U256` amount field; fee `bps` values are preserved.
1547///
1548/// # Returns
1549///
1550/// A new [`QuoteAmountsAndCosts`] with every amount field mapped through `f`.
1551///
1552/// # Example
1553///
1554/// ```
1555/// use alloy_primitives::U256;
1556/// use cow_trading::{
1557///     Amounts, NetworkFee, PartnerFeeCost, ProtocolFeeCost, QuoteAmountsAndCosts,
1558///     map_quote_amounts_and_costs,
1559/// };
1560///
1561/// let costs = QuoteAmountsAndCosts {
1562///     is_sell: true,
1563///     before_all_fees: Amounts {
1564///         sell_amount: U256::from(200u32),
1565///         buy_amount: U256::from(110u32),
1566///     },
1567///     before_network_costs: Amounts {
1568///         sell_amount: U256::from(200u32),
1569///         buy_amount: U256::from(100u32),
1570///     },
1571///     after_network_costs: Amounts {
1572///         sell_amount: U256::from(190u32),
1573///         buy_amount: U256::from(100u32),
1574///     },
1575///     after_partner_fees: Amounts {
1576///         sell_amount: U256::from(190u32),
1577///         buy_amount: U256::from(100u32),
1578///     },
1579///     after_slippage: Amounts { sell_amount: U256::from(190u32), buy_amount: U256::from(95u32) },
1580///     network_fee: NetworkFee {
1581///         amount_in_sell_currency: U256::from(10u32),
1582///         amount_in_buy_currency: U256::ZERO,
1583///     },
1584///     partner_fee: PartnerFeeCost { amount: U256::ZERO, bps: 0 },
1585///     protocol_fee: ProtocolFeeCost { amount: U256::ZERO, bps: 0 },
1586/// };
1587///
1588/// // Double all amounts
1589/// let doubled = map_quote_amounts_and_costs(&costs, |a| a * U256::from(2u32));
1590/// assert_eq!(doubled.before_network_costs.sell_amount, U256::from(400u32));
1591/// ```
1592#[must_use]
1593pub fn map_quote_amounts_and_costs<F>(
1594    costs: &QuoteAmountsAndCosts,
1595    mut f: F,
1596) -> QuoteAmountsAndCosts
1597where
1598    F: FnMut(U256) -> U256,
1599{
1600    QuoteAmountsAndCosts {
1601        is_sell: costs.is_sell,
1602        before_all_fees: Amounts {
1603            sell_amount: f(costs.before_all_fees.sell_amount),
1604            buy_amount: f(costs.before_all_fees.buy_amount),
1605        },
1606        before_network_costs: Amounts {
1607            sell_amount: f(costs.before_network_costs.sell_amount),
1608            buy_amount: f(costs.before_network_costs.buy_amount),
1609        },
1610        after_network_costs: Amounts {
1611            sell_amount: f(costs.after_network_costs.sell_amount),
1612            buy_amount: f(costs.after_network_costs.buy_amount),
1613        },
1614        after_partner_fees: Amounts {
1615            sell_amount: f(costs.after_partner_fees.sell_amount),
1616            buy_amount: f(costs.after_partner_fees.buy_amount),
1617        },
1618        after_slippage: Amounts {
1619            sell_amount: f(costs.after_slippage.sell_amount),
1620            buy_amount: f(costs.after_slippage.buy_amount),
1621        },
1622        network_fee: NetworkFee {
1623            amount_in_sell_currency: f(costs.network_fee.amount_in_sell_currency),
1624            amount_in_buy_currency: f(costs.network_fee.amount_in_buy_currency),
1625        },
1626        partner_fee: PartnerFeeCost {
1627            amount: f(costs.partner_fee.amount),
1628            bps: costs.partner_fee.bps,
1629        },
1630        protocol_fee: ProtocolFeeCost {
1631            amount: f(costs.protocol_fee.amount),
1632            bps: costs.protocol_fee.bps,
1633        },
1634    }
1635}
1636
1637/// Parameters for a limit order (fixed price, no slippage).
1638///
1639/// Unlike [`TradeParameters`], the amounts here are exact — the order is
1640/// submitted as-is without slippage adjustment. Use `TradingSdk::post_limit_order`
1641/// to sign and submit.
1642#[derive(Debug, Clone)]
1643pub struct LimitTradeParameters {
1644    /// Sell or buy direction.
1645    pub kind: cow_types::OrderKind,
1646    /// Token to sell.
1647    pub sell_token: Address,
1648    /// Token to buy.
1649    pub buy_token: Address,
1650    /// Amount to sell (exact, in atoms, for `kind = Sell`).
1651    pub sell_amount: U256,
1652    /// Amount to buy (minimum, in atoms, for `kind = Sell`; exact for `kind = Buy`).
1653    pub buy_amount: U256,
1654    /// Override for the order receiver (defaults to the signer's address).
1655    pub receiver: Option<Address>,
1656    /// Order TTL in seconds. Defaults to [`super::sdk::DEFAULT_QUOTE_VALIDITY`].
1657    ///
1658    /// Ignored when `valid_to` is `Some`.
1659    pub valid_for: Option<u32>,
1660    /// Absolute order expiry as a Unix timestamp.
1661    ///
1662    /// When set, overrides `valid_for`.
1663    pub valid_to: Option<u32>,
1664    /// Whether the order may be partially filled.
1665    pub partially_fillable: bool,
1666    /// Pre-computed app-data hash (hex, `0x`-prefixed `bytes32`).
1667    /// Uses `0x000…0` when `None`.
1668    pub app_data: Option<String>,
1669    /// Per-trade partner fee override.
1670    ///
1671    /// When set, replaces any partner fee configured at the [`crate::TradingSdkConfig`] level.
1672    pub partner_fee: Option<cow_app_data::types::PartnerFee>,
1673}
1674
1675/// The result of a successful order submission.
1676///
1677/// Mirrors `OrderPostingResult` from the `TypeScript` SDK — bundles the order
1678/// UID with the signing details used to place it.
1679#[derive(Debug, Clone)]
1680pub struct OrderPostingResult {
1681    /// The unique order identifier returned by `POST /api/v1/orders`.
1682    pub order_id: String,
1683    /// The signing scheme used.
1684    pub signing_scheme: cow_types::SigningScheme,
1685    /// Hex-encoded signature (format depends on `signing_scheme`).
1686    pub signature: String,
1687    /// The order struct that was signed.
1688    pub order_to_sign: UnsignedOrder,
1689}
1690
1691impl OrderPostingResult {
1692    /// Construct an [`OrderPostingResult`] from the four fields returned by order submission.
1693    ///
1694    /// # Arguments
1695    ///
1696    /// * `order_id` — the unique order identifier from `POST /api/v1/orders`.
1697    /// * `signing_scheme` — the [`SigningScheme`](cow_types::SigningScheme) used.
1698    /// * `signature` — hex-encoded signature string.
1699    /// * `order_to_sign` — the order struct that was signed.
1700    ///
1701    /// # Returns
1702    ///
1703    /// A new [`OrderPostingResult`] instance.
1704    #[must_use]
1705    pub fn new(
1706        order_id: impl Into<String>,
1707        signing_scheme: cow_types::SigningScheme,
1708        signature: impl Into<String>,
1709        order_to_sign: UnsignedOrder,
1710    ) -> Self {
1711        Self {
1712            order_id: order_id.into(),
1713            signing_scheme,
1714            signature: signature.into(),
1715            order_to_sign,
1716        }
1717    }
1718
1719    /// Returns `true` if the order was signed with `EIP-712`.
1720    ///
1721    /// # Returns
1722    ///
1723    /// `true` when `signing_scheme` is
1724    /// [`SigningScheme::Eip712`](cow_types::SigningScheme::Eip712).
1725    #[must_use]
1726    pub const fn is_eip712(&self) -> bool {
1727        matches!(self.signing_scheme, cow_types::SigningScheme::Eip712)
1728    }
1729
1730    /// Returns `true` if the order was signed with `eth_sign` (`EIP-191`).
1731    ///
1732    /// # Returns
1733    ///
1734    /// `true` when `signing_scheme` is
1735    /// [`SigningScheme::EthSign`](cow_types::SigningScheme::EthSign).
1736    #[must_use]
1737    pub const fn is_eth_sign(&self) -> bool {
1738        matches!(self.signing_scheme, cow_types::SigningScheme::EthSign)
1739    }
1740
1741    /// Returns `true` if the order was signed with `EIP-1271` (smart-contract signature).
1742    ///
1743    /// # Returns
1744    ///
1745    /// `true` when `signing_scheme` is
1746    /// [`SigningScheme::Eip1271`](cow_types::SigningScheme::Eip1271).
1747    #[must_use]
1748    pub const fn is_eip1271(&self) -> bool {
1749        matches!(self.signing_scheme, cow_types::SigningScheme::Eip1271)
1750    }
1751
1752    /// Returns `true` if the order uses a pre-signature (on-chain sign-later flow).
1753    ///
1754    /// # Returns
1755    ///
1756    /// `true` when `signing_scheme` is
1757    /// [`SigningScheme::PreSign`](cow_types::SigningScheme::PreSign).
1758    #[must_use]
1759    pub const fn is_presign(&self) -> bool {
1760        matches!(self.signing_scheme, cow_types::SigningScheme::PreSign)
1761    }
1762
1763    /// Returns the order UID string.
1764    ///
1765    /// # Returns
1766    ///
1767    /// A `&str` reference to the order identifier.
1768    #[must_use]
1769    pub fn order_id_ref(&self) -> &str {
1770        &self.order_id
1771    }
1772
1773    /// Returns the hex-encoded signature string.
1774    ///
1775    /// # Returns
1776    ///
1777    /// A `&str` reference to the hex-encoded signature.
1778    #[must_use]
1779    pub fn signature_ref(&self) -> &str {
1780        &self.signature
1781    }
1782}
1783
1784impl fmt::Display for OrderPostingResult {
1785    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1786        write!(f, "order({})", self.order_id)
1787    }
1788}
1789
1790impl fmt::Display for QuoteResults {
1791    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1792        write!(f, "quote slippage={}bps {}", self.suggested_slippage_bps, self.amounts_and_costs)
1793    }
1794}
1795
1796/// The result of a successful quote, bundled with posting capability.
1797#[derive(Debug, Clone)]
1798pub struct QuoteResults {
1799    /// The order struct ready to be signed and submitted.
1800    pub order_to_sign: UnsignedOrder,
1801    /// Full EIP-712 typed-data envelope for the order.
1802    ///
1803    /// Pass this to any EIP-712-aware signer (hardware wallet, `eth_signTypedData_v4`)
1804    /// that needs the structured domain and types alongside the message.
1805    /// Mirrors `QuoteResults.orderTypedData` from the `TypeScript` SDK.
1806    pub order_typed_data: OrderTypedData,
1807    /// Raw quote response from the orderbook.
1808    pub quote_response: OrderQuoteResponse,
1809    /// Detailed fee and amount breakdown.
1810    pub amounts_and_costs: QuoteAmountsAndCosts,
1811    /// Suggested slippage (may differ from the requested value).
1812    pub suggested_slippage_bps: u32,
1813    /// Full app-data document and its `keccak256` hash.
1814    pub app_data_info: TradingAppDataInfo,
1815}
1816
1817impl QuoteResults {
1818    /// Returns a reference to the order ready for signing.
1819    ///
1820    /// # Returns
1821    ///
1822    /// A `&UnsignedOrder` reference to the order struct.
1823    #[must_use]
1824    pub const fn order_ref(&self) -> &UnsignedOrder {
1825        &self.order_to_sign
1826    }
1827
1828    /// Returns a reference to the raw quote response from the orderbook.
1829    ///
1830    /// # Returns
1831    ///
1832    /// A `&OrderQuoteResponse` reference to the raw API response.
1833    #[must_use]
1834    pub const fn quote_ref(&self) -> &OrderQuoteResponse {
1835        &self.quote_response
1836    }
1837}
1838
1839// ── BuildAppDataParams ───────────────────────────────────────────────────────
1840
1841/// Parameters for building the app-data document for a trade.
1842///
1843/// Mirrors `BuildAppDataParams` from the `TypeScript` SDK.
1844#[derive(Debug, Clone)]
1845pub struct BuildAppDataParams {
1846    /// Application code identifying the dApp (e.g. `"CoW Swap"`).
1847    pub app_code: String,
1848    /// Slippage tolerance in basis points.
1849    pub slippage_bps: u32,
1850    /// Order class classification.
1851    pub order_class: cow_app_data::types::OrderClassKind,
1852    /// Optional partner fee to embed in the app-data.
1853    pub partner_fee: Option<cow_app_data::types::PartnerFee>,
1854}
1855
1856impl BuildAppDataParams {
1857    /// Construct a [`BuildAppDataParams`] with the required fields.
1858    ///
1859    /// # Arguments
1860    ///
1861    /// * `app_code` — application code identifying the dApp (e.g. `"CoW Swap"`).
1862    /// * `slippage_bps` — slippage tolerance in basis points.
1863    /// * `order_class` — order class classification for the app-data document.
1864    ///
1865    /// # Returns
1866    ///
1867    /// A new [`BuildAppDataParams`] with `partner_fee` set to `None`.
1868    #[must_use]
1869    pub fn new(
1870        app_code: impl Into<String>,
1871        slippage_bps: u32,
1872        order_class: cow_app_data::types::OrderClassKind,
1873    ) -> Self {
1874        Self { app_code: app_code.into(), slippage_bps, order_class, partner_fee: None }
1875    }
1876
1877    /// Attach a partner fee to embed in the app-data.
1878    ///
1879    /// # Arguments
1880    ///
1881    /// * `fee` — the [`PartnerFee`](cow_app_data::types::PartnerFee) to embed.
1882    ///
1883    /// # Returns
1884    ///
1885    /// The modified [`BuildAppDataParams`] with the partner fee set.
1886    #[must_use]
1887    pub fn with_partner_fee(mut self, fee: cow_app_data::types::PartnerFee) -> Self {
1888        self.partner_fee = Some(fee);
1889        self
1890    }
1891
1892    /// Returns `true` if a partner fee is set.
1893    ///
1894    /// # Returns
1895    ///
1896    /// `true` when `partner_fee` is `Some`.
1897    #[must_use]
1898    pub const fn has_partner_fee(&self) -> bool {
1899        self.partner_fee.is_some()
1900    }
1901}
1902
1903impl fmt::Display for BuildAppDataParams {
1904    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1905        write!(
1906            f,
1907            "build-app-data({}, {}bps, {})",
1908            self.app_code, self.slippage_bps, self.order_class
1909        )
1910    }
1911}
1912
1913// ── SlippageTolerance request/response ───────────────────────────────────────
1914
1915/// Request parameters for a slippage tolerance suggestion.
1916///
1917/// Mirrors `SlippageToleranceRequest` from the `TypeScript` SDK.
1918#[derive(Debug, Clone)]
1919pub struct SlippageToleranceRequest {
1920    /// Chain ID on which the trade will execute.
1921    pub chain_id: u64,
1922    /// Token to sell.
1923    pub sell_token: Address,
1924    /// Token to buy.
1925    pub buy_token: Address,
1926    /// Sell amount in atoms (optional; improves suggestion accuracy).
1927    pub sell_amount: Option<U256>,
1928    /// Buy amount in atoms (optional; improves suggestion accuracy).
1929    pub buy_amount: Option<U256>,
1930}
1931
1932impl SlippageToleranceRequest {
1933    /// Construct a [`SlippageToleranceRequest`] with the required fields.
1934    ///
1935    /// # Arguments
1936    ///
1937    /// * `chain_id` — chain ID on which the trade will execute.
1938    /// * `sell_token` — address of the token to sell.
1939    /// * `buy_token` — address of the token to buy.
1940    ///
1941    /// # Returns
1942    ///
1943    /// A new [`SlippageToleranceRequest`] with optional amounts set to `None`.
1944    #[must_use]
1945    pub const fn new(chain_id: u64, sell_token: Address, buy_token: Address) -> Self {
1946        Self { chain_id, sell_token, buy_token, sell_amount: None, buy_amount: None }
1947    }
1948
1949    /// Attach a sell amount to improve the suggestion.
1950    ///
1951    /// # Arguments
1952    ///
1953    /// * `amount` — sell amount in atoms.
1954    ///
1955    /// # Returns
1956    ///
1957    /// The modified [`SlippageToleranceRequest`] with the sell amount set.
1958    #[must_use]
1959    pub const fn with_sell_amount(mut self, amount: U256) -> Self {
1960        self.sell_amount = Some(amount);
1961        self
1962    }
1963
1964    /// Attach a buy amount to improve the suggestion.
1965    ///
1966    /// # Arguments
1967    ///
1968    /// * `amount` — buy amount in atoms.
1969    ///
1970    /// # Returns
1971    ///
1972    /// The modified [`SlippageToleranceRequest`] with the buy amount set.
1973    #[must_use]
1974    pub const fn with_buy_amount(mut self, amount: U256) -> Self {
1975        self.buy_amount = Some(amount);
1976        self
1977    }
1978}
1979
1980impl fmt::Display for SlippageToleranceRequest {
1981    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1982        write!(
1983            f,
1984            "slippage-req(chain={}, {:#x} -> {:#x})",
1985            self.chain_id, self.sell_token, self.buy_token
1986        )
1987    }
1988}
1989
1990/// Response from a slippage tolerance suggestion.
1991///
1992/// Mirrors `SlippageToleranceResponse` from the `TypeScript` SDK.
1993#[derive(Debug, Clone)]
1994pub struct SlippageToleranceResponse {
1995    /// Suggested slippage in basis points, or `None` if no suggestion is available.
1996    pub slippage_bps: Option<u32>,
1997}
1998
1999impl SlippageToleranceResponse {
2000    /// Construct a [`SlippageToleranceResponse`] with a suggested slippage value.
2001    ///
2002    /// # Arguments
2003    ///
2004    /// * `slippage_bps` — suggested slippage in basis points, or `None` if unavailable.
2005    ///
2006    /// # Returns
2007    ///
2008    /// A new [`SlippageToleranceResponse`] instance.
2009    #[must_use]
2010    pub const fn new(slippage_bps: Option<u32>) -> Self {
2011        Self { slippage_bps }
2012    }
2013
2014    /// Returns `true` if a slippage suggestion is available.
2015    ///
2016    /// # Returns
2017    ///
2018    /// `true` when `slippage_bps` is `Some`.
2019    #[must_use]
2020    pub const fn has_suggestion(&self) -> bool {
2021        self.slippage_bps.is_some()
2022    }
2023}
2024
2025impl fmt::Display for SlippageToleranceResponse {
2026    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2027        match self.slippage_bps {
2028            Some(bps) => write!(f, "slippage-resp({bps}bps)"),
2029            None => f.write_str("slippage-resp(none)"),
2030        }
2031    }
2032}
2033
2034#[cfg(test)]
2035mod tests {
2036    use super::*;
2037
2038    use alloy_primitives::B256;
2039
2040    use cow_app_data::types::{OrderClassKind, PartnerFee, PartnerFeeEntry};
2041    use cow_types::{SigningScheme, TokenBalance};
2042
2043    // ── Amounts ─────────────────────────────────────────────────────────────
2044
2045    #[test]
2046    fn amounts_new_stores_fields() {
2047        let a = Amounts::new(U256::from(100u32), U256::from(200u32));
2048        assert_eq!(a.sell_amount, U256::from(100u32));
2049        assert_eq!(a.buy_amount, U256::from(200u32));
2050    }
2051
2052    #[test]
2053    fn amounts_is_zero() {
2054        assert!(Amounts::default().is_zero());
2055        assert!(Amounts::new(U256::ZERO, U256::ZERO).is_zero());
2056        assert!(!Amounts::new(U256::from(1u32), U256::ZERO).is_zero());
2057        assert!(!Amounts::new(U256::ZERO, U256::from(1u32)).is_zero());
2058    }
2059
2060    #[test]
2061    fn amounts_total() {
2062        let a = Amounts::new(U256::from(100u32), U256::from(90u32));
2063        assert_eq!(a.total(), U256::from(190u32));
2064    }
2065
2066    #[test]
2067    fn amounts_total_saturates() {
2068        let a = Amounts::new(U256::MAX, U256::from(1u32));
2069        assert_eq!(a.total(), U256::MAX);
2070    }
2071
2072    #[test]
2073    fn amounts_display() {
2074        let a = Amounts::new(U256::from(42u32), U256::from(7u32));
2075        let s = format!("{a}");
2076        assert!(s.contains("sell"));
2077        assert!(s.contains("buy"));
2078        assert!(s.contains("42"));
2079        assert!(s.contains('7'));
2080    }
2081
2082    #[test]
2083    fn amounts_default() {
2084        let a = Amounts::default();
2085        assert_eq!(a.sell_amount, U256::ZERO);
2086        assert_eq!(a.buy_amount, U256::ZERO);
2087    }
2088
2089    // ── NetworkFee ──────────────────────────────────────────────────────────
2090
2091    #[test]
2092    fn network_fee_new_stores_fields() {
2093        let nf = NetworkFee::new(U256::from(10u32), U256::from(20u32));
2094        assert_eq!(nf.amount_in_sell_currency, U256::from(10u32));
2095        assert_eq!(nf.amount_in_buy_currency, U256::from(20u32));
2096    }
2097
2098    #[test]
2099    fn network_fee_is_zero() {
2100        assert!(NetworkFee::default().is_zero());
2101        assert!(!NetworkFee::new(U256::from(1u32), U256::ZERO).is_zero());
2102        assert!(!NetworkFee::new(U256::ZERO, U256::from(1u32)).is_zero());
2103    }
2104
2105    #[test]
2106    fn network_fee_total_atoms() {
2107        let nf = NetworkFee::new(U256::from(5u32), U256::from(3u32));
2108        assert_eq!(nf.total_atoms(), U256::from(8u32));
2109    }
2110
2111    #[test]
2112    fn network_fee_total_atoms_saturates() {
2113        let nf = NetworkFee::new(U256::MAX, U256::from(1u32));
2114        assert_eq!(nf.total_atoms(), U256::MAX);
2115    }
2116
2117    #[test]
2118    fn network_fee_display() {
2119        let nf = NetworkFee::new(U256::from(10u32), U256::from(20u32));
2120        let s = format!("{nf}");
2121        assert!(s.contains("network-fee"));
2122        assert!(s.contains("10"));
2123        assert!(s.contains("20"));
2124    }
2125
2126    #[test]
2127    fn network_fee_default() {
2128        let nf = NetworkFee::default();
2129        assert_eq!(nf.amount_in_sell_currency, U256::ZERO);
2130        assert_eq!(nf.amount_in_buy_currency, U256::ZERO);
2131    }
2132
2133    // ── PartnerFeeCost ──────────────────────────────────────────────────────
2134
2135    #[test]
2136    fn partner_fee_cost_new() {
2137        let pf = PartnerFeeCost::new(U256::from(500u32), 50);
2138        assert_eq!(pf.amount, U256::from(500u32));
2139        assert_eq!(pf.bps, 50);
2140    }
2141
2142    #[test]
2143    fn partner_fee_cost_is_zero() {
2144        assert!(PartnerFeeCost::default().is_zero());
2145        assert!(!PartnerFeeCost::new(U256::from(1u32), 0).is_zero());
2146        assert!(!PartnerFeeCost::new(U256::ZERO, 1).is_zero());
2147    }
2148
2149    #[test]
2150    fn partner_fee_cost_has_bps() {
2151        assert!(!PartnerFeeCost::default().has_bps());
2152        assert!(PartnerFeeCost::new(U256::ZERO, 10).has_bps());
2153    }
2154
2155    #[test]
2156    fn partner_fee_cost_display() {
2157        let pf = PartnerFeeCost::new(U256::from(99u32), 25);
2158        let s = format!("{pf}");
2159        assert!(s.contains("partner-fee"));
2160        assert!(s.contains("25bps"));
2161    }
2162
2163    #[test]
2164    fn partner_fee_cost_default() {
2165        let pf = PartnerFeeCost::default();
2166        assert_eq!(pf.amount, U256::ZERO);
2167        assert_eq!(pf.bps, 0);
2168    }
2169
2170    // ── ProtocolFeeCost ─────────────────────────────────────────────────────
2171
2172    #[test]
2173    fn protocol_fee_cost_new() {
2174        let pf = ProtocolFeeCost::new(U256::from(300u32), 15);
2175        assert_eq!(pf.amount, U256::from(300u32));
2176        assert_eq!(pf.bps, 15);
2177    }
2178
2179    #[test]
2180    fn protocol_fee_cost_is_zero() {
2181        assert!(ProtocolFeeCost::default().is_zero());
2182        assert!(!ProtocolFeeCost::new(U256::from(1u32), 0).is_zero());
2183        assert!(!ProtocolFeeCost::new(U256::ZERO, 1).is_zero());
2184    }
2185
2186    #[test]
2187    fn protocol_fee_cost_has_bps() {
2188        assert!(!ProtocolFeeCost::default().has_bps());
2189        assert!(ProtocolFeeCost::new(U256::ZERO, 5).has_bps());
2190    }
2191
2192    #[test]
2193    fn protocol_fee_cost_display() {
2194        let pf = ProtocolFeeCost::new(U256::from(77u32), 10);
2195        let s = format!("{pf}");
2196        assert!(s.contains("protocol-fee"));
2197        assert!(s.contains("10bps"));
2198    }
2199
2200    #[test]
2201    fn protocol_fee_cost_default() {
2202        let pf = ProtocolFeeCost::default();
2203        assert_eq!(pf.amount, U256::ZERO);
2204        assert_eq!(pf.bps, 0);
2205    }
2206
2207    // ── TradingAppDataInfo ──────────────────────────────────────────────────
2208
2209    #[test]
2210    fn trading_app_data_info_new() {
2211        let info = TradingAppDataInfo::new("{\"v\":1}", "0xabc");
2212        assert_eq!(info.full_app_data, "{\"v\":1}");
2213        assert_eq!(info.app_data_keccak256, "0xabc");
2214    }
2215
2216    #[test]
2217    fn trading_app_data_info_has_full_app_data() {
2218        let with = TradingAppDataInfo::new("{}", "0x1");
2219        assert!(with.has_full_app_data());
2220
2221        let without = TradingAppDataInfo::new("", "0x1");
2222        assert!(!without.has_full_app_data());
2223    }
2224
2225    #[test]
2226    fn trading_app_data_info_refs() {
2227        let info = TradingAppDataInfo::new("doc", "0xhash");
2228        assert_eq!(info.full_app_data_ref(), "doc");
2229        assert_eq!(info.keccak256_ref(), "0xhash");
2230    }
2231
2232    #[test]
2233    fn trading_app_data_info_display() {
2234        let info = TradingAppDataInfo::new("{}", "0xdeadbeef");
2235        let s = format!("{info}");
2236        assert!(s.contains("app-data"));
2237        assert!(s.contains("0xdeadbeef"));
2238    }
2239
2240    // ── TradingTransactionParams ────────────────────────────────────────────
2241
2242    #[test]
2243    fn trading_tx_params_new() {
2244        let data = vec![0xAA, 0xBB];
2245        let to = Address::ZERO;
2246        let tx = TradingTransactionParams::new(data.clone(), to, 21_000, U256::from(1u32));
2247        assert_eq!(tx.data, data);
2248        assert_eq!(tx.to, to);
2249        assert_eq!(tx.gas_limit, 21_000);
2250        assert_eq!(tx.value, U256::from(1u32));
2251    }
2252
2253    #[test]
2254    fn trading_tx_params_builders() {
2255        let tx = TradingTransactionParams::new(vec![], Address::ZERO, 0, U256::ZERO)
2256            .with_data(vec![1, 2, 3])
2257            .with_to(Address::with_last_byte(0x01))
2258            .with_gas_limit(50_000)
2259            .with_value(U256::from(999u32));
2260
2261        assert_eq!(tx.data, vec![1, 2, 3]);
2262        assert_eq!(tx.to, Address::with_last_byte(0x01));
2263        assert_eq!(tx.gas_limit, 50_000);
2264        assert_eq!(tx.value, U256::from(999u32));
2265    }
2266
2267    #[test]
2268    fn trading_tx_params_data_len() {
2269        let tx = TradingTransactionParams::new(vec![0; 64], Address::ZERO, 0, U256::ZERO);
2270        assert_eq!(tx.data_len(), 64);
2271    }
2272
2273    #[test]
2274    fn trading_tx_params_has_value() {
2275        let no_val = TradingTransactionParams::new(vec![], Address::ZERO, 0, U256::ZERO);
2276        assert!(!no_val.has_value());
2277
2278        let with_val = no_val.with_value(U256::from(1u32));
2279        assert!(with_val.has_value());
2280    }
2281
2282    #[test]
2283    fn trading_tx_params_display() {
2284        let tx = TradingTransactionParams::new(vec![], Address::ZERO, 21_000, U256::ZERO);
2285        let s = format!("{tx}");
2286        assert!(s.contains("tx"));
2287        assert!(s.contains("21000"));
2288    }
2289
2290    // ── PostTradeAdditionalParams ───────────────────────────────────────────
2291
2292    #[test]
2293    fn post_trade_default() {
2294        let p = PostTradeAdditionalParams::default();
2295        assert!(!p.has_signing_scheme());
2296        assert!(!p.has_network_costs());
2297        assert!(!p.should_apply_costs());
2298    }
2299
2300    #[test]
2301    fn post_trade_with_signing_scheme() {
2302        let p = PostTradeAdditionalParams::default().with_signing_scheme(SigningScheme::PreSign);
2303        assert!(p.has_signing_scheme());
2304        assert!(matches!(p.signing_scheme, Some(SigningScheme::PreSign)));
2305    }
2306
2307    #[test]
2308    fn post_trade_with_network_costs_amount() {
2309        let p = PostTradeAdditionalParams::default().with_network_costs_amount("12345");
2310        assert!(p.has_network_costs());
2311        assert_eq!(p.network_costs_amount.as_deref(), Some("12345"));
2312    }
2313
2314    #[test]
2315    fn post_trade_with_apply_costs() {
2316        let p = PostTradeAdditionalParams::default().with_apply_costs_slippage_and_fees(true);
2317        assert!(p.should_apply_costs());
2318
2319        let p2 = PostTradeAdditionalParams::default().with_apply_costs_slippage_and_fees(false);
2320        assert!(!p2.should_apply_costs());
2321    }
2322
2323    #[test]
2324    fn post_trade_display() {
2325        let p = PostTradeAdditionalParams::default();
2326        assert_eq!(format!("{p}"), "post-trade-params");
2327    }
2328
2329    // ── SwapAdvancedSettings ────────────────────────────────────────────────
2330
2331    #[test]
2332    fn swap_settings_default() {
2333        let s = SwapAdvancedSettings::default();
2334        assert!(!s.has_app_data());
2335        assert!(!s.has_slippage_bps());
2336        assert!(!s.has_partner_fee());
2337    }
2338
2339    #[test]
2340    fn swap_settings_with_app_data() {
2341        let s = SwapAdvancedSettings::default().with_app_data(serde_json::json!({"k": "v"}));
2342        assert!(s.has_app_data());
2343    }
2344
2345    #[test]
2346    fn swap_settings_with_slippage_bps() {
2347        let s = SwapAdvancedSettings::default().with_slippage_bps(100);
2348        assert!(s.has_slippage_bps());
2349        assert_eq!(s.slippage_bps, Some(100));
2350    }
2351
2352    #[test]
2353    fn swap_settings_with_partner_fee() {
2354        let fee = PartnerFee::single(PartnerFeeEntry::volume(50, "0xRecipient"));
2355        let s = SwapAdvancedSettings::default().with_partner_fee(fee);
2356        assert!(s.has_partner_fee());
2357    }
2358
2359    #[test]
2360    fn swap_settings_display() {
2361        let s = SwapAdvancedSettings::default();
2362        assert_eq!(format!("{s}"), "swap-settings");
2363    }
2364
2365    // ── LimitOrderAdvancedSettings ──────────────────────────────────────────
2366
2367    #[test]
2368    fn limit_settings_default() {
2369        let s = LimitOrderAdvancedSettings::default();
2370        assert!(!s.has_receiver());
2371        assert!(!s.has_valid_to());
2372        assert!(!s.has_partner_fee());
2373        assert!(!s.has_partially_fillable());
2374        assert!(!s.has_app_data());
2375    }
2376
2377    #[test]
2378    fn limit_settings_with_receiver() {
2379        let addr = Address::with_last_byte(0x42);
2380        let s = LimitOrderAdvancedSettings::default().with_receiver(addr);
2381        assert!(s.has_receiver());
2382        assert_eq!(s.receiver, Some(addr));
2383    }
2384
2385    #[test]
2386    fn limit_settings_with_valid_to() {
2387        let s = LimitOrderAdvancedSettings::default().with_valid_to(1_700_000_000);
2388        assert!(s.has_valid_to());
2389        assert_eq!(s.valid_to, Some(1_700_000_000));
2390    }
2391
2392    #[test]
2393    fn limit_settings_with_partner_fee() {
2394        let fee = PartnerFee::single(PartnerFeeEntry::volume(25, "0xAddr"));
2395        let s = LimitOrderAdvancedSettings::default().with_partner_fee(fee);
2396        assert!(s.has_partner_fee());
2397    }
2398
2399    #[test]
2400    fn limit_settings_with_partially_fillable() {
2401        let s = LimitOrderAdvancedSettings::default().with_partially_fillable(true);
2402        assert!(s.has_partially_fillable());
2403        assert_eq!(s.partially_fillable, Some(true));
2404    }
2405
2406    #[test]
2407    fn limit_settings_with_app_data() {
2408        let s = LimitOrderAdvancedSettings::default().with_app_data("0xabc123");
2409        assert!(s.has_app_data());
2410        assert_eq!(s.app_data.as_deref(), Some("0xabc123"));
2411    }
2412
2413    #[test]
2414    fn limit_settings_display() {
2415        let s = LimitOrderAdvancedSettings::default();
2416        assert_eq!(format!("{s}"), "limit-settings");
2417    }
2418
2419    // ── apply_settings_to_limit_trade_parameters ────────────────────────────
2420
2421    #[test]
2422    fn apply_settings_none_returns_unchanged() {
2423        let params = LimitTradeParameters::sell(
2424            Address::ZERO,
2425            Address::ZERO,
2426            U256::from(1000u32),
2427            U256::from(900u32),
2428        );
2429        let result = apply_settings_to_limit_trade_parameters(params, None);
2430        assert_eq!(result.sell_amount, U256::from(1000u32));
2431        assert!(!result.partially_fillable);
2432    }
2433
2434    #[test]
2435    fn apply_settings_overrides_fields() {
2436        let params = LimitTradeParameters::sell(
2437            Address::ZERO,
2438            Address::ZERO,
2439            U256::from(1000u32),
2440            U256::from(900u32),
2441        );
2442        let settings = LimitOrderAdvancedSettings::default()
2443            .with_receiver(Address::with_last_byte(0x01))
2444            .with_valid_to(9999)
2445            .with_partially_fillable(true)
2446            .with_app_data("0xbeef");
2447
2448        let result = apply_settings_to_limit_trade_parameters(params, Some(&settings));
2449        assert_eq!(result.receiver, Some(Address::with_last_byte(0x01)));
2450        assert_eq!(result.valid_to, Some(9999));
2451        assert!(result.partially_fillable);
2452        assert_eq!(result.app_data.as_deref(), Some("0xbeef"));
2453    }
2454
2455    // ── LimitTradeParametersFromQuote ───────────────────────────────────────
2456
2457    #[test]
2458    fn limit_from_quote_new() {
2459        let p = LimitTradeParametersFromQuote::new(
2460            Address::ZERO,
2461            Address::with_last_byte(1),
2462            U256::from(100u32),
2463            U256::from(90u32),
2464        );
2465        assert_eq!(p.sell_token, Address::ZERO);
2466        assert_eq!(p.buy_token, Address::with_last_byte(1));
2467        assert_eq!(p.sell_amount, U256::from(100u32));
2468        assert_eq!(p.buy_amount, U256::from(90u32));
2469        assert!(!p.has_quote_id());
2470    }
2471
2472    #[test]
2473    fn limit_from_quote_with_quote_id() {
2474        let p = LimitTradeParametersFromQuote::new(
2475            Address::ZERO,
2476            Address::ZERO,
2477            U256::from(1u32),
2478            U256::from(1u32),
2479        )
2480        .with_quote_id(42);
2481        assert!(p.has_quote_id());
2482        assert_eq!(p.quote_id, Some(42));
2483    }
2484
2485    #[test]
2486    fn limit_from_quote_display() {
2487        let p = LimitTradeParametersFromQuote::new(
2488            Address::ZERO,
2489            Address::ZERO,
2490            U256::from(100u32),
2491            U256::from(90u32),
2492        );
2493        let s = format!("{p}");
2494        assert!(s.contains("limit-from-quote"));
2495    }
2496
2497    // ── TradeParameters ─────────────────────────────────────────────────────
2498
2499    #[test]
2500    fn trade_params_sell() {
2501        let p = TradeParameters::sell(
2502            Address::ZERO,
2503            18,
2504            Address::with_last_byte(1),
2505            6,
2506            U256::from(1000u32),
2507        );
2508        assert!(p.is_sell());
2509        assert!(!p.is_buy());
2510        assert_eq!(p.sell_token_decimals, 18);
2511        assert_eq!(p.buy_token_decimals, 6);
2512        assert_eq!(p.amount, U256::from(1000u32));
2513        assert!(!p.has_slippage_bps());
2514        assert!(!p.has_receiver());
2515        assert!(!p.has_partner_fee());
2516    }
2517
2518    #[test]
2519    fn trade_params_buy() {
2520        let p = TradeParameters::buy(
2521            Address::ZERO,
2522            18,
2523            Address::with_last_byte(1),
2524            6,
2525            U256::from(500u32),
2526        );
2527        assert!(p.is_buy());
2528        assert!(!p.is_sell());
2529    }
2530
2531    #[test]
2532    fn trade_params_builders() {
2533        let recv = Address::with_last_byte(0x99);
2534        let p = TradeParameters::sell(Address::ZERO, 18, Address::ZERO, 18, U256::from(1u32))
2535            .with_slippage_bps(50)
2536            .with_receiver(recv)
2537            .with_valid_for(600)
2538            .with_valid_to(1_700_000_000)
2539            .with_partially_fillable();
2540
2541        assert!(p.has_slippage_bps());
2542        assert_eq!(p.slippage_bps, Some(50));
2543        assert!(p.has_receiver());
2544        assert_eq!(p.receiver, Some(recv));
2545        assert_eq!(p.valid_for, Some(600));
2546        assert_eq!(p.valid_to, Some(1_700_000_000));
2547        assert_eq!(p.partially_fillable, Some(true));
2548    }
2549
2550    #[test]
2551    fn trade_params_display() {
2552        let p = TradeParameters::sell(Address::ZERO, 18, Address::ZERO, 18, U256::from(1u32));
2553        let s = format!("{p}");
2554        assert!(s.contains("sell"));
2555    }
2556
2557    // ── LimitTradeParameters ────────────────────────────────────────────────
2558
2559    #[test]
2560    fn limit_trade_params_sell_and_buy() {
2561        let sell = LimitTradeParameters::sell(
2562            Address::ZERO,
2563            Address::with_last_byte(1),
2564            U256::from(100u32),
2565            U256::from(90u32),
2566        );
2567        assert!(sell.is_sell());
2568        assert!(!sell.is_buy());
2569        assert!(!sell.partially_fillable);
2570
2571        let buy = LimitTradeParameters::buy(
2572            Address::ZERO,
2573            Address::with_last_byte(1),
2574            U256::from(100u32),
2575            U256::from(90u32),
2576        );
2577        assert!(buy.is_buy());
2578        assert!(!buy.is_sell());
2579    }
2580
2581    #[test]
2582    fn limit_trade_params_builders() {
2583        let recv = Address::with_last_byte(0x42);
2584        let p = LimitTradeParameters::sell(
2585            Address::ZERO,
2586            Address::ZERO,
2587            U256::from(1u32),
2588            U256::from(1u32),
2589        )
2590        .with_receiver(recv)
2591        .with_valid_for(300)
2592        .with_valid_to(9999)
2593        .with_partially_fillable();
2594
2595        assert!(p.has_receiver());
2596        assert_eq!(p.receiver, Some(recv));
2597        assert!(p.has_valid_for());
2598        assert_eq!(p.valid_for, Some(300));
2599        assert!(p.has_valid_to());
2600        assert_eq!(p.valid_to, Some(9999));
2601        assert!(p.partially_fillable);
2602    }
2603
2604    #[test]
2605    fn limit_trade_params_has_app_data_and_partner_fee() {
2606        let p = LimitTradeParameters::sell(
2607            Address::ZERO,
2608            Address::ZERO,
2609            U256::from(1u32),
2610            U256::from(1u32),
2611        );
2612        assert!(!p.has_app_data());
2613        assert!(!p.has_partner_fee());
2614    }
2615
2616    #[test]
2617    fn limit_trade_params_display() {
2618        let p = LimitTradeParameters::sell(
2619            Address::ZERO,
2620            Address::ZERO,
2621            U256::from(100u32),
2622            U256::from(90u32),
2623        );
2624        let s = format!("{p}");
2625        assert!(s.contains("limit"));
2626        assert!(s.contains("sell"));
2627    }
2628
2629    // ── QuoteAmountsAndCosts ────────────────────────────────────────────────
2630
2631    fn sample_quote_costs() -> QuoteAmountsAndCosts {
2632        QuoteAmountsAndCosts {
2633            is_sell: true,
2634            before_all_fees: Amounts::new(U256::from(200u32), U256::from(110u32)),
2635            before_network_costs: Amounts::new(U256::from(200u32), U256::from(100u32)),
2636            after_network_costs: Amounts::new(U256::from(190u32), U256::from(100u32)),
2637            after_partner_fees: Amounts::new(U256::from(190u32), U256::from(95u32)),
2638            after_slippage: Amounts::new(U256::from(190u32), U256::from(90u32)),
2639            network_fee: NetworkFee::new(U256::from(10u32), U256::ZERO),
2640            partner_fee: PartnerFeeCost::new(U256::from(5u32), 50),
2641            protocol_fee: ProtocolFeeCost::new(U256::from(3u32), 30),
2642        }
2643    }
2644
2645    #[test]
2646    fn quote_costs_is_buy() {
2647        let sell = sample_quote_costs();
2648        assert!(!sell.is_buy());
2649
2650        let mut buy = sample_quote_costs();
2651        buy.is_sell = false;
2652        assert!(buy.is_buy());
2653    }
2654
2655    #[test]
2656    fn quote_costs_max_slippage_atoms() {
2657        let q = sample_quote_costs();
2658        // after_partner_fees.buy = 95, after_slippage.buy = 90 => slippage = 5
2659        assert_eq!(q.max_slippage_atoms(), U256::from(5u32));
2660    }
2661
2662    #[test]
2663    fn quote_costs_total_fees_atoms() {
2664        let q = sample_quote_costs();
2665        // network_fee.sell=10 + partner_fee.amount=5 + protocol_fee.amount=3 = 18
2666        assert_eq!(q.total_fees_atoms(), U256::from(18u32));
2667    }
2668
2669    #[test]
2670    fn quote_costs_has_fees() {
2671        let q = sample_quote_costs();
2672        assert!(q.has_network_fee());
2673        assert!(q.has_partner_fee());
2674        assert!(q.has_protocol_fee());
2675
2676        let zero_q = QuoteAmountsAndCosts {
2677            is_sell: true,
2678            before_all_fees: Amounts::default(),
2679            before_network_costs: Amounts::default(),
2680            after_network_costs: Amounts::default(),
2681            after_partner_fees: Amounts::default(),
2682            after_slippage: Amounts::default(),
2683            network_fee: NetworkFee::default(),
2684            partner_fee: PartnerFeeCost::default(),
2685            protocol_fee: ProtocolFeeCost::default(),
2686        };
2687        assert!(!zero_q.has_network_fee());
2688        assert!(!zero_q.has_partner_fee());
2689        assert!(!zero_q.has_protocol_fee());
2690    }
2691
2692    #[test]
2693    fn quote_costs_display() {
2694        let q = sample_quote_costs();
2695        let s = format!("{q}");
2696        assert!(s.contains("sell"));
2697        assert!(s.contains("network-fee"));
2698        assert!(s.contains("partner-fee"));
2699        assert!(s.contains("protocol-fee"));
2700    }
2701
2702    // ── map_quote_amounts_and_costs ─────────────────────────────────────────
2703
2704    #[test]
2705    fn map_doubles_amounts_preserves_bps() {
2706        let q = sample_quote_costs();
2707        let doubled = map_quote_amounts_and_costs(&q, |a| a * U256::from(2u32));
2708
2709        assert_eq!(doubled.before_all_fees.sell_amount, U256::from(400u32));
2710        assert_eq!(doubled.network_fee.amount_in_sell_currency, U256::from(20u32));
2711        assert_eq!(doubled.partner_fee.amount, U256::from(10u32));
2712        assert_eq!(doubled.protocol_fee.amount, U256::from(6u32));
2713        // bps preserved
2714        assert_eq!(doubled.partner_fee.bps, 50);
2715        assert_eq!(doubled.protocol_fee.bps, 30);
2716        assert!(doubled.is_sell);
2717    }
2718
2719    // ── OrderPostingResult ──────────────────────────────────────────────────
2720
2721    fn sample_unsigned_order() -> UnsignedOrder {
2722        UnsignedOrder {
2723            sell_token: Address::ZERO,
2724            buy_token: Address::ZERO,
2725            receiver: Address::ZERO,
2726            sell_amount: U256::from(100u32),
2727            buy_amount: U256::from(90u32),
2728            valid_to: 0,
2729            app_data: B256::ZERO,
2730            fee_amount: U256::ZERO,
2731            kind: OrderKind::Sell,
2732            partially_fillable: false,
2733            sell_token_balance: TokenBalance::Erc20,
2734            buy_token_balance: TokenBalance::Erc20,
2735        }
2736    }
2737
2738    #[test]
2739    fn order_posting_result_new() {
2740        let r = OrderPostingResult::new(
2741            "uid123",
2742            SigningScheme::Eip712,
2743            "0xsig",
2744            sample_unsigned_order(),
2745        );
2746        assert_eq!(r.order_id_ref(), "uid123");
2747        assert_eq!(r.signature_ref(), "0xsig");
2748    }
2749
2750    #[test]
2751    fn order_posting_result_signing_scheme_predicates() {
2752        let eip712 =
2753            OrderPostingResult::new("a", SigningScheme::Eip712, "", sample_unsigned_order());
2754        assert!(eip712.is_eip712());
2755        assert!(!eip712.is_eth_sign());
2756        assert!(!eip712.is_eip1271());
2757        assert!(!eip712.is_presign());
2758
2759        let eth_sign =
2760            OrderPostingResult::new("b", SigningScheme::EthSign, "", sample_unsigned_order());
2761        assert!(eth_sign.is_eth_sign());
2762
2763        let eip1271 =
2764            OrderPostingResult::new("c", SigningScheme::Eip1271, "", sample_unsigned_order());
2765        assert!(eip1271.is_eip1271());
2766
2767        let presign =
2768            OrderPostingResult::new("d", SigningScheme::PreSign, "", sample_unsigned_order());
2769        assert!(presign.is_presign());
2770    }
2771
2772    #[test]
2773    fn order_posting_result_display() {
2774        let r =
2775            OrderPostingResult::new("uid-xyz", SigningScheme::Eip712, "", sample_unsigned_order());
2776        let s = format!("{r}");
2777        assert!(s.contains("order"));
2778        assert!(s.contains("uid-xyz"));
2779    }
2780
2781    // ── BuildAppDataParams ──────────────────────────────────────────────────
2782
2783    #[test]
2784    fn build_app_data_params_new() {
2785        let p = BuildAppDataParams::new("CoW Swap", 50, OrderClassKind::Market);
2786        assert_eq!(p.app_code, "CoW Swap");
2787        assert_eq!(p.slippage_bps, 50);
2788        assert!(!p.has_partner_fee());
2789    }
2790
2791    #[test]
2792    fn build_app_data_params_with_partner_fee() {
2793        let fee = PartnerFee::single(PartnerFeeEntry::volume(50, "0xRecip"));
2794        let p = BuildAppDataParams::new("App", 25, OrderClassKind::Limit).with_partner_fee(fee);
2795        assert!(p.has_partner_fee());
2796    }
2797
2798    #[test]
2799    fn build_app_data_params_display() {
2800        let p = BuildAppDataParams::new("MyApp", 100, OrderClassKind::Market);
2801        let s = format!("{p}");
2802        assert!(s.contains("build-app-data"));
2803        assert!(s.contains("MyApp"));
2804        assert!(s.contains("100bps"));
2805    }
2806
2807    // ── SlippageToleranceRequest ────────────────────────────────────────────
2808
2809    #[test]
2810    fn slippage_request_new() {
2811        let r = SlippageToleranceRequest::new(1, Address::ZERO, Address::with_last_byte(1));
2812        assert_eq!(r.chain_id, 1);
2813        assert_eq!(r.sell_token, Address::ZERO);
2814        assert_eq!(r.buy_token, Address::with_last_byte(1));
2815        assert!(r.sell_amount.is_none());
2816        assert!(r.buy_amount.is_none());
2817    }
2818
2819    #[test]
2820    fn slippage_request_with_amounts() {
2821        let r = SlippageToleranceRequest::new(1, Address::ZERO, Address::ZERO)
2822            .with_sell_amount(U256::from(100u32))
2823            .with_buy_amount(U256::from(90u32));
2824        assert_eq!(r.sell_amount, Some(U256::from(100u32)));
2825        assert_eq!(r.buy_amount, Some(U256::from(90u32)));
2826    }
2827
2828    #[test]
2829    fn slippage_request_display() {
2830        let r = SlippageToleranceRequest::new(1, Address::ZERO, Address::ZERO);
2831        let s = format!("{r}");
2832        assert!(s.contains("slippage-req"));
2833        assert!(s.contains("chain=1"));
2834    }
2835
2836    // ── SlippageToleranceResponse ───────────────────────────────────────────
2837
2838    #[test]
2839    fn slippage_response_new() {
2840        let with = SlippageToleranceResponse::new(Some(50));
2841        assert!(with.has_suggestion());
2842        assert_eq!(with.slippage_bps, Some(50));
2843
2844        let without = SlippageToleranceResponse::new(None);
2845        assert!(!without.has_suggestion());
2846    }
2847
2848    #[test]
2849    fn slippage_response_display() {
2850        let with = SlippageToleranceResponse::new(Some(100));
2851        assert_eq!(format!("{with}"), "slippage-resp(100bps)");
2852
2853        let without = SlippageToleranceResponse::new(None);
2854        assert_eq!(format!("{without}"), "slippage-resp(none)");
2855    }
2856}