Skip to main content

cow_app_data/
types.rs

1//! App-data document types for `CoW` Protocol order metadata.
2//!
3//! This module defines the Rust types that mirror the `CoW` Protocol's
4//! app-data JSON schema (currently v1.14.0). Every type serialises to /
5//! deserialises from `camelCase` JSON via `serde`, matching the on-chain
6//! format exactly.
7//!
8//! # Type overview
9//!
10//! | Type | Role |
11//! |---|---|
12//! | [`AppDataDoc`] | Root document — version, app code, metadata |
13//! | [`Metadata`] | Container for all optional metadata fields |
14//! | [`OrderClassKind`] | `market` / `limit` / `liquidity` / `twap` |
15//! | [`CowHook`] | Pre- or post-settlement interaction hook |
16//! | [`PartnerFee`] | Single or multi-entry partner fee policy |
17//! | [`Quote`] | Slippage tolerance embedded in the order |
18//! | [`Referrer`] | Partner referral tracking address |
19//! | [`Utm`] | UTM campaign tracking parameters |
20//! | [`Bridging`] | Cross-chain bridge metadata |
21//! | [`Flashloan`] | Flash loan execution metadata |
22
23use std::fmt;
24
25use serde::{Deserialize, Serialize};
26
27// `CowHook` has been pushed down to `cow-types` (L1) so that sibling
28// L2 crates (permit, cow-shed) can reference it without depending on
29// app-data. This module re-exports it for backwards compatibility.
30pub use cow_types::CowHook;
31
32/// Latest app-data schema version this crate targets.
33///
34/// Matches [`super::schema::LATEST_VERSION`]. Documents created via
35/// [`AppDataDoc::new`] declare this version, which means their
36/// `metadata.referrer` (if set) must be a
37/// [`Referrer::Code`](Referrer::code) — the v1.14.0 shape. To target
38/// an older schema, build the doc explicitly and set its `version`
39/// field to e.g. `"1.13.0"` before calling
40/// [`super::schema::validate`].
41pub const LATEST_APP_DATA_VERSION: &str = "1.14.0";
42
43/// Latest version of the quote metadata schema.
44pub const LATEST_QUOTE_METADATA_VERSION: &str = "1.1.0";
45
46/// Latest version of the referrer metadata schema.
47pub const LATEST_REFERRER_METADATA_VERSION: &str = "1.0.0";
48
49/// Latest version of the order class metadata schema.
50pub const LATEST_ORDER_CLASS_METADATA_VERSION: &str = "0.3.0";
51
52/// Latest version of the UTM metadata schema.
53pub const LATEST_UTM_METADATA_VERSION: &str = "0.3.0";
54
55/// Latest version of the hooks metadata schema.
56pub const LATEST_HOOKS_METADATA_VERSION: &str = "0.2.0";
57
58/// Latest version of the signer metadata schema.
59pub const LATEST_SIGNER_METADATA_VERSION: &str = "0.1.0";
60
61/// Latest version of the widget metadata schema.
62pub const LATEST_WIDGET_METADATA_VERSION: &str = "0.1.0";
63
64/// Latest version of the partner fee metadata schema.
65pub const LATEST_PARTNER_FEE_METADATA_VERSION: &str = "1.0.0";
66
67/// Latest version of the replaced order metadata schema.
68pub const LATEST_REPLACED_ORDER_METADATA_VERSION: &str = "0.1.0";
69
70/// Latest version of the wrappers metadata schema.
71pub const LATEST_WRAPPERS_METADATA_VERSION: &str = "0.2.0";
72
73/// Latest version of the user consents metadata schema.
74pub const LATEST_USER_CONSENTS_METADATA_VERSION: &str = "0.1.0";
75
76/// Root document for `CoW` Protocol order app-data (schema v1.14.0).
77///
78/// Every `CoW` Protocol order carries a 32-byte `appData` field that commits
79/// to a JSON document describing the order's intent, referral, hooks, and
80/// more. `AppDataDoc` is the Rust representation of that JSON document.
81///
82/// Serialise this struct to canonical JSON with
83/// [`appdata_hex`](super::hash::appdata_hex) to obtain the `keccak256` hash
84/// used on-chain, or use [`get_app_data_info`](super::ipfs::get_app_data_info)
85/// to derive the hash, CID, and canonical JSON in one call.
86///
87/// Use the builder methods (`with_*`) to attach optional metadata:
88///
89/// # Example
90///
91/// ```
92/// use cow_app_data::{AppDataDoc, OrderClassKind, Quote, Referrer};
93///
94/// let doc = AppDataDoc::new("MyDApp")
95///     .with_environment("production")
96///     .with_referrer(Referrer::code("COWRS-PARTNER"))
97///     .with_order_class(OrderClassKind::Limit);
98///
99/// assert_eq!(doc.app_code.as_deref(), Some("MyDApp"));
100/// assert_eq!(doc.environment.as_deref(), Some("production"));
101/// assert!(doc.metadata.has_referrer());
102/// assert!(doc.metadata.has_order_class());
103/// ```
104#[derive(Debug, Clone, Default, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct AppDataDoc {
107    /// Schema version, e.g. `"1.14.0"`.
108    pub version: String,
109    /// Application identifier, e.g. `"CoW Swap"` or your app name.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub app_code: Option<String>,
112    /// Deployment environment, e.g. `"production"`.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub environment: Option<String>,
115    /// Structured metadata attached to the order.
116    pub metadata: Metadata,
117}
118
119impl AppDataDoc {
120    /// Create a minimal [`AppDataDoc`] with the given `app_code` and no extra
121    /// metadata.
122    ///
123    /// Sets [`version`](Self::version) to [`LATEST_APP_DATA_VERSION`],
124    /// `app_code` to the provided value, and [`metadata`](Self::metadata) to
125    /// its `Default` (all fields `None`).
126    ///
127    /// # Parameters
128    ///
129    /// * `app_code` — application identifier (e.g. `"CoW Swap"`, `"MyDApp"`). Must be ≤ 50
130    ///   characters to pass validation.
131    ///
132    /// # Returns
133    ///
134    /// A new [`AppDataDoc`] ready to be hashed or further customised with the
135    /// `with_*` builder methods.
136    ///
137    /// # Example
138    ///
139    /// ```
140    /// use cow_app_data::AppDataDoc;
141    ///
142    /// let doc = AppDataDoc::new("MyDApp");
143    /// assert_eq!(doc.app_code.as_deref(), Some("MyDApp"));
144    /// assert_eq!(doc.version, "1.14.0");
145    /// assert!(!doc.metadata.has_referrer());
146    /// ```
147    #[must_use]
148    pub fn new(app_code: impl Into<String>) -> Self {
149        Self {
150            version: LATEST_APP_DATA_VERSION.to_owned(),
151            app_code: Some(app_code.into()),
152            environment: None,
153            metadata: Metadata::default(),
154        }
155    }
156
157    /// Set the deployment environment (e.g. `"production"`, `"staging"`).
158    ///
159    /// The environment string is included in the canonical JSON and therefore
160    /// affects the `keccak256` hash. Use it to distinguish orders from
161    /// different deployment stages.
162    ///
163    /// # Parameters
164    ///
165    /// * `env` — free-form environment label.
166    ///
167    /// # Returns
168    ///
169    /// `self` with [`environment`](Self::environment) set.
170    #[must_use]
171    pub fn with_environment(mut self, env: impl Into<String>) -> Self {
172        self.environment = Some(env.into());
173        self
174    }
175
176    /// Attach a [`Referrer`] address for partner attribution.
177    ///
178    /// The referrer's Ethereum address is embedded in the order's app-data
179    /// so the protocol can attribute volume to integration partners.
180    ///
181    /// # Parameters
182    ///
183    /// * `referrer` — the [`Referrer`] containing the partner address.
184    ///
185    /// # Returns
186    ///
187    /// `self` with [`metadata.referrer`](Metadata::referrer) set.
188    #[must_use]
189    pub fn with_referrer(mut self, referrer: Referrer) -> Self {
190        self.metadata.referrer = Some(referrer);
191        self
192    }
193
194    /// Attach [`Utm`] campaign tracking parameters.
195    ///
196    /// UTM parameters (source, medium, campaign, content, term) let analytics
197    /// pipelines attribute order volume to marketing campaigns.
198    ///
199    /// # Parameters
200    ///
201    /// * `utm` — the [`Utm`] tracking parameters.
202    ///
203    /// # Returns
204    ///
205    /// `self` with [`metadata.utm`](Metadata::utm) set.
206    #[must_use]
207    pub fn with_utm(mut self, utm: Utm) -> Self {
208        self.metadata.utm = Some(utm);
209        self
210    }
211
212    /// Attach pre- and/or post-settlement interaction hooks.
213    ///
214    /// Hooks are arbitrary contract calls the settlement contract executes
215    /// before (`pre`) or after (`post`) the trade. See [`CowHook`] for
216    /// details on individual hook entries.
217    ///
218    /// # Parameters
219    ///
220    /// * `hooks` — the [`OrderInteractionHooks`] containing pre/post lists.
221    ///
222    /// # Returns
223    ///
224    /// `self` with [`metadata.hooks`](Metadata::hooks) set.
225    #[must_use]
226    pub fn with_hooks(mut self, hooks: OrderInteractionHooks) -> Self {
227        self.metadata.hooks = Some(hooks);
228        self
229    }
230
231    /// Attach a [`PartnerFee`] policy to this order.
232    ///
233    /// Partner fees are charged by integration partners as a percentage of
234    /// the trade. Each fee entry specifies a basis-point rate and a recipient
235    /// address.
236    ///
237    /// # Parameters
238    ///
239    /// * `fee` — the [`PartnerFee`] (single or multi-entry).
240    ///
241    /// # Returns
242    ///
243    /// `self` with [`metadata.partner_fee`](Metadata::partner_fee) set.
244    ///
245    /// # Example
246    ///
247    /// ```
248    /// use cow_app_data::{AppDataDoc, PartnerFee, PartnerFeeEntry};
249    ///
250    /// let doc = AppDataDoc::new("MyDApp")
251    ///     .with_partner_fee(PartnerFee::single(PartnerFeeEntry::volume(50, "0xRecipient")));
252    /// assert!(doc.metadata.has_partner_fee());
253    /// ```
254    #[must_use]
255    pub fn with_partner_fee(mut self, fee: PartnerFee) -> Self {
256        self.metadata.partner_fee = Some(fee);
257        self
258    }
259
260    /// Mark this order as replacing a previously submitted order.
261    ///
262    /// The protocol uses this to link replacement orders for analytics and
263    /// to avoid double-fills.
264    ///
265    /// # Parameters
266    ///
267    /// * `uid` — the `0x`-prefixed order UID of the order being replaced (56 bytes = `0x` + 112 hex
268    ///   chars).
269    ///
270    /// # Returns
271    ///
272    /// `self` with [`metadata.replaced_order`](Metadata::replaced_order) set.
273    #[must_use]
274    pub fn with_replaced_order(mut self, uid: impl Into<String>) -> Self {
275        self.metadata.replaced_order = Some(ReplacedOrder { uid: uid.into() });
276        self
277    }
278
279    /// Attach the signer address for `EIP-1271` or other smart-contract
280    /// signers.
281    ///
282    /// When the order is signed by a smart contract (not an EOA), this field
283    /// records the contract address that will validate the signature on-chain.
284    ///
285    /// # Parameters
286    ///
287    /// * `signer` — the `0x`-prefixed Ethereum address of the signing contract.
288    ///
289    /// # Returns
290    ///
291    /// `self` with [`metadata.signer`](Metadata::signer) set.
292    #[must_use]
293    pub fn with_signer(mut self, signer: impl Into<String>) -> Self {
294        self.metadata.signer = Some(signer.into());
295        self
296    }
297
298    /// Set the order class kind (`market`, `limit`, `liquidity`, or `twap`).
299    ///
300    /// Solvers and the protocol UI use this to decide execution strategy and
301    /// display. See [`OrderClassKind`] for the available variants.
302    ///
303    /// # Parameters
304    ///
305    /// * `kind` — the [`OrderClassKind`] variant.
306    ///
307    /// # Returns
308    ///
309    /// `self` with [`metadata.order_class`](Metadata::order_class) set.
310    #[must_use]
311    pub const fn with_order_class(mut self, kind: OrderClassKind) -> Self {
312        self.metadata.order_class = Some(OrderClass { order_class: kind });
313        self
314    }
315
316    /// Attach cross-chain [`Bridging`] metadata.
317    ///
318    /// Records which bridge provider was used, the destination chain, and the
319    /// destination token address so solvers and analytics can trace
320    /// cross-chain flows.
321    ///
322    /// # Parameters
323    ///
324    /// * `bridging` — the [`Bridging`] record.
325    ///
326    /// # Returns
327    ///
328    /// `self` with [`metadata.bridging`](Metadata::bridging) set.
329    #[must_use]
330    pub fn with_bridging(mut self, bridging: Bridging) -> Self {
331        self.metadata.bridging = Some(bridging);
332        self
333    }
334
335    /// Attach [`Flashloan`] execution metadata.
336    ///
337    /// Records the flash-loan parameters (amount, provider, token, adapter,
338    /// receiver) so the settlement contract and solvers can reconstruct the
339    /// flash-loan flow.
340    ///
341    /// # Parameters
342    ///
343    /// * `flashloan` — the [`Flashloan`] record.
344    ///
345    /// # Returns
346    ///
347    /// `self` with [`metadata.flashloan`](Metadata::flashloan) set.
348    #[must_use]
349    pub fn with_flashloan(mut self, flashloan: Flashloan) -> Self {
350        self.metadata.flashloan = Some(flashloan);
351        self
352    }
353
354    /// Attach token [`WrapperEntry`] records.
355    ///
356    /// Wrapper entries describe token wrapping/unwrapping operations applied
357    /// during order execution (e.g. WETH ↔ ETH).
358    ///
359    /// # Parameters
360    ///
361    /// * `wrappers` — list of [`WrapperEntry`] records.
362    ///
363    /// # Returns
364    ///
365    /// `self` with [`metadata.wrappers`](Metadata::wrappers) set.
366    #[must_use]
367    pub fn with_wrappers(mut self, wrappers: Vec<WrapperEntry>) -> Self {
368        self.metadata.wrappers = Some(wrappers);
369        self
370    }
371
372    /// Attach [`UserConsent`] records for terms of service acceptance.
373    ///
374    /// # Parameters
375    ///
376    /// * `consents` — list of [`UserConsent`] records.
377    ///
378    /// # Returns
379    ///
380    /// `self` with [`metadata.user_consents`](Metadata::user_consents) set.
381    #[must_use]
382    pub fn with_user_consents(mut self, consents: Vec<UserConsent>) -> Self {
383        self.metadata.user_consents = Some(consents);
384        self
385    }
386}
387
388impl fmt::Display for AppDataDoc {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        let code = self.app_code.as_deref().map_or("none", |s| s);
391        write!(f, "app-data(v{}, code={})", self.version, code)
392    }
393}
394/// Metadata container — all fields are optional.
395///
396/// Each field corresponds to a section of the `CoW` Protocol app-data
397/// schema. Fields are serialised only when `Some` (via
398/// `#[serde(skip_serializing_if = "Option::is_none")]`), keeping the JSON
399/// compact.
400///
401/// Use the builder methods (`with_*`) to populate fields, or the `has_*`
402/// predicates to check which fields are set.
403///
404/// # Example
405///
406/// ```
407/// use cow_app_data::{Metadata, Quote, Referrer};
408///
409/// let meta = Metadata::default()
410///     .with_referrer(Referrer::code("COWRS-PARTNER"))
411///     .with_quote(Quote::new(50));
412///
413/// assert!(meta.has_referrer());
414/// assert!(meta.has_quote());
415/// assert!(!meta.has_hooks());
416/// ```
417#[derive(Debug, Clone, Default, Serialize, Deserialize)]
418#[serde(rename_all = "camelCase")]
419pub struct Metadata {
420    /// Referrer address for partner attribution.
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub referrer: Option<Referrer>,
423    /// UTM tracking parameters.
424    #[serde(skip_serializing_if = "Option::is_none")]
425    pub utm: Option<Utm>,
426    /// Quote-level slippage settings.
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub quote: Option<Quote>,
429    /// Classification of the order intent.
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub order_class: Option<OrderClass>,
432    /// Pre- and post-interaction hooks.
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub hooks: Option<OrderInteractionHooks>,
435    /// Widget metadata when the order originates from a widget integration.
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub widget: Option<Widget>,
438    /// Protocol fee charged by an integration partner.
439    #[serde(skip_serializing_if = "Option::is_none")]
440    pub partner_fee: Option<PartnerFee>,
441    /// UID of a previous order that this order replaces.
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub replaced_order: Option<ReplacedOrder>,
444    /// Signer wallet address (for `EIP-1271` or other non-EOA signers).
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub signer: Option<String>,
447    /// Cross-chain bridging metadata (if the order used a bridge).
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub bridging: Option<Bridging>,
450    /// Flash loan metadata (if the order used a flash loan).
451    #[serde(skip_serializing_if = "Option::is_none")]
452    pub flashloan: Option<Flashloan>,
453    /// Token wrapper entries applied during execution.
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub wrappers: Option<Vec<WrapperEntry>>,
456    /// User consent records attached to this order.
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub user_consents: Option<Vec<UserConsent>>,
459}
460
461impl Metadata {
462    /// Set the [`Referrer`] address for partner attribution.
463    ///
464    /// # Parameters
465    ///
466    /// * `referrer` — the [`Referrer`] containing the partner address.
467    ///
468    /// # Returns
469    ///
470    /// `self` with `referrer` set.
471    #[must_use]
472    pub fn with_referrer(mut self, referrer: Referrer) -> Self {
473        self.referrer = Some(referrer);
474        self
475    }
476
477    /// Set the [`Utm`] campaign tracking parameters.
478    ///
479    /// # Parameters
480    ///
481    /// * `utm` — the [`Utm`] parameters (source, medium, campaign, …).
482    ///
483    /// # Returns
484    ///
485    /// `self` with `utm` set.
486    #[must_use]
487    pub fn with_utm(mut self, utm: Utm) -> Self {
488        self.utm = Some(utm);
489        self
490    }
491
492    /// Set the quote-level slippage settings.
493    ///
494    /// # Parameters
495    ///
496    /// * `quote` — the [`Quote`] containing the slippage tolerance in basis points and optional
497    ///   smart-slippage flag.
498    ///
499    /// # Returns
500    ///
501    /// `self` with `quote` set.
502    #[must_use]
503    pub const fn with_quote(mut self, quote: Quote) -> Self {
504        self.quote = Some(quote);
505        self
506    }
507
508    /// Set the order class classification.
509    ///
510    /// # Parameters
511    ///
512    /// * `order_class` — the [`OrderClass`] wrapping an [`OrderClassKind`].
513    ///
514    /// # Returns
515    ///
516    /// `self` with `order_class` set.
517    #[must_use]
518    pub const fn with_order_class(mut self, order_class: OrderClass) -> Self {
519        self.order_class = Some(order_class);
520        self
521    }
522
523    /// Set the pre- and post-settlement interaction hooks.
524    ///
525    /// # Parameters
526    ///
527    /// * `hooks` — the [`OrderInteractionHooks`] containing pre/post lists.
528    ///
529    /// # Returns
530    ///
531    /// `self` with `hooks` set.
532    #[must_use]
533    pub fn with_hooks(mut self, hooks: OrderInteractionHooks) -> Self {
534        self.hooks = Some(hooks);
535        self
536    }
537
538    /// Set the widget integration metadata.
539    ///
540    /// # Parameters
541    ///
542    /// * `widget` — the [`Widget`] identifying the widget host.
543    ///
544    /// # Returns
545    ///
546    /// `self` with `widget` set.
547    #[must_use]
548    pub fn with_widget(mut self, widget: Widget) -> Self {
549        self.widget = Some(widget);
550        self
551    }
552
553    /// Set the partner fee policy.
554    ///
555    /// # Parameters
556    ///
557    /// * `fee` — the [`PartnerFee`] (single or multi-entry).
558    ///
559    /// # Returns
560    ///
561    /// `self` with `partner_fee` set.
562    #[must_use]
563    pub fn with_partner_fee(mut self, fee: PartnerFee) -> Self {
564        self.partner_fee = Some(fee);
565        self
566    }
567
568    /// Set the replaced-order reference.
569    ///
570    /// # Parameters
571    ///
572    /// * `order` — the [`ReplacedOrder`] containing the UID of the order being superseded.
573    ///
574    /// # Returns
575    ///
576    /// `self` with `replaced_order` set.
577    #[must_use]
578    pub fn with_replaced_order(mut self, order: ReplacedOrder) -> Self {
579        self.replaced_order = Some(order);
580        self
581    }
582
583    /// Set the signer address override for smart-contract wallets.
584    ///
585    /// # Parameters
586    ///
587    /// * `signer` — `0x`-prefixed Ethereum address of the signing contract.
588    ///
589    /// # Returns
590    ///
591    /// `self` with `signer` set.
592    #[must_use]
593    pub fn with_signer(mut self, signer: impl Into<String>) -> Self {
594        self.signer = Some(signer.into());
595        self
596    }
597
598    /// Set the cross-chain [`Bridging`] metadata.
599    ///
600    /// # Parameters
601    ///
602    /// * `bridging` — the [`Bridging`] record.
603    ///
604    /// # Returns
605    ///
606    /// `self` with `bridging` set.
607    #[must_use]
608    pub fn with_bridging(mut self, bridging: Bridging) -> Self {
609        self.bridging = Some(bridging);
610        self
611    }
612
613    /// Set the [`Flashloan`] execution metadata.
614    ///
615    /// # Parameters
616    ///
617    /// * `flashloan` — the [`Flashloan`] record.
618    ///
619    /// # Returns
620    ///
621    /// `self` with `flashloan` set.
622    #[must_use]
623    pub fn with_flashloan(mut self, flashloan: Flashloan) -> Self {
624        self.flashloan = Some(flashloan);
625        self
626    }
627
628    /// Set the token [`WrapperEntry`] records.
629    ///
630    /// # Parameters
631    ///
632    /// * `wrappers` — list of wrapper entries applied during execution.
633    ///
634    /// # Returns
635    ///
636    /// `self` with `wrappers` set.
637    #[must_use]
638    pub fn with_wrappers(mut self, wrappers: Vec<WrapperEntry>) -> Self {
639        self.wrappers = Some(wrappers);
640        self
641    }
642
643    /// Set the [`UserConsent`] records for terms of service acceptance.
644    ///
645    /// # Parameters
646    ///
647    /// * `consents` — list of consent records.
648    ///
649    /// # Returns
650    ///
651    /// `self` with `user_consents` set.
652    #[must_use]
653    pub fn with_user_consents(mut self, consents: Vec<UserConsent>) -> Self {
654        self.user_consents = Some(consents);
655        self
656    }
657
658    /// Returns `true` if a referrer tracking code is set.
659    #[must_use]
660    pub const fn has_referrer(&self) -> bool {
661        self.referrer.is_some()
662    }
663
664    /// Returns `true` if `UTM` campaign parameters are set.
665    #[must_use]
666    pub const fn has_utm(&self) -> bool {
667        self.utm.is_some()
668    }
669
670    /// Returns `true` if quote-level slippage settings are set.
671    #[must_use]
672    pub const fn has_quote(&self) -> bool {
673        self.quote.is_some()
674    }
675
676    /// Returns `true` if an order class classification is set.
677    #[must_use]
678    pub const fn has_order_class(&self) -> bool {
679        self.order_class.is_some()
680    }
681
682    /// Returns `true` if pre/post interaction hooks are set.
683    #[must_use]
684    pub const fn has_hooks(&self) -> bool {
685        self.hooks.is_some()
686    }
687
688    /// Returns `true` if widget integration metadata is set.
689    #[must_use]
690    pub const fn has_widget(&self) -> bool {
691        self.widget.is_some()
692    }
693
694    /// Returns `true` if a partner fee is set.
695    #[must_use]
696    pub const fn has_partner_fee(&self) -> bool {
697        self.partner_fee.is_some()
698    }
699
700    /// Returns `true` if a replaced-order reference is set.
701    #[must_use]
702    pub const fn has_replaced_order(&self) -> bool {
703        self.replaced_order.is_some()
704    }
705
706    /// Returns `true` if a signer address override is set.
707    #[must_use]
708    pub const fn has_signer(&self) -> bool {
709        self.signer.is_some()
710    }
711
712    /// Returns `true` if cross-chain bridging metadata is set.
713    #[must_use]
714    pub const fn has_bridging(&self) -> bool {
715        self.bridging.is_some()
716    }
717
718    /// Returns `true` if flash loan metadata is set.
719    #[must_use]
720    pub const fn has_flashloan(&self) -> bool {
721        self.flashloan.is_some()
722    }
723
724    /// Returns `true` if at least one token wrapper entry is set.
725    #[must_use]
726    pub fn has_wrappers(&self) -> bool {
727        self.wrappers.as_ref().is_some_and(|v| !v.is_empty())
728    }
729
730    /// Returns `true` if at least one user consent record is set.
731    #[must_use]
732    pub fn has_user_consents(&self) -> bool {
733        self.user_consents.as_ref().is_some_and(|v| !v.is_empty())
734    }
735}
736
737impl fmt::Display for Metadata {
738    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
739        f.write_str("metadata")
740    }
741}
742
743/// Partner referral tracking information.
744///
745/// The upstream `CoW` Protocol app-data schema for `referrer` changed
746/// shape between v1.13.0 and v1.14.0:
747///
748/// | Schema version | Shape | Pattern |
749/// |---|---|---|
750/// | v0.1.0 – v1.13.0 | `{ "address": "0x…" }` (partner Ethereum address) | `^0x[a-fA-F0-9]{40}$` |
751/// | v1.14.0+ | `{ "code": "ABCDE" }` (affiliate code, uppercase) | `^[A-Z0-9_-]{5,20}$` |
752///
753/// Both forms are modelled as variants of this enum with
754/// `#[serde(untagged)]`, so a single [`Referrer`] value deserialises
755/// correctly from either shape and serialises back into the same shape.
756/// Runtime schema validation via [`super::schema::validate`] dispatches
757/// on the document's declared `version` and picks the matching bundled
758/// schema, so an `Address`-flavoured referrer must accompany a
759/// v1.13.0-or-earlier document and a `Code`-flavoured one a v1.14.0+
760/// document.
761///
762/// # Construction
763///
764/// Prefer the dedicated constructors [`Referrer::address`] and
765/// [`Referrer::code`]; [`Referrer::new`] is kept as a deprecated alias
766/// for the address form so existing v1.13.0-era code keeps compiling.
767#[derive(Debug, Clone, Serialize, Deserialize)]
768#[serde(untagged)]
769pub enum Referrer {
770    /// Legacy form used by schema versions up to and including v1.13.0.
771    Address {
772        /// Partner's Ethereum address (`0x`-prefixed, 40 hex chars).
773        address: String,
774    },
775    /// Current form used by schema v1.14.0 and later.
776    Code {
777        /// Affiliate / referral code. Case-insensitive but expected to
778        /// be stored uppercase; must match `^[A-Z0-9_-]{5,20}$`.
779        code: String,
780    },
781}
782
783impl Referrer {
784    /// Construct an address-flavoured referrer (schema v0.1.0 – v1.13.0).
785    ///
786    /// The address is stored verbatim; callers are responsible for
787    /// passing a well-formed `0x`-prefixed 40-character hex string.
788    /// Runtime schema validation under v1.13.0 rejects non-conforming
789    /// values.
790    ///
791    /// # Example
792    ///
793    /// ```
794    /// use cow_app_data::Referrer;
795    ///
796    /// let r = Referrer::address("0xb6BAd41ae76A11D10f7b0E664C5007b908bC77C9");
797    /// assert_eq!(r.as_address(), Some("0xb6BAd41ae76A11D10f7b0E664C5007b908bC77C9"));
798    /// assert_eq!(r.as_code(), None);
799    /// ```
800    #[must_use]
801    pub fn address(address: impl Into<String>) -> Self {
802        Self::Address { address: address.into() }
803    }
804
805    /// Construct a code-flavoured referrer (schema v1.14.0+).
806    ///
807    /// The code is stored verbatim; callers are responsible for
808    /// matching the upstream regex `^[A-Z0-9_-]{5,20}$`. Runtime schema
809    /// validation under v1.14.0 rejects non-conforming values.
810    ///
811    /// # Example
812    ///
813    /// ```
814    /// use cow_app_data::Referrer;
815    ///
816    /// let r = Referrer::code("COWRS");
817    /// assert_eq!(r.as_code(), Some("COWRS"));
818    /// assert_eq!(r.as_address(), None);
819    /// ```
820    #[must_use]
821    pub fn code(code: impl Into<String>) -> Self {
822        Self::Code { code: code.into() }
823    }
824
825    /// Deprecated alias for [`Referrer::address`] — kept so existing
826    /// v1.13.0-era call sites keep compiling unchanged.
827    ///
828    /// New code should call [`Referrer::address`] or [`Referrer::code`]
829    /// explicitly to make the schema-version affinity obvious.
830    #[must_use]
831    #[deprecated(since = "1.1.0", note = "use Referrer::address or Referrer::code explicitly")]
832    pub fn new(address: impl Into<String>) -> Self {
833        Self::address(address)
834    }
835
836    /// Return the address string if this is an [`Self::Address`] variant.
837    #[must_use]
838    pub const fn as_address(&self) -> Option<&str> {
839        match self {
840            Self::Address { address } => Some(address.as_str()),
841            Self::Code { .. } => None,
842        }
843    }
844
845    /// Return the code string if this is a [`Self::Code`] variant.
846    #[must_use]
847    pub const fn as_code(&self) -> Option<&str> {
848        match self {
849            Self::Code { code } => Some(code.as_str()),
850            Self::Address { .. } => None,
851        }
852    }
853}
854
855impl fmt::Display for Referrer {
856    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
857        match self {
858            Self::Address { address } => write!(f, "referrer(address={address})"),
859            Self::Code { code } => write!(f, "referrer(code={code})"),
860        }
861    }
862}
863
864/// UTM campaign tracking parameters.
865#[derive(Debug, Clone, Default, Serialize, Deserialize)]
866#[serde(rename_all = "camelCase")]
867pub struct Utm {
868    /// UTM source parameter.
869    #[serde(skip_serializing_if = "Option::is_none")]
870    pub utm_source: Option<String>,
871    /// UTM medium parameter.
872    #[serde(skip_serializing_if = "Option::is_none")]
873    pub utm_medium: Option<String>,
874    /// UTM campaign parameter.
875    #[serde(skip_serializing_if = "Option::is_none")]
876    pub utm_campaign: Option<String>,
877    /// UTM content parameter.
878    #[serde(skip_serializing_if = "Option::is_none")]
879    pub utm_content: Option<String>,
880    /// UTM keyword / term parameter.
881    #[serde(skip_serializing_if = "Option::is_none")]
882    pub utm_term: Option<String>,
883}
884
885impl Utm {
886    /// Construct a [`Utm`] with all fields `None`.
887    ///
888    /// Use the `with_*` builder methods to populate individual UTM
889    /// parameters. Only non-`None` fields are serialised into the JSON.
890    ///
891    /// # Returns
892    ///
893    /// An empty [`Utm`] instance.
894    ///
895    /// # Example
896    ///
897    /// ```
898    /// use cow_app_data::Utm;
899    ///
900    /// let utm = Utm::new().with_source("twitter").with_campaign("launch-2025");
901    /// assert!(utm.has_source());
902    /// assert!(utm.has_campaign());
903    /// assert!(!utm.has_medium());
904    /// ```
905    #[must_use]
906    pub fn new() -> Self {
907        Self::default()
908    }
909
910    /// Set the `utm_source` parameter (e.g. `"twitter"`, `"google"`).
911    ///
912    /// # Parameters
913    ///
914    /// * `source` — the traffic source identifier.
915    ///
916    /// # Returns
917    ///
918    /// `self` with `utm_source` set.
919    #[must_use]
920    pub fn with_source(mut self, source: impl Into<String>) -> Self {
921        self.utm_source = Some(source.into());
922        self
923    }
924
925    /// Set the `utm_medium` parameter (e.g. `"cpc"`, `"email"`).
926    ///
927    /// # Parameters
928    ///
929    /// * `medium` — the marketing medium identifier.
930    ///
931    /// # Returns
932    ///
933    /// `self` with `utm_medium` set.
934    #[must_use]
935    pub fn with_medium(mut self, medium: impl Into<String>) -> Self {
936        self.utm_medium = Some(medium.into());
937        self
938    }
939
940    /// Set the `utm_campaign` parameter (e.g. `"launch-2025"`).
941    ///
942    /// # Parameters
943    ///
944    /// * `campaign` — the campaign name.
945    ///
946    /// # Returns
947    ///
948    /// `self` with `utm_campaign` set.
949    #[must_use]
950    pub fn with_campaign(mut self, campaign: impl Into<String>) -> Self {
951        self.utm_campaign = Some(campaign.into());
952        self
953    }
954
955    /// Set the `utm_content` parameter for A/B testing or ad variants.
956    ///
957    /// # Parameters
958    ///
959    /// * `content` — the content variant identifier.
960    ///
961    /// # Returns
962    ///
963    /// `self` with `utm_content` set.
964    #[must_use]
965    pub fn with_content(mut self, content: impl Into<String>) -> Self {
966        self.utm_content = Some(content.into());
967        self
968    }
969
970    /// Set the `utm_term` parameter for paid search keywords.
971    ///
972    /// # Parameters
973    ///
974    /// * `term` — the search keyword or term.
975    ///
976    /// # Returns
977    ///
978    /// `self` with `utm_term` set.
979    #[must_use]
980    pub fn with_term(mut self, term: impl Into<String>) -> Self {
981        self.utm_term = Some(term.into());
982        self
983    }
984
985    /// Returns `true` if the `utm_source` parameter is set.
986    #[must_use]
987    pub const fn has_source(&self) -> bool {
988        self.utm_source.is_some()
989    }
990
991    /// Returns `true` if the `utm_medium` parameter is set.
992    #[must_use]
993    pub const fn has_medium(&self) -> bool {
994        self.utm_medium.is_some()
995    }
996
997    /// Returns `true` if the `utm_campaign` parameter is set.
998    #[must_use]
999    pub const fn has_campaign(&self) -> bool {
1000        self.utm_campaign.is_some()
1001    }
1002
1003    /// Returns `true` if the `utm_content` parameter is set.
1004    #[must_use]
1005    pub const fn has_content(&self) -> bool {
1006        self.utm_content.is_some()
1007    }
1008
1009    /// Returns `true` if the `utm_term` parameter is set.
1010    #[must_use]
1011    pub const fn has_term(&self) -> bool {
1012        self.utm_term.is_some()
1013    }
1014}
1015
1016impl fmt::Display for Utm {
1017    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1018        let src = self.utm_source.as_deref().map_or("none", |s| s);
1019        write!(f, "utm(source={src})")
1020    }
1021}
1022
1023/// Quote-level slippage settings embedded in app-data.
1024///
1025/// Records the slippage tolerance the user chose when placing the order, so
1026/// solvers and analytics can reconstruct the original intent.
1027///
1028/// # Example
1029///
1030/// ```
1031/// use cow_app_data::Quote;
1032///
1033/// // 0.5 % slippage with smart slippage enabled
1034/// let quote = Quote::new(50).with_smart_slippage();
1035/// assert_eq!(quote.slippage_bips, 50);
1036/// assert_eq!(quote.smart_slippage, Some(true));
1037/// ```
1038#[derive(Debug, Clone, Serialize, Deserialize)]
1039#[serde(rename_all = "camelCase")]
1040pub struct Quote {
1041    /// Slippage tolerance in basis points (e.g. `50` = 0.5 %).
1042    pub slippage_bips: u32,
1043    /// Whether smart (dynamic per-trade) slippage is enabled.
1044    #[serde(skip_serializing_if = "Option::is_none")]
1045    pub smart_slippage: Option<bool>,
1046}
1047
1048impl Quote {
1049    /// Construct a [`Quote`] with the given slippage tolerance.
1050    ///
1051    /// # Parameters
1052    ///
1053    /// * `slippage_bips` — slippage tolerance in basis points. `50` = 0.5 %, `100` = 1 %, `10_000`
1054    ///   = 100 %.
1055    ///
1056    /// # Returns
1057    ///
1058    /// A new [`Quote`] with `smart_slippage` set to `None` (disabled).
1059    /// Chain [`with_smart_slippage`](Self::with_smart_slippage) to enable it.
1060    ///
1061    /// # Example
1062    ///
1063    /// ```
1064    /// use cow_app_data::Quote;
1065    ///
1066    /// let q = Quote::new(50);
1067    /// assert_eq!(q.slippage_bips, 50);
1068    /// assert_eq!(q.smart_slippage, None);
1069    /// ```
1070    #[must_use]
1071    pub const fn new(slippage_bips: u32) -> Self {
1072        Self { slippage_bips, smart_slippage: None }
1073    }
1074
1075    /// Enable dynamic (smart) slippage adjustment.
1076    ///
1077    /// When enabled, the protocol may adjust the slippage tolerance
1078    /// per-trade based on market conditions rather than using a fixed value.
1079    ///
1080    /// # Returns
1081    ///
1082    /// `self` with `smart_slippage` set to `Some(true)`.
1083    #[must_use]
1084    pub const fn with_smart_slippage(mut self) -> Self {
1085        self.smart_slippage = Some(true);
1086        self
1087    }
1088}
1089
1090impl fmt::Display for Quote {
1091    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1092        write!(f, "quote({}bips)", self.slippage_bips)
1093    }
1094}
1095
1096/// High-level classification of the order's intent.
1097///
1098/// Solvers and the protocol UI use this to decide execution strategy and
1099/// display. The variant is serialised as a `camelCase` string in the JSON
1100/// document (e.g. `"market"`, `"twap"`).
1101///
1102/// # Example
1103///
1104/// ```
1105/// use cow_app_data::OrderClassKind;
1106///
1107/// let kind = OrderClassKind::Limit;
1108/// assert_eq!(kind.as_str(), "limit");
1109/// assert!(kind.is_limit());
1110/// assert!(!kind.is_market());
1111/// ```
1112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1113#[serde(rename_all = "camelCase")]
1114pub enum OrderClassKind {
1115    /// Standard market order.
1116    Market,
1117    /// Limit order with a price constraint.
1118    Limit,
1119    /// Programmatic liquidity order.
1120    Liquidity,
1121    /// Time-Weighted Average Price order.
1122    Twap,
1123}
1124
1125impl OrderClassKind {
1126    /// Returns the camelCase string used by the `CoW` Protocol schema.
1127    #[must_use]
1128    pub const fn as_str(self) -> &'static str {
1129        match self {
1130            Self::Market => "market",
1131            Self::Limit => "limit",
1132            Self::Liquidity => "liquidity",
1133            Self::Twap => "twap",
1134        }
1135    }
1136
1137    /// Returns `true` if this is a market order class.
1138    #[must_use]
1139    pub const fn is_market(self) -> bool {
1140        matches!(self, Self::Market)
1141    }
1142
1143    /// Returns `true` if this is a limit order class.
1144    #[must_use]
1145    pub const fn is_limit(self) -> bool {
1146        matches!(self, Self::Limit)
1147    }
1148
1149    /// Returns `true` if this is a liquidity order class.
1150    #[must_use]
1151    pub const fn is_liquidity(self) -> bool {
1152        matches!(self, Self::Liquidity)
1153    }
1154
1155    /// Returns `true` if this is a `TWAP` order class.
1156    #[must_use]
1157    pub const fn is_twap(self) -> bool {
1158        matches!(self, Self::Twap)
1159    }
1160}
1161
1162impl fmt::Display for OrderClassKind {
1163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1164        f.write_str(self.as_str())
1165    }
1166}
1167
1168impl TryFrom<&str> for OrderClassKind {
1169    type Error = cow_errors::CowError;
1170
1171    /// Parse an [`OrderClassKind`] from the `CoW` Protocol schema string.
1172    fn try_from(s: &str) -> Result<Self, Self::Error> {
1173        match s {
1174            "market" => Ok(Self::Market),
1175            "limit" => Ok(Self::Limit),
1176            "liquidity" => Ok(Self::Liquidity),
1177            "twap" => Ok(Self::Twap),
1178            other => Err(cow_errors::CowError::Parse {
1179                field: "OrderClassKind",
1180                reason: format!("unknown value: {other}"),
1181            }),
1182        }
1183    }
1184}
1185
1186/// Wrapper for [`OrderClassKind`] as it appears in the metadata schema.
1187#[derive(Debug, Clone, Serialize, Deserialize)]
1188#[serde(rename_all = "camelCase")]
1189pub struct OrderClass {
1190    /// Order classification kind.
1191    pub order_class: OrderClassKind,
1192}
1193
1194impl OrderClass {
1195    /// Construct an [`OrderClass`] from an [`OrderClassKind`].
1196    ///
1197    /// This is a thin wrapper — [`OrderClass`] exists because the JSON
1198    /// schema nests the classification under `{ "orderClass": "market" }`
1199    /// rather than using the enum value directly.
1200    ///
1201    /// # Parameters
1202    ///
1203    /// * `order_class` — the [`OrderClassKind`] variant.
1204    ///
1205    /// # Returns
1206    ///
1207    /// A new [`OrderClass`] wrapping the given kind.
1208    #[must_use]
1209    pub const fn new(order_class: OrderClassKind) -> Self {
1210        Self { order_class }
1211    }
1212}
1213
1214impl fmt::Display for OrderClass {
1215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1216        fmt::Display::fmt(&self.order_class, f)
1217    }
1218}
1219
1220/// Pre- and post-settlement interaction hooks.
1221///
1222/// Contains optional lists of [`CowHook`] entries that the settlement
1223/// contract will execute before (`pre`) and after (`post`) the trade.
1224/// When both lists are empty, the field is typically omitted from the JSON.
1225///
1226/// # Example
1227///
1228/// ```
1229/// use cow_app_data::{CowHook, OrderInteractionHooks};
1230///
1231/// let pre_hook =
1232///     CowHook::new("0x1234567890abcdef1234567890abcdef12345678", "0xabcdef00", "50000");
1233/// let hooks = OrderInteractionHooks::new(vec![pre_hook], vec![]);
1234/// assert!(hooks.has_pre());
1235/// assert!(!hooks.has_post());
1236/// ```
1237#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1238#[serde(rename_all = "camelCase")]
1239pub struct OrderInteractionHooks {
1240    /// Hook schema version.
1241    #[serde(skip_serializing_if = "Option::is_none")]
1242    pub version: Option<String>,
1243    /// Hooks executed before the settlement.
1244    #[serde(skip_serializing_if = "Option::is_none")]
1245    pub pre: Option<Vec<CowHook>>,
1246    /// Hooks executed after the settlement.
1247    #[serde(skip_serializing_if = "Option::is_none")]
1248    pub post: Option<Vec<CowHook>>,
1249}
1250
1251impl OrderInteractionHooks {
1252    /// Create hooks with the given pre- and post-execution lists.
1253    ///
1254    /// Empty vectors are stored as `None` (omitted from JSON) rather than
1255    /// as empty arrays, matching the `TypeScript` SDK's behaviour.
1256    ///
1257    /// # Parameters
1258    ///
1259    /// * `pre` — hooks to execute **before** the settlement trade.
1260    /// * `post` — hooks to execute **after** the settlement trade.
1261    ///
1262    /// # Returns
1263    ///
1264    /// A new [`OrderInteractionHooks`] with `version` set to `None`.
1265    ///
1266    /// # Example
1267    ///
1268    /// ```
1269    /// use cow_app_data::{CowHook, OrderInteractionHooks};
1270    ///
1271    /// let pre =
1272    ///     vec![CowHook::new("0x1234567890abcdef1234567890abcdef12345678", "0x095ea7b3", "50000")];
1273    /// let hooks = OrderInteractionHooks::new(pre, vec![]);
1274    /// assert!(hooks.has_pre());
1275    /// assert!(!hooks.has_post());
1276    /// ```
1277    #[must_use]
1278    pub fn new(pre: Vec<CowHook>, post: Vec<CowHook>) -> Self {
1279        Self {
1280            version: None,
1281            pre: if pre.is_empty() { None } else { Some(pre) },
1282            post: if post.is_empty() { None } else { Some(post) },
1283        }
1284    }
1285
1286    /// Override the hook schema version.
1287    ///
1288    /// # Parameters
1289    ///
1290    /// * `version` — the hook schema version string (e.g. `"0.2.0"`).
1291    ///
1292    /// # Returns
1293    ///
1294    /// `self` with `version` set.
1295    #[must_use]
1296    pub fn with_version(mut self, version: impl Into<String>) -> Self {
1297        self.version = Some(version.into());
1298        self
1299    }
1300
1301    /// Returns `true` if at least one pre-settlement hook is set.
1302    #[must_use]
1303    pub fn has_pre(&self) -> bool {
1304        self.pre.as_ref().is_some_and(|v| !v.is_empty())
1305    }
1306
1307    /// Returns `true` if at least one post-settlement hook is set.
1308    #[must_use]
1309    pub fn has_post(&self) -> bool {
1310        self.post.as_ref().is_some_and(|v| !v.is_empty())
1311    }
1312}
1313
1314// `CowHook` now lives in `cow-types`; see the `pub use` at the top of this
1315// module for the re-export.
1316
1317/// Widget integration metadata.
1318#[derive(Debug, Clone, Serialize, Deserialize)]
1319#[serde(rename_all = "camelCase")]
1320pub struct Widget {
1321    /// App code of the widget host.
1322    pub app_code: String,
1323    /// Deployment environment of the widget.
1324    #[serde(skip_serializing_if = "Option::is_none")]
1325    pub environment: Option<String>,
1326}
1327
1328impl Widget {
1329    /// Construct a new [`Widget`] for the given app code.
1330    ///
1331    /// Used when an order originates from an embedded widget integration.
1332    /// The `app_code` identifies the widget host application.
1333    ///
1334    /// # Parameters
1335    ///
1336    /// * `app_code` — the widget host's application identifier.
1337    ///
1338    /// # Returns
1339    ///
1340    /// A new [`Widget`] with `environment` set to `None`.
1341    ///
1342    /// # Example
1343    ///
1344    /// ```
1345    /// use cow_app_data::Widget;
1346    ///
1347    /// let w = Widget::new("WidgetHost").with_environment("production");
1348    /// assert_eq!(w.app_code, "WidgetHost");
1349    /// assert!(w.has_environment());
1350    /// ```
1351    #[must_use]
1352    pub fn new(app_code: impl Into<String>) -> Self {
1353        Self { app_code: app_code.into(), environment: None }
1354    }
1355
1356    /// Attach a deployment environment string (e.g. `"production"`).
1357    ///
1358    /// # Parameters
1359    ///
1360    /// * `env` — free-form environment label.
1361    ///
1362    /// # Returns
1363    ///
1364    /// `self` with `environment` set.
1365    #[must_use]
1366    pub fn with_environment(mut self, env: impl Into<String>) -> Self {
1367        self.environment = Some(env.into());
1368        self
1369    }
1370
1371    /// Returns `true` if a deployment environment string is set.
1372    #[must_use]
1373    pub const fn has_environment(&self) -> bool {
1374        self.environment.is_some()
1375    }
1376}
1377
1378impl fmt::Display for Widget {
1379    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1380        write!(f, "widget({})", self.app_code)
1381    }
1382}
1383
1384/// A single partner fee policy entry (schema v1.14.0).
1385///
1386/// Exactly one of `volume_bps`, `surplus_bps`, or `price_improvement_bps`
1387/// should be set; the other two should be `None`. Use the named
1388/// constructors [`volume`](Self::volume), [`surplus`](Self::surplus), or
1389/// [`price_improvement`](Self::price_improvement) to enforce this invariant.
1390///
1391/// All basis-point values must be ≤ 10 000 (= 100 %). Values above that
1392/// threshold will be flagged by [`validate_app_data_doc`](super::ipfs::validate_app_data_doc).
1393///
1394/// # Example
1395///
1396/// ```
1397/// use cow_app_data::PartnerFeeEntry;
1398///
1399/// // 0.5 % volume-based fee
1400/// let fee = PartnerFeeEntry::volume(50, "0xRecipientAddress");
1401/// assert_eq!(fee.volume_bps(), Some(50));
1402/// assert_eq!(fee.surplus_bps(), None);
1403/// ```
1404#[derive(Debug, Clone, Serialize, Deserialize)]
1405#[serde(rename_all = "camelCase")]
1406pub struct PartnerFeeEntry {
1407    /// Volume-based fee in basis points of the sell amount.
1408    #[serde(skip_serializing_if = "Option::is_none")]
1409    pub volume_bps: Option<u32>,
1410    /// Surplus-based fee in basis points.
1411    #[serde(skip_serializing_if = "Option::is_none")]
1412    pub surplus_bps: Option<u32>,
1413    /// Price-improvement fee in basis points.
1414    #[serde(skip_serializing_if = "Option::is_none")]
1415    pub price_improvement_bps: Option<u32>,
1416    /// Volume cap in basis points (required for surplus/price-improvement variants).
1417    #[serde(skip_serializing_if = "Option::is_none")]
1418    pub max_volume_bps: Option<u32>,
1419    /// Address that receives the fee.
1420    pub recipient: String,
1421}
1422
1423impl PartnerFeeEntry {
1424    /// Construct a volume-based fee entry.
1425    ///
1426    /// The fee is charged as a percentage of the sell amount. This is the
1427    /// most common fee model for integration partners.
1428    ///
1429    /// # Parameters
1430    ///
1431    /// * `volume_bps` — fee rate in basis points (e.g. `50` = 0.5 %). Must be ≤ 10 000 to pass
1432    ///   validation.
1433    /// * `recipient` — the `0x`-prefixed Ethereum address that receives the fee.
1434    ///
1435    /// # Returns
1436    ///
1437    /// A new [`PartnerFeeEntry`] with only `volume_bps` set.
1438    ///
1439    /// # Example
1440    ///
1441    /// ```
1442    /// use cow_app_data::PartnerFeeEntry;
1443    ///
1444    /// let fee = PartnerFeeEntry::volume(50, "0xRecipient");
1445    /// assert_eq!(fee.volume_bps(), Some(50));
1446    /// assert_eq!(fee.surplus_bps(), None);
1447    /// assert_eq!(fee.max_volume_bps(), None);
1448    /// ```
1449    #[must_use]
1450    pub fn volume(volume_bps: u32, recipient: impl Into<String>) -> Self {
1451        Self {
1452            volume_bps: Some(volume_bps),
1453            surplus_bps: None,
1454            price_improvement_bps: None,
1455            max_volume_bps: None,
1456            recipient: recipient.into(),
1457        }
1458    }
1459
1460    /// Construct a surplus-based fee entry.
1461    ///
1462    /// The fee is charged as a percentage of the surplus (the difference
1463    /// between the execution price and the limit price). A `max_volume_bps`
1464    /// cap is required to bound the fee as a percentage of the sell amount.
1465    ///
1466    /// # Parameters
1467    ///
1468    /// * `surplus_bps` — fee rate in basis points on the surplus.
1469    /// * `max_volume_bps` — cap on the fee as a percentage of sell amount.
1470    /// * `recipient` — the `0x`-prefixed Ethereum address that receives the fee.
1471    ///
1472    /// # Returns
1473    ///
1474    /// A new [`PartnerFeeEntry`] with `surplus_bps` and `max_volume_bps` set.
1475    ///
1476    /// # Example
1477    ///
1478    /// ```
1479    /// use cow_app_data::PartnerFeeEntry;
1480    ///
1481    /// let fee = PartnerFeeEntry::surplus(30, 100, "0xRecipient");
1482    /// assert_eq!(fee.surplus_bps(), Some(30));
1483    /// assert_eq!(fee.max_volume_bps(), Some(100));
1484    /// ```
1485    #[must_use]
1486    pub fn surplus(surplus_bps: u32, max_volume_bps: u32, recipient: impl Into<String>) -> Self {
1487        Self {
1488            volume_bps: None,
1489            surplus_bps: Some(surplus_bps),
1490            price_improvement_bps: None,
1491            max_volume_bps: Some(max_volume_bps),
1492            recipient: recipient.into(),
1493        }
1494    }
1495
1496    /// Construct a price-improvement fee entry.
1497    ///
1498    /// The fee is charged as a percentage of the price improvement the
1499    /// solver achieved. A `max_volume_bps` cap is required to bound the
1500    /// fee as a percentage of the sell amount.
1501    ///
1502    /// # Parameters
1503    ///
1504    /// * `price_improvement_bps` — fee rate in basis points on the price improvement.
1505    /// * `max_volume_bps` — cap on the fee as a percentage of sell amount.
1506    /// * `recipient` — the `0x`-prefixed Ethereum address that receives the fee.
1507    ///
1508    /// # Returns
1509    ///
1510    /// A new [`PartnerFeeEntry`] with `price_improvement_bps` and
1511    /// `max_volume_bps` set.
1512    ///
1513    /// # Example
1514    ///
1515    /// ```
1516    /// use cow_app_data::PartnerFeeEntry;
1517    ///
1518    /// let fee = PartnerFeeEntry::price_improvement(20, 80, "0xRecipient");
1519    /// assert_eq!(fee.price_improvement_bps(), Some(20));
1520    /// assert_eq!(fee.max_volume_bps(), Some(80));
1521    /// assert_eq!(fee.volume_bps(), None);
1522    /// ```
1523    #[must_use]
1524    pub fn price_improvement(
1525        price_improvement_bps: u32,
1526        max_volume_bps: u32,
1527        recipient: impl Into<String>,
1528    ) -> Self {
1529        Self {
1530            volume_bps: None,
1531            surplus_bps: None,
1532            price_improvement_bps: Some(price_improvement_bps),
1533            max_volume_bps: Some(max_volume_bps),
1534            recipient: recipient.into(),
1535        }
1536    }
1537
1538    /// Extract the volume fee in basis points, if present.
1539    ///
1540    /// # Returns
1541    ///
1542    /// `Some(bps)` if this is a volume-based fee entry, `None` otherwise.
1543    #[must_use]
1544    pub const fn volume_bps(&self) -> Option<u32> {
1545        self.volume_bps
1546    }
1547
1548    /// Extract the surplus fee in basis points, if present.
1549    ///
1550    /// # Returns
1551    ///
1552    /// `Some(bps)` if this is a surplus-based fee entry, `None` otherwise.
1553    #[must_use]
1554    pub const fn surplus_bps(&self) -> Option<u32> {
1555        self.surplus_bps
1556    }
1557
1558    /// Extract the price-improvement fee in basis points, if present.
1559    ///
1560    /// # Returns
1561    ///
1562    /// `Some(bps)` if this is a price-improvement fee entry, `None` otherwise.
1563    #[must_use]
1564    pub const fn price_improvement_bps(&self) -> Option<u32> {
1565        self.price_improvement_bps
1566    }
1567
1568    /// Extract the max-volume cap in basis points, if present.
1569    ///
1570    /// # Returns
1571    ///
1572    /// `Some(bps)` for surplus/price-improvement entries, `None` for volume entries.
1573    #[must_use]
1574    pub const fn max_volume_bps(&self) -> Option<u32> {
1575        self.max_volume_bps
1576    }
1577}
1578
1579impl fmt::Display for PartnerFeeEntry {
1580    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1581        if let Some(bps) = self.volume_bps {
1582            write!(f, "volume-fee({}bps, {})", bps, self.recipient)
1583        } else if let Some(bps) = self.surplus_bps {
1584            write!(f, "surplus-fee({}bps, {})", bps, self.recipient)
1585        } else if let Some(bps) = self.price_improvement_bps {
1586            write!(f, "price-improvement-fee({}bps, {})", bps, self.recipient)
1587        } else {
1588            write!(f, "fee({})", self.recipient)
1589        }
1590    }
1591}
1592/// Partner fee attached to a `CoW` Protocol order (schema v1.14.0).
1593///
1594/// Can be a single [`PartnerFeeEntry`] or a list of entries. The most common
1595/// form is a single volume-based entry: `PartnerFee::single(PartnerFeeEntry::volume(50, "0x..."))`.
1596///
1597/// Use [`get_partner_fee_bps`] to extract the first `volumeBps` value.
1598#[derive(Debug, Clone, Serialize, Deserialize)]
1599#[serde(untagged)]
1600pub enum PartnerFee {
1601    /// A single fee policy.
1602    Single(PartnerFeeEntry),
1603    /// A list of fee policies (one per fee type).
1604    Multiple(Vec<PartnerFeeEntry>),
1605}
1606
1607impl PartnerFee {
1608    /// Convenience constructor for the common single-entry case.
1609    ///
1610    /// Most integrations use a single fee policy. Wrap a
1611    /// [`PartnerFeeEntry`] in [`PartnerFee::Single`] for ergonomic use.
1612    ///
1613    /// # Parameters
1614    ///
1615    /// * `entry` — the fee policy entry (use [`PartnerFeeEntry::volume`],
1616    ///   [`PartnerFeeEntry::surplus`], or [`PartnerFeeEntry::price_improvement`] to create one).
1617    ///
1618    /// # Returns
1619    ///
1620    /// A [`PartnerFee::Single`] wrapping the given entry.
1621    ///
1622    /// # Example
1623    ///
1624    /// ```
1625    /// use cow_app_data::{PartnerFee, PartnerFeeEntry};
1626    ///
1627    /// let fee = PartnerFee::single(PartnerFeeEntry::volume(50, "0xAddr"));
1628    /// assert!(fee.is_single());
1629    /// assert_eq!(fee.count(), 1);
1630    /// ```
1631    #[must_use]
1632    pub const fn single(entry: PartnerFeeEntry) -> Self {
1633        Self::Single(entry)
1634    }
1635
1636    /// Iterate over all fee entries.
1637    ///
1638    /// Returns a single-element iterator for [`Single`](Self::Single), or
1639    /// iterates the full vector for [`Multiple`](Self::Multiple).
1640    ///
1641    /// # Returns
1642    ///
1643    /// An iterator yielding `&PartnerFeeEntry` references.
1644    pub fn entries(&self) -> impl Iterator<Item = &PartnerFeeEntry> {
1645        match self {
1646            Self::Single(e) => std::slice::from_ref(e).iter(),
1647            Self::Multiple(v) => v.iter(),
1648        }
1649    }
1650
1651    /// Returns `true` if this is a single-entry partner fee.
1652    #[must_use]
1653    pub const fn is_single(&self) -> bool {
1654        matches!(self, Self::Single(_))
1655    }
1656
1657    /// Returns `true` if this is a multi-entry partner fee.
1658    #[must_use]
1659    pub const fn is_multiple(&self) -> bool {
1660        matches!(self, Self::Multiple(_))
1661    }
1662
1663    /// Returns the number of fee entries: `1` for [`Single`](Self::Single),
1664    /// or the vector length for [`Multiple`](Self::Multiple).
1665    ///
1666    /// ```
1667    /// use cow_app_data::{PartnerFee, PartnerFeeEntry};
1668    ///
1669    /// let fee = PartnerFee::single(PartnerFeeEntry::volume(50, "0x1234"));
1670    /// assert_eq!(fee.count(), 1);
1671    ///
1672    /// let multi = PartnerFee::Multiple(vec![
1673    ///     PartnerFeeEntry::volume(50, "0x1234"),
1674    ///     PartnerFeeEntry::surplus(30, 100, "0x5678"),
1675    /// ]);
1676    /// assert_eq!(multi.count(), 2);
1677    /// ```
1678    #[must_use]
1679    pub const fn count(&self) -> usize {
1680        match self {
1681            Self::Single(_) => 1,
1682            Self::Multiple(v) => v.len(),
1683        }
1684    }
1685}
1686
1687impl fmt::Display for PartnerFee {
1688    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1689        match self {
1690            Self::Single(e) => fmt::Display::fmt(e, f),
1691            Self::Multiple(v) => write!(f, "fees({})", v.len()),
1692        }
1693    }
1694}
1695/// Extract the first `volumeBps` value from an optional [`PartnerFee`].
1696///
1697/// Iterates over the fee entries and returns the first
1698/// [`PartnerFeeEntry::volume_bps`] that is `Some`. Returns `None` if `fee`
1699/// is `None` or no entry has a volume-based fee.
1700///
1701/// Mirrors `getPartnerFeeBps` from the `@cowprotocol/app-data` `TypeScript`
1702/// package.
1703///
1704/// # Parameters
1705///
1706/// * `fee` — optional reference to a [`PartnerFee`].
1707///
1708/// # Returns
1709///
1710/// The first volume-based fee in basis points, or `None`.
1711///
1712/// # Example
1713///
1714/// ```
1715/// use cow_app_data::{PartnerFee, PartnerFeeEntry, get_partner_fee_bps};
1716///
1717/// let fee = PartnerFee::single(PartnerFeeEntry::volume(50, "0x1234"));
1718/// assert_eq!(get_partner_fee_bps(Some(&fee)), Some(50));
1719/// assert_eq!(get_partner_fee_bps(None), None);
1720/// ```
1721#[must_use]
1722pub fn get_partner_fee_bps(fee: Option<&PartnerFee>) -> Option<u32> {
1723    fee?.entries().find_map(PartnerFeeEntry::volume_bps)
1724}
1725
1726/// Links this order to a previously submitted order it supersedes.
1727#[derive(Debug, Clone, Serialize, Deserialize)]
1728#[serde(rename_all = "camelCase")]
1729pub struct ReplacedOrder {
1730    /// UID of the order being replaced.
1731    pub uid: String,
1732}
1733
1734impl ReplacedOrder {
1735    /// Construct a [`ReplacedOrder`] reference from the given order UID.
1736    ///
1737    /// # Parameters
1738    ///
1739    /// * `uid` — the `0x`-prefixed order UID of the order being replaced. Must be 56 bytes (`0x` +
1740    ///   112 hex chars) to pass validation.
1741    ///
1742    /// # Returns
1743    ///
1744    /// A new [`ReplacedOrder`] instance.
1745    ///
1746    /// # Example
1747    ///
1748    /// ```
1749    /// use cow_app_data::ReplacedOrder;
1750    ///
1751    /// let uid = format!("0x{}", "ab".repeat(56)); // 0x + 112 hex chars
1752    /// let ro = ReplacedOrder::new(&uid);
1753    /// assert_eq!(ro.uid.len(), 114);
1754    /// ```
1755    #[must_use]
1756    pub fn new(uid: impl Into<String>) -> Self {
1757        Self { uid: uid.into() }
1758    }
1759}
1760
1761impl fmt::Display for ReplacedOrder {
1762    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1763        write!(f, "replaced({})", self.uid)
1764    }
1765}
1766
1767/// Cross-chain bridging metadata.
1768///
1769/// Embedded in [`Metadata`] when an order was placed via a cross-chain
1770/// bridge (e.g. Across, Bungee). Records the bridge provider, destination
1771/// chain, destination token, and optional quote/attestation data so solvers
1772/// and analytics can trace cross-chain flows.
1773///
1774/// # Example
1775///
1776/// ```
1777/// use cow_app_data::types::Bridging;
1778///
1779/// let bridging = Bridging::new("across", "42161", "0xTokenOnArbitrum").with_quote_id("quote-123");
1780/// assert!(bridging.has_quote_id());
1781/// ```
1782#[derive(Debug, Clone, Serialize, Deserialize)]
1783#[serde(rename_all = "camelCase")]
1784pub struct Bridging {
1785    /// Bridge provider identifier.
1786    pub provider: String,
1787    /// Destination chain ID (as a decimal string).
1788    pub destination_chain_id: String,
1789    /// Destination token contract address.
1790    pub destination_token_address: String,
1791    /// Bridge quote identifier, if available.
1792    #[serde(skip_serializing_if = "Option::is_none")]
1793    pub quote_id: Option<String>,
1794    /// Bridge quote signature bytes, if available.
1795    #[serde(skip_serializing_if = "Option::is_none")]
1796    pub quote_signature: Option<String>,
1797    /// Bridge attestation signature bytes, if available.
1798    #[serde(skip_serializing_if = "Option::is_none")]
1799    pub attestation_signature: Option<String>,
1800    /// Opaque bridge quote body, if available.
1801    #[serde(skip_serializing_if = "Option::is_none")]
1802    pub quote_body: Option<String>,
1803}
1804
1805impl Bridging {
1806    /// Construct a [`Bridging`] record with the three required fields.
1807    ///
1808    /// Optional fields (quote ID, signatures, quote body) can be attached
1809    /// afterwards via the `with_*` builder methods.
1810    ///
1811    /// # Parameters
1812    ///
1813    /// * `provider` — bridge provider identifier (e.g. `"across"`, `"bungee"`).
1814    /// * `destination_chain_id` — target chain ID as a decimal string (e.g. `"42161"` for Arbitrum
1815    ///   One).
1816    /// * `destination_token_address` — `0x`-prefixed contract address of the token on the
1817    ///   destination chain.
1818    ///
1819    /// # Returns
1820    ///
1821    /// A new [`Bridging`] with all optional fields set to `None`.
1822    ///
1823    /// # Example
1824    ///
1825    /// ```
1826    /// use cow_app_data::types::Bridging;
1827    ///
1828    /// let b = Bridging::new("across", "42161", "0xTokenOnArbitrum");
1829    /// assert_eq!(b.provider, "across");
1830    /// assert!(!b.has_quote_id());
1831    /// ```
1832    #[must_use]
1833    pub fn new(
1834        provider: impl Into<String>,
1835        destination_chain_id: impl Into<String>,
1836        destination_token_address: impl Into<String>,
1837    ) -> Self {
1838        Self {
1839            provider: provider.into(),
1840            destination_chain_id: destination_chain_id.into(),
1841            destination_token_address: destination_token_address.into(),
1842            quote_id: None,
1843            quote_signature: None,
1844            attestation_signature: None,
1845            quote_body: None,
1846        }
1847    }
1848
1849    /// Attach a bridge quote identifier.
1850    ///
1851    /// # Parameters
1852    ///
1853    /// * `id` — the quote identifier returned by the bridge provider.
1854    ///
1855    /// # Returns
1856    ///
1857    /// `self` with `quote_id` set.
1858    #[must_use]
1859    pub fn with_quote_id(mut self, id: impl Into<String>) -> Self {
1860        self.quote_id = Some(id.into());
1861        self
1862    }
1863
1864    /// Attach a bridge quote signature.
1865    ///
1866    /// # Parameters
1867    ///
1868    /// * `sig` — the hex-encoded signature bytes from the bridge provider.
1869    ///
1870    /// # Returns
1871    ///
1872    /// `self` with `quote_signature` set.
1873    #[must_use]
1874    pub fn with_quote_signature(mut self, sig: impl Into<String>) -> Self {
1875        self.quote_signature = Some(sig.into());
1876        self
1877    }
1878
1879    /// Attach an attestation signature.
1880    ///
1881    /// # Parameters
1882    ///
1883    /// * `sig` — the hex-encoded attestation signature bytes.
1884    ///
1885    /// # Returns
1886    ///
1887    /// `self` with `attestation_signature` set.
1888    #[must_use]
1889    pub fn with_attestation_signature(mut self, sig: impl Into<String>) -> Self {
1890        self.attestation_signature = Some(sig.into());
1891        self
1892    }
1893
1894    /// Attach an opaque bridge quote body.
1895    ///
1896    /// # Parameters
1897    ///
1898    /// * `body` — the raw quote body string from the bridge provider.
1899    ///
1900    /// # Returns
1901    ///
1902    /// `self` with `quote_body` set.
1903    #[must_use]
1904    pub fn with_quote_body(mut self, body: impl Into<String>) -> Self {
1905        self.quote_body = Some(body.into());
1906        self
1907    }
1908
1909    /// Returns `true` if a bridge quote identifier is set.
1910    #[must_use]
1911    pub const fn has_quote_id(&self) -> bool {
1912        self.quote_id.is_some()
1913    }
1914
1915    /// Returns `true` if a bridge quote signature is set.
1916    #[must_use]
1917    pub const fn has_quote_signature(&self) -> bool {
1918        self.quote_signature.is_some()
1919    }
1920
1921    /// Returns `true` if an attestation signature is set.
1922    #[must_use]
1923    pub const fn has_attestation_signature(&self) -> bool {
1924        self.attestation_signature.is_some()
1925    }
1926
1927    /// Returns `true` if an opaque quote body is set.
1928    #[must_use]
1929    pub const fn has_quote_body(&self) -> bool {
1930        self.quote_body.is_some()
1931    }
1932}
1933
1934impl fmt::Display for Bridging {
1935    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1936        write!(f, "bridge({}, chain={})", self.provider, self.destination_chain_id)
1937    }
1938}
1939
1940/// Flash loan metadata.
1941///
1942/// Embedded in [`Metadata`] when the order uses a flash loan for execution.
1943/// Records the loan amount, liquidity provider, protocol adapter, receiver,
1944/// and token address so the settlement contract and solvers can reconstruct
1945/// the flash-loan flow.
1946///
1947/// # Example
1948///
1949/// ```
1950/// use cow_app_data::types::Flashloan;
1951///
1952/// let fl = Flashloan::new(
1953///     "1000000000000000000", // 1 ETH in wei
1954///     "0xLiquidityProvider",
1955///     "0xTokenAddress",
1956/// )
1957/// .with_protocol_adapter("0xAdapterAddress")
1958/// .with_receiver("0xReceiverAddress");
1959/// ```
1960#[derive(Debug, Clone, Serialize, Deserialize)]
1961#[serde(rename_all = "camelCase")]
1962pub struct Flashloan {
1963    /// Loan amount in token atoms (decimal string).
1964    pub loan_amount: String,
1965    /// Address of the liquidity provider.
1966    pub liquidity_provider_address: String,
1967    /// Address of the protocol adapter contract.
1968    pub protocol_adapter_address: String,
1969    /// Address that receives the flash loan proceeds.
1970    pub receiver_address: String,
1971    /// Address of the token being flash-loaned.
1972    pub token_address: String,
1973}
1974
1975impl Flashloan {
1976    /// Construct a [`Flashloan`] record with the core required fields.
1977    ///
1978    /// `protocol_adapter_address` and `receiver_address` default to empty
1979    /// strings; set them via [`with_protocol_adapter`](Self::with_protocol_adapter)
1980    /// and [`with_receiver`](Self::with_receiver).
1981    ///
1982    /// # Parameters
1983    ///
1984    /// * `loan_amount` — the flash-loan amount in token atoms (decimal string, e.g.
1985    ///   `"1000000000000000000"` for 1 ETH).
1986    /// * `liquidity_provider_address` — `0x`-prefixed address of the liquidity pool providing the
1987    ///   flash loan.
1988    /// * `token_address` — `0x`-prefixed address of the token being flash-loaned.
1989    ///
1990    /// # Returns
1991    ///
1992    /// A new [`Flashloan`] with adapter and receiver addresses empty.
1993    ///
1994    /// # Example
1995    ///
1996    /// ```
1997    /// use cow_app_data::types::Flashloan;
1998    ///
1999    /// let fl = Flashloan::new("1000000000000000000", "0xPool", "0xToken")
2000    ///     .with_protocol_adapter("0xAdapter")
2001    ///     .with_receiver("0xReceiver");
2002    /// assert_eq!(fl.loan_amount, "1000000000000000000");
2003    /// assert_eq!(fl.protocol_adapter_address, "0xAdapter");
2004    /// ```
2005    #[must_use]
2006    pub fn new(
2007        loan_amount: impl Into<String>,
2008        liquidity_provider_address: impl Into<String>,
2009        token_address: impl Into<String>,
2010    ) -> Self {
2011        Self {
2012            loan_amount: loan_amount.into(),
2013            liquidity_provider_address: liquidity_provider_address.into(),
2014            protocol_adapter_address: String::new(),
2015            receiver_address: String::new(),
2016            token_address: token_address.into(),
2017        }
2018    }
2019
2020    /// Set the protocol adapter contract address.
2021    ///
2022    /// The adapter contract mediates between the settlement contract and the
2023    /// flash-loan liquidity provider.
2024    ///
2025    /// # Parameters
2026    ///
2027    /// * `address` — `0x`-prefixed Ethereum address.
2028    ///
2029    /// # Returns
2030    ///
2031    /// `self` with `protocol_adapter_address` set.
2032    #[must_use]
2033    pub fn with_protocol_adapter(mut self, address: impl Into<String>) -> Self {
2034        self.protocol_adapter_address = address.into();
2035        self
2036    }
2037
2038    /// Set the receiver address for flash loan proceeds.
2039    ///
2040    /// # Parameters
2041    ///
2042    /// * `address` — `0x`-prefixed Ethereum address that receives the borrowed tokens.
2043    ///
2044    /// # Returns
2045    ///
2046    /// `self` with `receiver_address` set.
2047    #[must_use]
2048    pub fn with_receiver(mut self, address: impl Into<String>) -> Self {
2049        self.receiver_address = address.into();
2050        self
2051    }
2052}
2053
2054impl fmt::Display for Flashloan {
2055    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2056        write!(f, "flashloan({}, amount={})", self.token_address, self.loan_amount)
2057    }
2058}
2059
2060/// A single token wrapper entry.
2061#[derive(Debug, Clone, Serialize, Deserialize)]
2062#[serde(rename_all = "camelCase")]
2063pub struct WrapperEntry {
2064    /// Address of the wrapper contract.
2065    pub wrapper_address: String,
2066    /// Optional wrapper-specific data.
2067    #[serde(skip_serializing_if = "Option::is_none")]
2068    pub wrapper_data: Option<String>,
2069    /// Whether this wrapper can be omitted if not needed.
2070    #[serde(skip_serializing_if = "Option::is_none")]
2071    pub is_omittable: Option<bool>,
2072}
2073
2074impl WrapperEntry {
2075    /// Construct a [`WrapperEntry`] with just the wrapper contract address.
2076    ///
2077    /// Wrapper entries describe token wrapping/unwrapping operations applied
2078    /// during order execution (e.g. WETH ↔ ETH).
2079    ///
2080    /// # Parameters
2081    ///
2082    /// * `wrapper_address` — `0x`-prefixed address of the wrapper contract.
2083    ///
2084    /// # Returns
2085    ///
2086    /// A new [`WrapperEntry`] with `wrapper_data` and `is_omittable` unset.
2087    ///
2088    /// # Example
2089    ///
2090    /// ```
2091    /// use cow_app_data::types::WrapperEntry;
2092    ///
2093    /// let w = WrapperEntry::new("0xWrapperContract").with_is_omittable(true);
2094    /// assert!(w.is_omittable());
2095    /// ```
2096    #[must_use]
2097    pub fn new(wrapper_address: impl Into<String>) -> Self {
2098        Self { wrapper_address: wrapper_address.into(), wrapper_data: None, is_omittable: None }
2099    }
2100
2101    /// Attach wrapper-specific data (e.g. ABI-encoded parameters).
2102    ///
2103    /// # Parameters
2104    ///
2105    /// * `data` — opaque data string specific to the wrapper contract.
2106    ///
2107    /// # Returns
2108    ///
2109    /// `self` with `wrapper_data` set.
2110    #[must_use]
2111    pub fn with_wrapper_data(mut self, data: impl Into<String>) -> Self {
2112        self.wrapper_data = Some(data.into());
2113        self
2114    }
2115
2116    /// Mark this wrapper as omittable when not needed.
2117    ///
2118    /// When `true`, the settlement contract may skip this wrapper if the
2119    /// wrapping/unwrapping step is unnecessary for the specific execution
2120    /// path.
2121    ///
2122    /// # Parameters
2123    ///
2124    /// * `omittable` — whether the wrapper can be skipped.
2125    ///
2126    /// # Returns
2127    ///
2128    /// `self` with `is_omittable` set.
2129    #[must_use]
2130    pub const fn with_is_omittable(mut self, omittable: bool) -> Self {
2131        self.is_omittable = Some(omittable);
2132        self
2133    }
2134
2135    /// Returns `true` if wrapper-specific data is attached.
2136    #[must_use]
2137    pub const fn has_wrapper_data(&self) -> bool {
2138        self.wrapper_data.is_some()
2139    }
2140
2141    /// Returns `true` if the omittable flag is explicitly set.
2142    #[must_use]
2143    pub const fn has_is_omittable(&self) -> bool {
2144        self.is_omittable.is_some()
2145    }
2146
2147    /// Returns `true` if this wrapper is explicitly marked as omittable.
2148    #[must_use]
2149    pub const fn is_omittable(&self) -> bool {
2150        matches!(self.is_omittable, Some(true))
2151    }
2152}
2153
2154impl fmt::Display for WrapperEntry {
2155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2156        write!(f, "wrapper({})", self.wrapper_address)
2157    }
2158}
2159
2160/// User acceptance record for terms of service.
2161#[derive(Debug, Clone, Serialize, Deserialize)]
2162#[serde(rename_all = "camelCase")]
2163pub struct UserConsent {
2164    /// Identifier or URL of the accepted terms.
2165    pub terms: String,
2166    /// ISO 8601 date when the terms were accepted.
2167    pub accepted_date: String,
2168}
2169
2170impl UserConsent {
2171    /// Construct a [`UserConsent`] record for terms-of-service acceptance.
2172    ///
2173    /// # Parameters
2174    ///
2175    /// * `terms` — identifier or URL of the accepted terms of service.
2176    /// * `accepted_date` — ISO 8601 date string when the terms were accepted (e.g. `"2025-04-07"`).
2177    ///
2178    /// # Returns
2179    ///
2180    /// A new [`UserConsent`] instance.
2181    ///
2182    /// # Example
2183    ///
2184    /// ```
2185    /// use cow_app_data::types::UserConsent;
2186    ///
2187    /// let consent = UserConsent::new("https://cow.fi/tos", "2025-04-07");
2188    /// assert_eq!(consent.terms, "https://cow.fi/tos");
2189    /// ```
2190    #[must_use]
2191    pub fn new(terms: impl Into<String>, accepted_date: impl Into<String>) -> Self {
2192        Self { terms: terms.into(), accepted_date: accepted_date.into() }
2193    }
2194}
2195
2196impl fmt::Display for UserConsent {
2197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2198        write!(f, "consent({}, {})", self.terms, self.accepted_date)
2199    }
2200}
2201
2202#[cfg(test)]
2203mod tests {
2204    use super::*;
2205
2206    // ── Constants ──
2207
2208    #[test]
2209    fn constants_are_expected_values() {
2210        assert_eq!(LATEST_APP_DATA_VERSION, "1.14.0");
2211        assert_eq!(LATEST_QUOTE_METADATA_VERSION, "1.1.0");
2212        assert_eq!(LATEST_REFERRER_METADATA_VERSION, "1.0.0");
2213        assert_eq!(LATEST_ORDER_CLASS_METADATA_VERSION, "0.3.0");
2214        assert_eq!(LATEST_UTM_METADATA_VERSION, "0.3.0");
2215        assert_eq!(LATEST_HOOKS_METADATA_VERSION, "0.2.0");
2216        assert_eq!(LATEST_SIGNER_METADATA_VERSION, "0.1.0");
2217        assert_eq!(LATEST_WIDGET_METADATA_VERSION, "0.1.0");
2218        assert_eq!(LATEST_PARTNER_FEE_METADATA_VERSION, "1.0.0");
2219        assert_eq!(LATEST_REPLACED_ORDER_METADATA_VERSION, "0.1.0");
2220        assert_eq!(LATEST_WRAPPERS_METADATA_VERSION, "0.2.0");
2221        assert_eq!(LATEST_USER_CONSENTS_METADATA_VERSION, "0.1.0");
2222    }
2223
2224    // ── AppDataDoc ──
2225
2226    #[test]
2227    fn app_data_doc_new() {
2228        let doc = AppDataDoc::new("TestApp");
2229        assert_eq!(doc.version, LATEST_APP_DATA_VERSION);
2230        assert_eq!(doc.app_code.as_deref(), Some("TestApp"));
2231        assert!(doc.environment.is_none());
2232        assert!(!doc.metadata.has_referrer());
2233    }
2234
2235    #[test]
2236    fn app_data_doc_default() {
2237        let doc = AppDataDoc::default();
2238        assert!(doc.version.is_empty());
2239        assert!(doc.app_code.is_none());
2240        assert!(doc.environment.is_none());
2241    }
2242
2243    #[test]
2244    fn app_data_doc_with_environment() {
2245        let doc = AppDataDoc::new("App").with_environment("staging");
2246        assert_eq!(doc.environment.as_deref(), Some("staging"));
2247    }
2248
2249    #[test]
2250    fn app_data_doc_with_referrer() {
2251        let doc = AppDataDoc::new("App").with_referrer(Referrer::code("COWRS"));
2252        assert!(doc.metadata.has_referrer());
2253        assert_eq!(doc.metadata.referrer.unwrap().as_code(), Some("COWRS"));
2254    }
2255
2256    #[test]
2257    fn app_data_doc_with_utm() {
2258        let utm = Utm::new().with_source("twitter");
2259        let doc = AppDataDoc::new("App").with_utm(utm);
2260        assert!(doc.metadata.has_utm());
2261    }
2262
2263    #[test]
2264    fn app_data_doc_with_hooks() {
2265        let hook = CowHook::new("0xTarget", "0xData", "50000");
2266        let hooks = OrderInteractionHooks::new(vec![hook], vec![]);
2267        let doc = AppDataDoc::new("App").with_hooks(hooks);
2268        assert!(doc.metadata.has_hooks());
2269    }
2270
2271    #[test]
2272    fn app_data_doc_with_partner_fee() {
2273        let fee = PartnerFee::single(PartnerFeeEntry::volume(50, "0xAddr"));
2274        let doc = AppDataDoc::new("App").with_partner_fee(fee);
2275        assert!(doc.metadata.has_partner_fee());
2276    }
2277
2278    #[test]
2279    fn app_data_doc_with_replaced_order() {
2280        let uid = format!("0x{}", "ab".repeat(56));
2281        let doc = AppDataDoc::new("App").with_replaced_order(&uid);
2282        assert!(doc.metadata.has_replaced_order());
2283        assert_eq!(doc.metadata.replaced_order.unwrap().uid, uid);
2284    }
2285
2286    #[test]
2287    fn app_data_doc_with_signer() {
2288        let doc = AppDataDoc::new("App").with_signer("0xSignerAddr");
2289        assert!(doc.metadata.has_signer());
2290        assert_eq!(doc.metadata.signer.as_deref(), Some("0xSignerAddr"));
2291    }
2292
2293    #[test]
2294    fn app_data_doc_with_order_class() {
2295        let doc = AppDataDoc::new("App").with_order_class(OrderClassKind::Limit);
2296        assert!(doc.metadata.has_order_class());
2297        assert_eq!(doc.metadata.order_class.unwrap().order_class, OrderClassKind::Limit);
2298    }
2299
2300    #[test]
2301    fn app_data_doc_with_bridging() {
2302        let b = Bridging::new("across", "42161", "0xToken");
2303        let doc = AppDataDoc::new("App").with_bridging(b);
2304        assert!(doc.metadata.has_bridging());
2305    }
2306
2307    #[test]
2308    fn app_data_doc_with_flashloan() {
2309        let fl = Flashloan::new("1000", "0xPool", "0xToken");
2310        let doc = AppDataDoc::new("App").with_flashloan(fl);
2311        assert!(doc.metadata.has_flashloan());
2312    }
2313
2314    #[test]
2315    fn app_data_doc_with_wrappers() {
2316        let w = WrapperEntry::new("0xWrapper");
2317        let doc = AppDataDoc::new("App").with_wrappers(vec![w]);
2318        assert!(doc.metadata.has_wrappers());
2319    }
2320
2321    #[test]
2322    fn app_data_doc_with_user_consents() {
2323        let c = UserConsent::new("https://cow.fi/tos", "2025-04-07");
2324        let doc = AppDataDoc::new("App").with_user_consents(vec![c]);
2325        assert!(doc.metadata.has_user_consents());
2326    }
2327
2328    #[test]
2329    fn app_data_doc_display() {
2330        let doc = AppDataDoc::new("MyApp");
2331        assert_eq!(doc.to_string(), "app-data(v1.14.0, code=MyApp)");
2332
2333        let doc_no_code = AppDataDoc::default();
2334        assert_eq!(doc_no_code.to_string(), "app-data(v, code=none)");
2335    }
2336
2337    // ── Metadata ──
2338
2339    #[test]
2340    fn metadata_default_all_none() {
2341        let m = Metadata::default();
2342        assert!(!m.has_referrer());
2343        assert!(!m.has_utm());
2344        assert!(!m.has_quote());
2345        assert!(!m.has_order_class());
2346        assert!(!m.has_hooks());
2347        assert!(!m.has_widget());
2348        assert!(!m.has_partner_fee());
2349        assert!(!m.has_replaced_order());
2350        assert!(!m.has_signer());
2351        assert!(!m.has_bridging());
2352        assert!(!m.has_flashloan());
2353        assert!(!m.has_wrappers());
2354        assert!(!m.has_user_consents());
2355    }
2356
2357    #[test]
2358    fn metadata_with_referrer() {
2359        let m = Metadata::default().with_referrer(Referrer::address("0xAddr"));
2360        assert!(m.has_referrer());
2361    }
2362
2363    #[test]
2364    fn metadata_with_utm() {
2365        let m = Metadata::default().with_utm(Utm::new());
2366        assert!(m.has_utm());
2367    }
2368
2369    #[test]
2370    fn metadata_with_quote() {
2371        let m = Metadata::default().with_quote(Quote::new(50));
2372        assert!(m.has_quote());
2373    }
2374
2375    #[test]
2376    fn metadata_with_order_class() {
2377        let oc = OrderClass::new(OrderClassKind::Market);
2378        let m = Metadata::default().with_order_class(oc);
2379        assert!(m.has_order_class());
2380    }
2381
2382    #[test]
2383    fn metadata_with_hooks() {
2384        let hooks = OrderInteractionHooks::new(vec![], vec![]);
2385        let m = Metadata::default().with_hooks(hooks);
2386        assert!(m.has_hooks());
2387    }
2388
2389    #[test]
2390    fn metadata_with_widget() {
2391        let w = Widget::new("Host");
2392        let m = Metadata::default().with_widget(w);
2393        assert!(m.has_widget());
2394    }
2395
2396    #[test]
2397    fn metadata_with_partner_fee() {
2398        let fee = PartnerFee::single(PartnerFeeEntry::volume(10, "0x1"));
2399        let m = Metadata::default().with_partner_fee(fee);
2400        assert!(m.has_partner_fee());
2401    }
2402
2403    #[test]
2404    fn metadata_with_replaced_order() {
2405        let ro = ReplacedOrder::new("0xUID");
2406        let m = Metadata::default().with_replaced_order(ro);
2407        assert!(m.has_replaced_order());
2408    }
2409
2410    #[test]
2411    fn metadata_with_signer() {
2412        let m = Metadata::default().with_signer("0xSigner");
2413        assert!(m.has_signer());
2414    }
2415
2416    #[test]
2417    fn metadata_with_bridging() {
2418        let b = Bridging::new("across", "1", "0xToken");
2419        let m = Metadata::default().with_bridging(b);
2420        assert!(m.has_bridging());
2421    }
2422
2423    #[test]
2424    fn metadata_with_flashloan() {
2425        let fl = Flashloan::new("100", "0xPool", "0xToken");
2426        let m = Metadata::default().with_flashloan(fl);
2427        assert!(m.has_flashloan());
2428    }
2429
2430    #[test]
2431    fn metadata_with_wrappers() {
2432        let m = Metadata::default().with_wrappers(vec![WrapperEntry::new("0xW")]);
2433        assert!(m.has_wrappers());
2434    }
2435
2436    #[test]
2437    fn metadata_with_wrappers_empty_is_false() {
2438        let m = Metadata::default().with_wrappers(vec![]);
2439        // has_wrappers checks is_some_and(!is_empty)
2440        assert!(!m.has_wrappers());
2441    }
2442
2443    #[test]
2444    fn metadata_with_user_consents() {
2445        let c = UserConsent::new("tos", "2025-01-01");
2446        let m = Metadata::default().with_user_consents(vec![c]);
2447        assert!(m.has_user_consents());
2448    }
2449
2450    #[test]
2451    fn metadata_with_user_consents_empty_is_false() {
2452        let m = Metadata::default().with_user_consents(vec![]);
2453        assert!(!m.has_user_consents());
2454    }
2455
2456    #[test]
2457    fn metadata_display() {
2458        assert_eq!(Metadata::default().to_string(), "metadata");
2459    }
2460
2461    // ── Referrer ──
2462
2463    #[test]
2464    fn referrer_address_variant() {
2465        let r = Referrer::address("0xb6BAd41ae76A11D10f7b0E664C5007b908bC77C9");
2466        assert_eq!(r.as_address(), Some("0xb6BAd41ae76A11D10f7b0E664C5007b908bC77C9"));
2467        assert_eq!(r.as_code(), None);
2468    }
2469
2470    #[test]
2471    fn referrer_code_variant() {
2472        let r = Referrer::code("COWRS-PARTNER");
2473        assert_eq!(r.as_code(), Some("COWRS-PARTNER"));
2474        assert_eq!(r.as_address(), None);
2475    }
2476
2477    #[test]
2478    #[allow(deprecated, reason = "testing deprecated Referrer::new alias")]
2479    fn referrer_new_deprecated_alias() {
2480        let r = Referrer::new("0xAddr");
2481        assert_eq!(r.as_address(), Some("0xAddr"));
2482    }
2483
2484    #[test]
2485    fn referrer_display_address() {
2486        let r = Referrer::address("0xABC");
2487        assert_eq!(r.to_string(), "referrer(address=0xABC)");
2488    }
2489
2490    #[test]
2491    fn referrer_display_code() {
2492        let r = Referrer::code("COWRS");
2493        assert_eq!(r.to_string(), "referrer(code=COWRS)");
2494    }
2495
2496    #[test]
2497    fn referrer_serde_address_roundtrip() {
2498        let r = Referrer::address("0xb6BAd41ae76A11D10f7b0E664C5007b908bC77C9");
2499        let json = serde_json::to_string(&r).unwrap();
2500        assert!(json.contains("\"address\""));
2501        let r2: Referrer = serde_json::from_str(&json).unwrap();
2502        assert_eq!(r2.as_address(), Some("0xb6BAd41ae76A11D10f7b0E664C5007b908bC77C9"));
2503    }
2504
2505    #[test]
2506    fn referrer_serde_code_roundtrip() {
2507        let r = Referrer::code("COWRS");
2508        let json = serde_json::to_string(&r).unwrap();
2509        assert!(json.contains("\"code\""));
2510        let r2: Referrer = serde_json::from_str(&json).unwrap();
2511        assert_eq!(r2.as_code(), Some("COWRS"));
2512    }
2513
2514    // ── Utm ──
2515
2516    #[test]
2517    fn utm_new_all_none() {
2518        let utm = Utm::new();
2519        assert!(!utm.has_source());
2520        assert!(!utm.has_medium());
2521        assert!(!utm.has_campaign());
2522        assert!(!utm.has_content());
2523        assert!(!utm.has_term());
2524    }
2525
2526    #[test]
2527    fn utm_with_all_fields() {
2528        let utm = Utm::new()
2529            .with_source("google")
2530            .with_medium("cpc")
2531            .with_campaign("launch")
2532            .with_content("banner")
2533            .with_term("cow protocol");
2534        assert!(utm.has_source());
2535        assert!(utm.has_medium());
2536        assert!(utm.has_campaign());
2537        assert!(utm.has_content());
2538        assert!(utm.has_term());
2539        assert_eq!(utm.utm_source.as_deref(), Some("google"));
2540        assert_eq!(utm.utm_medium.as_deref(), Some("cpc"));
2541        assert_eq!(utm.utm_campaign.as_deref(), Some("launch"));
2542        assert_eq!(utm.utm_content.as_deref(), Some("banner"));
2543        assert_eq!(utm.utm_term.as_deref(), Some("cow protocol"));
2544    }
2545
2546    #[test]
2547    fn utm_display() {
2548        let utm = Utm::new().with_source("twitter");
2549        assert_eq!(utm.to_string(), "utm(source=twitter)");
2550
2551        let utm_none = Utm::new();
2552        assert_eq!(utm_none.to_string(), "utm(source=none)");
2553    }
2554
2555    #[test]
2556    fn utm_serde_roundtrip() {
2557        let utm = Utm::new().with_source("google").with_campaign("test");
2558        let json = serde_json::to_string(&utm).unwrap();
2559        let utm2: Utm = serde_json::from_str(&json).unwrap();
2560        assert_eq!(utm2.utm_source.as_deref(), Some("google"));
2561        assert_eq!(utm2.utm_campaign.as_deref(), Some("test"));
2562        assert!(utm2.utm_medium.is_none());
2563    }
2564
2565    // ── Quote ──
2566
2567    #[test]
2568    fn quote_new() {
2569        let q = Quote::new(50);
2570        assert_eq!(q.slippage_bips, 50);
2571        assert_eq!(q.smart_slippage, None);
2572    }
2573
2574    #[test]
2575    fn quote_with_smart_slippage() {
2576        let q = Quote::new(100).with_smart_slippage();
2577        assert_eq!(q.slippage_bips, 100);
2578        assert_eq!(q.smart_slippage, Some(true));
2579    }
2580
2581    #[test]
2582    fn quote_display() {
2583        assert_eq!(Quote::new(50).to_string(), "quote(50bips)");
2584    }
2585
2586    #[test]
2587    fn quote_serde_roundtrip() {
2588        let q = Quote::new(75).with_smart_slippage();
2589        let json = serde_json::to_string(&q).unwrap();
2590        let q2: Quote = serde_json::from_str(&json).unwrap();
2591        assert_eq!(q2.slippage_bips, 75);
2592        assert_eq!(q2.smart_slippage, Some(true));
2593    }
2594
2595    // ── OrderClassKind ──
2596
2597    #[test]
2598    fn order_class_kind_as_str() {
2599        assert_eq!(OrderClassKind::Market.as_str(), "market");
2600        assert_eq!(OrderClassKind::Limit.as_str(), "limit");
2601        assert_eq!(OrderClassKind::Liquidity.as_str(), "liquidity");
2602        assert_eq!(OrderClassKind::Twap.as_str(), "twap");
2603    }
2604
2605    #[test]
2606    fn order_class_kind_is_predicates() {
2607        assert!(OrderClassKind::Market.is_market());
2608        assert!(!OrderClassKind::Market.is_limit());
2609        assert!(!OrderClassKind::Market.is_liquidity());
2610        assert!(!OrderClassKind::Market.is_twap());
2611
2612        assert!(OrderClassKind::Limit.is_limit());
2613        assert!(OrderClassKind::Liquidity.is_liquidity());
2614        assert!(OrderClassKind::Twap.is_twap());
2615    }
2616
2617    #[test]
2618    fn order_class_kind_display() {
2619        assert_eq!(OrderClassKind::Twap.to_string(), "twap");
2620    }
2621
2622    #[test]
2623    fn order_class_kind_try_from_valid() {
2624        assert_eq!(OrderClassKind::try_from("market").unwrap(), OrderClassKind::Market);
2625        assert_eq!(OrderClassKind::try_from("limit").unwrap(), OrderClassKind::Limit);
2626        assert_eq!(OrderClassKind::try_from("liquidity").unwrap(), OrderClassKind::Liquidity);
2627        assert_eq!(OrderClassKind::try_from("twap").unwrap(), OrderClassKind::Twap);
2628    }
2629
2630    #[test]
2631    fn order_class_kind_try_from_invalid() {
2632        assert!(OrderClassKind::try_from("unknown").is_err());
2633    }
2634
2635    #[test]
2636    fn order_class_kind_serde_roundtrip() {
2637        let kind = OrderClassKind::Limit;
2638        let json = serde_json::to_string(&kind).unwrap();
2639        assert_eq!(json, "\"limit\"");
2640        let kind2: OrderClassKind = serde_json::from_str(&json).unwrap();
2641        assert_eq!(kind2, OrderClassKind::Limit);
2642    }
2643
2644    // ── OrderClass ──
2645
2646    #[test]
2647    fn order_class_new() {
2648        let oc = OrderClass::new(OrderClassKind::Twap);
2649        assert_eq!(oc.order_class, OrderClassKind::Twap);
2650    }
2651
2652    #[test]
2653    fn order_class_display() {
2654        let oc = OrderClass::new(OrderClassKind::Market);
2655        assert_eq!(oc.to_string(), "market");
2656    }
2657
2658    // ── OrderInteractionHooks ──
2659
2660    #[test]
2661    fn hooks_new_empty_vecs_become_none() {
2662        let hooks = OrderInteractionHooks::new(vec![], vec![]);
2663        assert!(!hooks.has_pre());
2664        assert!(!hooks.has_post());
2665        assert!(hooks.pre.is_none());
2666        assert!(hooks.post.is_none());
2667    }
2668
2669    #[test]
2670    fn hooks_new_with_entries() {
2671        let pre = CowHook::new("0xTarget", "0xData", "50000");
2672        let post = CowHook::new("0xTarget2", "0xData2", "60000");
2673        let hooks = OrderInteractionHooks::new(vec![pre], vec![post]);
2674        assert!(hooks.has_pre());
2675        assert!(hooks.has_post());
2676    }
2677
2678    #[test]
2679    fn hooks_with_version() {
2680        let hooks = OrderInteractionHooks::new(vec![], vec![]).with_version("0.2.0");
2681        assert_eq!(hooks.version.as_deref(), Some("0.2.0"));
2682    }
2683
2684    // ── CowHook ──
2685
2686    #[test]
2687    fn cow_hook_new() {
2688        let hook = CowHook::new("0xTarget", "0xCallData", "100000");
2689        assert_eq!(hook.target, "0xTarget");
2690        assert_eq!(hook.call_data, "0xCallData");
2691        assert_eq!(hook.gas_limit, "100000");
2692        assert!(!hook.has_dapp_id());
2693    }
2694
2695    #[test]
2696    fn cow_hook_with_dapp_id() {
2697        let hook = CowHook::new("0xTarget", "0xData", "50000").with_dapp_id("my-dapp");
2698        assert!(hook.has_dapp_id());
2699        assert_eq!(hook.dapp_id.as_deref(), Some("my-dapp"));
2700    }
2701
2702    #[test]
2703    fn cow_hook_display() {
2704        let hook = CowHook::new("0xTarget", "0xData", "50000");
2705        assert_eq!(hook.to_string(), "hook(target=0xTarget, gas=50000)");
2706    }
2707
2708    // ── Widget ──
2709
2710    #[test]
2711    fn widget_new() {
2712        let w = Widget::new("WidgetHost");
2713        assert_eq!(w.app_code, "WidgetHost");
2714        assert!(!w.has_environment());
2715    }
2716
2717    #[test]
2718    fn widget_with_environment() {
2719        let w = Widget::new("Host").with_environment("production");
2720        assert!(w.has_environment());
2721        assert_eq!(w.environment.as_deref(), Some("production"));
2722    }
2723
2724    #[test]
2725    fn widget_display() {
2726        assert_eq!(Widget::new("Host").to_string(), "widget(Host)");
2727    }
2728
2729    // ── PartnerFeeEntry ──
2730
2731    #[test]
2732    fn partner_fee_entry_volume() {
2733        let fee = PartnerFeeEntry::volume(50, "0xRecipient");
2734        assert_eq!(fee.volume_bps(), Some(50));
2735        assert_eq!(fee.surplus_bps(), None);
2736        assert_eq!(fee.price_improvement_bps(), None);
2737        assert_eq!(fee.max_volume_bps(), None);
2738        assert_eq!(fee.recipient, "0xRecipient");
2739    }
2740
2741    #[test]
2742    fn partner_fee_entry_surplus() {
2743        let fee = PartnerFeeEntry::surplus(30, 100, "0xRecipient");
2744        assert_eq!(fee.volume_bps(), None);
2745        assert_eq!(fee.surplus_bps(), Some(30));
2746        assert_eq!(fee.price_improvement_bps(), None);
2747        assert_eq!(fee.max_volume_bps(), Some(100));
2748    }
2749
2750    #[test]
2751    fn partner_fee_entry_price_improvement() {
2752        let fee = PartnerFeeEntry::price_improvement(20, 80, "0xRecipient");
2753        assert_eq!(fee.volume_bps(), None);
2754        assert_eq!(fee.surplus_bps(), None);
2755        assert_eq!(fee.price_improvement_bps(), Some(20));
2756        assert_eq!(fee.max_volume_bps(), Some(80));
2757    }
2758
2759    #[test]
2760    fn partner_fee_entry_display_volume() {
2761        let fee = PartnerFeeEntry::volume(50, "0xAddr");
2762        assert_eq!(fee.to_string(), "volume-fee(50bps, 0xAddr)");
2763    }
2764
2765    #[test]
2766    fn partner_fee_entry_display_surplus() {
2767        let fee = PartnerFeeEntry::surplus(30, 100, "0xAddr");
2768        assert_eq!(fee.to_string(), "surplus-fee(30bps, 0xAddr)");
2769    }
2770
2771    #[test]
2772    fn partner_fee_entry_display_price_improvement() {
2773        let fee = PartnerFeeEntry::price_improvement(20, 80, "0xAddr");
2774        assert_eq!(fee.to_string(), "price-improvement-fee(20bps, 0xAddr)");
2775    }
2776
2777    // ── PartnerFee ──
2778
2779    #[test]
2780    fn partner_fee_single() {
2781        let fee = PartnerFee::single(PartnerFeeEntry::volume(50, "0xAddr"));
2782        assert!(fee.is_single());
2783        assert!(!fee.is_multiple());
2784        assert_eq!(fee.count(), 1);
2785    }
2786
2787    #[test]
2788    fn partner_fee_multiple() {
2789        let fee = PartnerFee::Multiple(vec![
2790            PartnerFeeEntry::volume(50, "0x1"),
2791            PartnerFeeEntry::surplus(30, 100, "0x2"),
2792        ]);
2793        assert!(!fee.is_single());
2794        assert!(fee.is_multiple());
2795        assert_eq!(fee.count(), 2);
2796    }
2797
2798    #[test]
2799    fn partner_fee_entries_iterator() {
2800        let fee = PartnerFee::single(PartnerFeeEntry::volume(50, "0xAddr"));
2801        let entries: Vec<_> = fee.entries().collect();
2802        assert_eq!(entries.len(), 1);
2803        assert_eq!(entries[0].volume_bps(), Some(50));
2804
2805        let multi = PartnerFee::Multiple(vec![
2806            PartnerFeeEntry::volume(10, "0x1"),
2807            PartnerFeeEntry::surplus(20, 50, "0x2"),
2808        ]);
2809        let entries: Vec<_> = multi.entries().collect();
2810        assert_eq!(entries.len(), 2);
2811    }
2812
2813    #[test]
2814    fn partner_fee_display_single() {
2815        let fee = PartnerFee::single(PartnerFeeEntry::volume(50, "0xAddr"));
2816        assert_eq!(fee.to_string(), "volume-fee(50bps, 0xAddr)");
2817    }
2818
2819    #[test]
2820    fn partner_fee_display_multiple() {
2821        let fee = PartnerFee::Multiple(vec![
2822            PartnerFeeEntry::volume(10, "0x1"),
2823            PartnerFeeEntry::surplus(20, 50, "0x2"),
2824        ]);
2825        assert_eq!(fee.to_string(), "fees(2)");
2826    }
2827
2828    // ── get_partner_fee_bps ──
2829
2830    #[test]
2831    fn get_partner_fee_bps_some() {
2832        let fee = PartnerFee::single(PartnerFeeEntry::volume(50, "0xAddr"));
2833        assert_eq!(get_partner_fee_bps(Some(&fee)), Some(50));
2834    }
2835
2836    #[test]
2837    fn get_partner_fee_bps_none() {
2838        assert_eq!(get_partner_fee_bps(None), None);
2839    }
2840
2841    #[test]
2842    fn get_partner_fee_bps_no_volume() {
2843        let fee = PartnerFee::single(PartnerFeeEntry::surplus(30, 100, "0xAddr"));
2844        assert_eq!(get_partner_fee_bps(Some(&fee)), None);
2845    }
2846
2847    #[test]
2848    fn get_partner_fee_bps_multiple_finds_first_volume() {
2849        let fee = PartnerFee::Multiple(vec![
2850            PartnerFeeEntry::surplus(30, 100, "0x1"),
2851            PartnerFeeEntry::volume(50, "0x2"),
2852        ]);
2853        assert_eq!(get_partner_fee_bps(Some(&fee)), Some(50));
2854    }
2855
2856    // ── ReplacedOrder ──
2857
2858    #[test]
2859    fn replaced_order_new() {
2860        let uid = format!("0x{}", "ab".repeat(56));
2861        let ro = ReplacedOrder::new(&uid);
2862        assert_eq!(ro.uid, uid);
2863        assert_eq!(ro.uid.len(), 114);
2864    }
2865
2866    #[test]
2867    fn replaced_order_display() {
2868        let ro = ReplacedOrder::new("0xUID");
2869        assert_eq!(ro.to_string(), "replaced(0xUID)");
2870    }
2871
2872    // ── Bridging ──
2873
2874    #[test]
2875    fn bridging_new() {
2876        let b = Bridging::new("across", "42161", "0xToken");
2877        assert_eq!(b.provider, "across");
2878        assert_eq!(b.destination_chain_id, "42161");
2879        assert_eq!(b.destination_token_address, "0xToken");
2880        assert!(!b.has_quote_id());
2881        assert!(!b.has_quote_signature());
2882        assert!(!b.has_attestation_signature());
2883        assert!(!b.has_quote_body());
2884    }
2885
2886    #[test]
2887    fn bridging_with_all_optional_fields() {
2888        let b = Bridging::new("bungee", "10", "0xToken")
2889            .with_quote_id("q-123")
2890            .with_quote_signature("0xSig")
2891            .with_attestation_signature("0xAttest")
2892            .with_quote_body("body-data");
2893        assert!(b.has_quote_id());
2894        assert!(b.has_quote_signature());
2895        assert!(b.has_attestation_signature());
2896        assert!(b.has_quote_body());
2897        assert_eq!(b.quote_id.as_deref(), Some("q-123"));
2898        assert_eq!(b.quote_signature.as_deref(), Some("0xSig"));
2899        assert_eq!(b.attestation_signature.as_deref(), Some("0xAttest"));
2900        assert_eq!(b.quote_body.as_deref(), Some("body-data"));
2901    }
2902
2903    #[test]
2904    fn bridging_display() {
2905        let b = Bridging::new("across", "42161", "0xToken");
2906        assert_eq!(b.to_string(), "bridge(across, chain=42161)");
2907    }
2908
2909    // ── Flashloan ──
2910
2911    #[test]
2912    fn flashloan_new() {
2913        let fl = Flashloan::new("1000000", "0xPool", "0xToken");
2914        assert_eq!(fl.loan_amount, "1000000");
2915        assert_eq!(fl.liquidity_provider_address, "0xPool");
2916        assert_eq!(fl.token_address, "0xToken");
2917        assert!(fl.protocol_adapter_address.is_empty());
2918        assert!(fl.receiver_address.is_empty());
2919    }
2920
2921    #[test]
2922    fn flashloan_with_builders() {
2923        let fl = Flashloan::new("1000", "0xPool", "0xToken")
2924            .with_protocol_adapter("0xAdapter")
2925            .with_receiver("0xReceiver");
2926        assert_eq!(fl.protocol_adapter_address, "0xAdapter");
2927        assert_eq!(fl.receiver_address, "0xReceiver");
2928    }
2929
2930    #[test]
2931    fn flashloan_display() {
2932        let fl = Flashloan::new("1000", "0xPool", "0xToken");
2933        assert_eq!(fl.to_string(), "flashloan(0xToken, amount=1000)");
2934    }
2935
2936    // ── WrapperEntry ──
2937
2938    #[test]
2939    fn wrapper_entry_new() {
2940        let w = WrapperEntry::new("0xWrapper");
2941        assert_eq!(w.wrapper_address, "0xWrapper");
2942        assert!(!w.has_wrapper_data());
2943        assert!(!w.has_is_omittable());
2944        assert!(!w.is_omittable());
2945    }
2946
2947    #[test]
2948    fn wrapper_entry_with_wrapper_data() {
2949        let w = WrapperEntry::new("0xW").with_wrapper_data("0xABI");
2950        assert!(w.has_wrapper_data());
2951        assert_eq!(w.wrapper_data.as_deref(), Some("0xABI"));
2952    }
2953
2954    #[test]
2955    fn wrapper_entry_with_is_omittable_true() {
2956        let w = WrapperEntry::new("0xW").with_is_omittable(true);
2957        assert!(w.has_is_omittable());
2958        assert!(w.is_omittable());
2959    }
2960
2961    #[test]
2962    fn wrapper_entry_with_is_omittable_false() {
2963        let w = WrapperEntry::new("0xW").with_is_omittable(false);
2964        assert!(w.has_is_omittable());
2965        assert!(!w.is_omittable());
2966    }
2967
2968    #[test]
2969    fn wrapper_entry_display() {
2970        assert_eq!(WrapperEntry::new("0xW").to_string(), "wrapper(0xW)");
2971    }
2972
2973    // ── UserConsent ──
2974
2975    #[test]
2976    fn user_consent_new() {
2977        let c = UserConsent::new("https://cow.fi/tos", "2025-04-07");
2978        assert_eq!(c.terms, "https://cow.fi/tos");
2979        assert_eq!(c.accepted_date, "2025-04-07");
2980    }
2981
2982    #[test]
2983    fn user_consent_display() {
2984        let c = UserConsent::new("tos", "2025-01-01");
2985        assert_eq!(c.to_string(), "consent(tos, 2025-01-01)");
2986    }
2987
2988    // ── Serde roundtrips ──
2989
2990    #[test]
2991    fn app_data_doc_serde_roundtrip() {
2992        let doc = AppDataDoc::new("TestApp")
2993            .with_environment("production")
2994            .with_referrer(Referrer::code("COWRS"))
2995            .with_order_class(OrderClassKind::Limit);
2996        let json = serde_json::to_string(&doc).unwrap();
2997        let doc2: AppDataDoc = serde_json::from_str(&json).unwrap();
2998        assert_eq!(doc2.version, LATEST_APP_DATA_VERSION);
2999        assert_eq!(doc2.app_code.as_deref(), Some("TestApp"));
3000        assert_eq!(doc2.environment.as_deref(), Some("production"));
3001        assert!(doc2.metadata.has_referrer());
3002        assert!(doc2.metadata.has_order_class());
3003    }
3004
3005    #[test]
3006    fn app_data_doc_serde_camel_case() {
3007        let doc = AppDataDoc::new("App").with_environment("prod");
3008        let json = serde_json::to_string(&doc).unwrap();
3009        assert!(json.contains("\"appCode\""));
3010        assert!(!json.contains("\"app_code\""));
3011    }
3012
3013    #[test]
3014    fn app_data_doc_serde_skip_none_fields() {
3015        let doc = AppDataDoc::new("App");
3016        let json = serde_json::to_string(&doc).unwrap();
3017        assert!(!json.contains("\"environment\""));
3018        assert!(!json.contains("\"referrer\""));
3019    }
3020
3021    #[test]
3022    fn bridging_serde_roundtrip() {
3023        let b = Bridging::new("across", "42161", "0xToken").with_quote_id("q-1");
3024        let json = serde_json::to_string(&b).unwrap();
3025        let b2: Bridging = serde_json::from_str(&json).unwrap();
3026        assert_eq!(b2.provider, "across");
3027        assert_eq!(b2.quote_id.as_deref(), Some("q-1"));
3028    }
3029
3030    #[test]
3031    fn flashloan_serde_roundtrip() {
3032        let fl = Flashloan::new("999", "0xPool", "0xToken")
3033            .with_protocol_adapter("0xA")
3034            .with_receiver("0xR");
3035        let json = serde_json::to_string(&fl).unwrap();
3036        let fl2: Flashloan = serde_json::from_str(&json).unwrap();
3037        assert_eq!(fl2.loan_amount, "999");
3038        assert_eq!(fl2.protocol_adapter_address, "0xA");
3039        assert_eq!(fl2.receiver_address, "0xR");
3040    }
3041
3042    #[test]
3043    fn partner_fee_serde_single_roundtrip() {
3044        let fee = PartnerFee::single(PartnerFeeEntry::volume(50, "0xAddr"));
3045        let json = serde_json::to_string(&fee).unwrap();
3046        let fee2: PartnerFee = serde_json::from_str(&json).unwrap();
3047        assert!(fee2.is_single());
3048        assert_eq!(fee2.entries().next().unwrap().volume_bps(), Some(50));
3049    }
3050
3051    #[test]
3052    fn partner_fee_serde_multiple_roundtrip() {
3053        let fee = PartnerFee::Multiple(vec![
3054            PartnerFeeEntry::volume(10, "0x1"),
3055            PartnerFeeEntry::surplus(20, 50, "0x2"),
3056        ]);
3057        let json = serde_json::to_string(&fee).unwrap();
3058        let fee2: PartnerFee = serde_json::from_str(&json).unwrap();
3059        assert!(fee2.is_multiple());
3060        assert_eq!(fee2.count(), 2);
3061    }
3062
3063    #[test]
3064    fn cow_hook_serde_roundtrip() {
3065        let hook = CowHook::new("0xTarget", "0xData", "100000").with_dapp_id("my-dapp");
3066        let json = serde_json::to_string(&hook).unwrap();
3067        let hook2: CowHook = serde_json::from_str(&json).unwrap();
3068        assert_eq!(hook2.target, "0xTarget");
3069        assert_eq!(hook2.call_data, "0xData");
3070        assert_eq!(hook2.gas_limit, "100000");
3071        assert_eq!(hook2.dapp_id.as_deref(), Some("my-dapp"));
3072    }
3073
3074    #[test]
3075    fn wrapper_entry_serde_roundtrip() {
3076        let w = WrapperEntry::new("0xW").with_wrapper_data("0xABI").with_is_omittable(true);
3077        let json = serde_json::to_string(&w).unwrap();
3078        let w2: WrapperEntry = serde_json::from_str(&json).unwrap();
3079        assert_eq!(w2.wrapper_address, "0xW");
3080        assert_eq!(w2.wrapper_data.as_deref(), Some("0xABI"));
3081        assert!(w2.is_omittable());
3082    }
3083
3084    #[test]
3085    fn user_consent_serde_roundtrip() {
3086        let c = UserConsent::new("tos-url", "2025-04-07");
3087        let json = serde_json::to_string(&c).unwrap();
3088        let c2: UserConsent = serde_json::from_str(&json).unwrap();
3089        assert_eq!(c2.terms, "tos-url");
3090        assert_eq!(c2.accepted_date, "2025-04-07");
3091    }
3092
3093    #[test]
3094    fn order_interaction_hooks_serde_roundtrip() {
3095        let pre = CowHook::new("0xA", "0xB", "1000");
3096        let hooks = OrderInteractionHooks::new(vec![pre], vec![]).with_version("0.2.0");
3097        let json = serde_json::to_string(&hooks).unwrap();
3098        let hooks2: OrderInteractionHooks = serde_json::from_str(&json).unwrap();
3099        assert!(hooks2.has_pre());
3100        assert!(!hooks2.has_post());
3101        assert_eq!(hooks2.version.as_deref(), Some("0.2.0"));
3102    }
3103
3104    // ── Full builder chain ──
3105
3106    #[test]
3107    fn app_data_doc_full_builder_chain() {
3108        let doc = AppDataDoc::new("FullApp")
3109            .with_environment("production")
3110            .with_referrer(Referrer::code("PARTNER"))
3111            .with_utm(Utm::new().with_source("test"))
3112            .with_hooks(OrderInteractionHooks::new(
3113                vec![CowHook::new("0xT", "0xD", "1000")],
3114                vec![],
3115            ))
3116            .with_partner_fee(PartnerFee::single(PartnerFeeEntry::volume(50, "0xFee")))
3117            .with_replaced_order("0xUID")
3118            .with_signer("0xSigner")
3119            .with_order_class(OrderClassKind::Market)
3120            .with_bridging(Bridging::new("across", "42161", "0xToken"))
3121            .with_flashloan(Flashloan::new("1000", "0xPool", "0xToken"))
3122            .with_wrappers(vec![WrapperEntry::new("0xW")])
3123            .with_user_consents(vec![UserConsent::new("tos", "2025-01-01")]);
3124
3125        assert_eq!(doc.app_code.as_deref(), Some("FullApp"));
3126        assert_eq!(doc.environment.as_deref(), Some("production"));
3127
3128        let m = &doc.metadata;
3129        assert!(m.has_referrer());
3130        assert!(m.has_utm());
3131        assert!(m.has_hooks());
3132        assert!(m.has_partner_fee());
3133        assert!(m.has_replaced_order());
3134        assert!(m.has_signer());
3135        assert!(m.has_order_class());
3136        assert!(m.has_bridging());
3137        assert!(m.has_flashloan());
3138        assert!(m.has_wrappers());
3139        assert!(m.has_user_consents());
3140
3141        // Roundtrip the whole thing through serde.
3142        let json = serde_json::to_string(&doc).unwrap();
3143        let doc2: AppDataDoc = serde_json::from_str(&json).unwrap();
3144        assert_eq!(doc2.version, LATEST_APP_DATA_VERSION);
3145        assert!(doc2.metadata.has_referrer());
3146        assert!(doc2.metadata.has_flashloan());
3147        assert!(doc2.metadata.has_user_consents());
3148    }
3149}