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}