Skip to main content

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