Skip to main content

cow_chains/
chain.rs

1//! Supported chain IDs, API base URLs, and explorer link helpers.
2//!
3//! This module defines the [`SupportedChainId`] enum (one variant per chain
4//! that the `CoW` Protocol orderbook supports), the [`Env`] enum (production
5//! vs. staging), and helpers to build API and explorer URLs.
6//!
7//! # Key items
8//!
9//! | Item | Purpose |
10//! |---|---|
11//! | [`SupportedChainId`] | Enum of all chains with EIP-155 discriminants |
12//! | [`Env`] | `Prod` / `Staging` orderbook environment |
13//! | [`api_base_url`] | Build the orderbook API URL for a chain + env |
14//! | [`order_explorer_link`] | Build a `CoW` Protocol Explorer URL for an order |
15
16use serde::{Deserialize, Serialize};
17
18/// Chains supported by the `CoW` Protocol orderbook.
19///
20/// Each variant's numeric discriminant matches the EIP-155 chain ID, so
21/// `SupportedChainId::Mainnet as u64 == 1`. Use [`try_from_u64`](Self::try_from_u64)
22/// to convert from a raw chain ID, or [`all`](Self::all) to iterate every
23/// supported chain.
24///
25/// # Example
26///
27/// ```
28/// use cow_chains::SupportedChainId;
29///
30/// let chain = SupportedChainId::try_from_u64(1).unwrap();
31/// assert_eq!(chain, SupportedChainId::Mainnet);
32/// assert_eq!(chain.as_u64(), 1);
33/// assert_eq!(chain.as_str(), "mainnet");
34/// assert!(!chain.is_testnet());
35/// ```
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37#[repr(u64)]
38pub enum SupportedChainId {
39    /// Ethereum mainnet (chain ID 1).
40    Mainnet = 1,
41    /// Gnosis Chain (chain ID 100).
42    GnosisChain = 100,
43    /// Arbitrum One (chain ID 42161).
44    ArbitrumOne = 42_161,
45    /// Base (chain ID 8453).
46    Base = 8_453,
47    /// Ethereum Sepolia testnet (chain ID 11155111).
48    Sepolia = 11_155_111,
49    /// Polygon `PoS` (chain ID 137).
50    Polygon = 137,
51    /// Avalanche C-Chain (chain ID 43114).
52    Avalanche = 43_114,
53    /// BNB Smart Chain (chain ID 56).
54    BnbChain = 56,
55    /// Linea (chain ID 59144).
56    Linea = 59_144,
57    /// Plasma (chain ID 9745).
58    Plasma = 9_745,
59    /// Ink (chain ID 57073).
60    Ink = 57_073,
61}
62
63impl SupportedChainId {
64    /// Return the numeric EIP-155 chain ID.
65    ///
66    /// # Returns
67    ///
68    /// The `u64` chain ID (e.g. `1` for Mainnet, `100` for Gnosis Chain).
69    #[must_use]
70    pub const fn as_u64(self) -> u64 {
71        self as u64
72    }
73
74    /// Try to construct a [`SupportedChainId`] from a raw EIP-155 chain ID.
75    ///
76    /// # Parameters
77    ///
78    /// * `chain_id` — the numeric EIP-155 chain ID.
79    ///
80    /// # Returns
81    ///
82    /// `Some(variant)` if `chain_id` is supported, `None` otherwise.
83    ///
84    /// # Example
85    ///
86    /// ```
87    /// use cow_chains::SupportedChainId;
88    ///
89    /// assert_eq!(SupportedChainId::try_from_u64(1), Some(SupportedChainId::Mainnet));
90    /// assert_eq!(SupportedChainId::try_from_u64(11155111), Some(SupportedChainId::Sepolia));
91    /// assert_eq!(SupportedChainId::try_from_u64(9999), None);
92    /// ```
93    #[must_use]
94    pub const fn try_from_u64(chain_id: u64) -> Option<Self> {
95        match chain_id {
96            1 => Some(Self::Mainnet),
97            100 => Some(Self::GnosisChain),
98            42_161 => Some(Self::ArbitrumOne),
99            8_453 => Some(Self::Base),
100            11_155_111 => Some(Self::Sepolia),
101            137 => Some(Self::Polygon),
102            43_114 => Some(Self::Avalanche),
103            56 => Some(Self::BnbChain),
104            59_144 => Some(Self::Linea),
105            9_745 => Some(Self::Plasma),
106            57_073 => Some(Self::Ink),
107            _ => None,
108        }
109    }
110
111    /// Return a slice of all chains supported by the `CoW` Protocol orderbook.
112    ///
113    /// # Returns
114    ///
115    /// A static slice containing every [`SupportedChainId`] variant.
116    #[must_use]
117    pub const fn all() -> &'static [Self] {
118        &[
119            Self::Mainnet,
120            Self::GnosisChain,
121            Self::ArbitrumOne,
122            Self::Base,
123            Self::Sepolia,
124            Self::Polygon,
125            Self::Avalanche,
126            Self::BnbChain,
127            Self::Linea,
128            Self::Plasma,
129            Self::Ink,
130        ]
131    }
132
133    /// Returns the `CoW` Protocol API path segment for this chain.
134    ///
135    /// Matches the path used in [`api_base_url`], e.g. `"mainnet"`, `"xdai"`,
136    /// `"sepolia"`. Useful for constructing API URLs manually.
137    ///
138    /// # Returns
139    ///
140    /// A static string suitable for use in API URL paths.
141    #[must_use]
142    pub const fn as_str(self) -> &'static str {
143        match self {
144            Self::Mainnet => "mainnet",
145            Self::GnosisChain => "xdai",
146            Self::ArbitrumOne => "arbitrum_one",
147            Self::Base => "base",
148            Self::Sepolia => "sepolia",
149            Self::Polygon => "polygon",
150            Self::Avalanche => "avalanche",
151            Self::BnbChain => "bnb",
152            Self::Linea => "linea",
153            Self::Plasma => "plasma",
154            Self::Ink => "ink",
155        }
156    }
157
158    /// Whether this chain is a testnet.
159    ///
160    /// # Returns
161    ///
162    /// `true` for [`Sepolia`](Self::Sepolia), `false` for all other chains.
163    #[must_use]
164    pub const fn is_testnet(self) -> bool {
165        matches!(self, Self::Sepolia)
166    }
167
168    /// Returns `true` for production chains (i.e. not a testnet).
169    ///
170    /// This is the logical complement of [`Self::is_testnet`].
171    ///
172    /// ```
173    /// use cow_chains::SupportedChainId;
174    /// assert!(SupportedChainId::Mainnet.is_mainnet());
175    /// assert!(!SupportedChainId::Sepolia.is_mainnet());
176    /// ```
177    #[must_use]
178    pub const fn is_mainnet(self) -> bool {
179        !self.is_testnet()
180    }
181
182    /// Returns `true` for layer-2 networks.
183    ///
184    /// Currently includes Arbitrum One, Base, Linea, Ink, and Polygon.
185    ///
186    /// ```
187    /// use cow_chains::SupportedChainId;
188    ///
189    /// assert!(SupportedChainId::ArbitrumOne.is_layer2());
190    /// assert!(SupportedChainId::Base.is_layer2());
191    /// assert!(SupportedChainId::Polygon.is_layer2());
192    /// assert!(!SupportedChainId::Mainnet.is_layer2());
193    /// assert!(!SupportedChainId::GnosisChain.is_layer2());
194    /// ```
195    #[must_use]
196    pub const fn is_layer2(self) -> bool {
197        matches!(self, Self::ArbitrumOne | Self::Base | Self::Linea | Self::Ink | Self::Polygon)
198    }
199
200    /// Return the network segment used in `CoW` Protocol Explorer URLs.
201    ///
202    /// Mainnet uses an empty segment (orders live at the root path).
203    ///
204    /// # Returns
205    ///
206    /// A static string for the URL path segment (empty for Mainnet).
207    #[must_use]
208    pub const fn explorer_network(self) -> &'static str {
209        match self {
210            Self::Mainnet => "",
211            Self::GnosisChain => "gc",
212            Self::ArbitrumOne => "arb1",
213            Self::Base => "base",
214            Self::Sepolia => "sepolia",
215            Self::Polygon => "polygon",
216            Self::Avalanche => "avalanche",
217            Self::BnbChain => "bnb",
218            Self::Linea => "linea",
219            Self::Plasma => "plasma",
220            Self::Ink => "ink",
221        }
222    }
223}
224
225/// Build a `CoW` Protocol Explorer link for an order.
226///
227/// Returns a URL pointing to `https://explorer.cow.fi/{network}/orders/{uid}`.
228/// Mainnet orders omit the network prefix (orders live at the root path).
229///
230/// # Parameters
231///
232/// * `chain` — the [`SupportedChainId`] the order was placed on.
233/// * `order_uid` — the order UID string (typically `0x`-prefixed hex).
234///
235/// # Returns
236///
237/// A `String` URL pointing to the order on the `CoW` Protocol Explorer.
238///
239/// # Example
240///
241/// ```
242/// use cow_chains::{SupportedChainId, order_explorer_link};
243///
244/// let url = order_explorer_link(SupportedChainId::Mainnet, "0xabc123...");
245/// assert!(url.starts_with("https://explorer.cow.fi/orders/"));
246///
247/// let url_sep = order_explorer_link(SupportedChainId::Sepolia, "0xabc123...");
248/// assert!(url_sep.starts_with("https://explorer.cow.fi/sepolia/orders/"));
249/// ```
250#[must_use]
251pub fn order_explorer_link(chain: SupportedChainId, order_uid: &str) -> String {
252    let net = chain.explorer_network();
253    if net.is_empty() {
254        format!("https://explorer.cow.fi/orders/{order_uid}")
255    } else {
256        format!("https://explorer.cow.fi/{net}/orders/{order_uid}")
257    }
258}
259
260impl std::fmt::Display for SupportedChainId {
261    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262        let name = match self {
263            Self::Mainnet => "Ethereum",
264            Self::GnosisChain => "Gnosis Chain",
265            Self::ArbitrumOne => "Arbitrum One",
266            Self::Base => "Base",
267            Self::Sepolia => "Sepolia",
268            Self::Polygon => "Polygon",
269            Self::Avalanche => "Avalanche",
270            Self::BnbChain => "BNB Smart Chain",
271            Self::Linea => "Linea",
272            Self::Plasma => "Plasma",
273            Self::Ink => "Ink",
274        };
275        f.write_str(name)
276    }
277}
278
279impl From<SupportedChainId> for u64 {
280    fn from(id: SupportedChainId) -> Self {
281        id.as_u64()
282    }
283}
284
285impl TryFrom<u64> for SupportedChainId {
286    type Error = u64;
287
288    fn try_from(chain_id: u64) -> Result<Self, Self::Error> {
289        Self::try_from_u64(chain_id).ok_or(chain_id)
290    }
291}
292
293impl TryFrom<&str> for SupportedChainId {
294    type Error = cow_errors::CowError;
295
296    /// Parse a [`SupportedChainId`] from the `CoW` Protocol API path segment.
297    ///
298    /// Accepts the same strings returned by [`SupportedChainId::as_str`]:
299    /// `"mainnet"`, `"xdai"`, `"arbitrum_one"`, `"base"`, `"sepolia"`,
300    /// `"polygon"`, `"avalanche"`, `"bnb"`, `"linea"`, `"plasma"`, `"ink"`.
301    fn try_from(s: &str) -> Result<Self, Self::Error> {
302        match s {
303            "mainnet" => Ok(Self::Mainnet),
304            "xdai" => Ok(Self::GnosisChain),
305            "arbitrum_one" => Ok(Self::ArbitrumOne),
306            "base" => Ok(Self::Base),
307            "sepolia" => Ok(Self::Sepolia),
308            "polygon" => Ok(Self::Polygon),
309            "avalanche" => Ok(Self::Avalanche),
310            "bnb" => Ok(Self::BnbChain),
311            "linea" => Ok(Self::Linea),
312            "plasma" => Ok(Self::Plasma),
313            "ink" => Ok(Self::Ink),
314            other => Err(cow_errors::CowError::Parse {
315                field: "SupportedChainId",
316                reason: format!("unknown chain: {other}"),
317            }),
318        }
319    }
320}
321
322/// Orderbook API environment.
323///
324/// The `CoW` Protocol runs two parallel orderbooks:
325///
326/// - **Prod** (`api.cow.fi`) — the production orderbook used for real trades.
327/// - **Staging** (`barn.api.cow.fi`) — the "barn" environment used for testing with real tokens but
328///   lower liquidity.
329///
330/// # Example
331///
332/// ```
333/// use cow_chains::Env;
334///
335/// let env = Env::Prod;
336/// assert!(env.is_prod());
337/// assert_eq!(env.as_str(), "prod");
338/// ```
339#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
340pub enum Env {
341    /// Production orderbook at `api.cow.fi`.
342    #[default]
343    Prod,
344    /// Staging (barn) orderbook at `barn.api.cow.fi`.
345    Staging,
346}
347
348impl Env {
349    /// Returns the string label for this environment.
350    ///
351    /// # Returns
352    ///
353    /// `"prod"` or `"staging"`.
354    #[must_use]
355    pub const fn as_str(self) -> &'static str {
356        match self {
357            Self::Prod => "prod",
358            Self::Staging => "staging",
359        }
360    }
361
362    /// Returns all supported environments.
363    ///
364    /// ```
365    /// use cow_chains::Env;
366    /// assert_eq!(Env::all().len(), 2);
367    /// ```
368    #[must_use]
369    pub const fn all() -> &'static [Self] {
370        &[Self::Prod, Self::Staging]
371    }
372
373    /// Returns `true` if this is the production environment.
374    ///
375    /// # Returns
376    ///
377    /// `true` for [`Env::Prod`].
378    #[must_use]
379    pub const fn is_prod(self) -> bool {
380        matches!(self, Self::Prod)
381    }
382
383    /// Returns `true` if this is the staging (barn) environment.
384    ///
385    /// # Returns
386    ///
387    /// `true` for [`Env::Staging`].
388    #[must_use]
389    pub const fn is_staging(self) -> bool {
390        matches!(self, Self::Staging)
391    }
392}
393
394impl std::fmt::Display for Env {
395    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
396        f.write_str(self.as_str())
397    }
398}
399
400impl TryFrom<&str> for Env {
401    type Error = cow_errors::CowError;
402
403    /// Parse an [`Env`] from its string label.
404    ///
405    /// Accepts `"prod"` or `"staging"`.
406    fn try_from(s: &str) -> Result<Self, Self::Error> {
407        match s {
408            "prod" => Ok(Self::Prod),
409            "staging" => Ok(Self::Staging),
410            other => Err(cow_errors::CowError::Parse {
411                field: "Env",
412                reason: format!("unknown env: {other}"),
413            }),
414        }
415    }
416}
417
418/// Return the orderbook API base URL for a chain and environment.
419///
420/// This is an alias for [`api_base_url`] matching the `apiUrl` name from
421/// the `TypeScript` `contracts-ts` package.
422///
423/// # Parameters
424///
425/// * `chain` — the target [`SupportedChainId`].
426/// * `env` — the [`Env`] (production or staging).
427///
428/// # Returns
429///
430/// A static string like `"https://api.cow.fi/mainnet"`.
431#[must_use]
432pub const fn api_url(chain: SupportedChainId, env: Env) -> &'static str {
433    api_base_url(chain, env)
434}
435
436/// Return the orderbook API base URL (no trailing slash) for `chain` in
437/// `env`.
438///
439/// Append `/api/v1/<endpoint>` to form a full request URL. For example,
440/// `api_base_url(Mainnet, Prod)` returns `"https://api.cow.fi/mainnet"`,
441/// so a quote request would go to
442/// `"https://api.cow.fi/mainnet/api/v1/quote"`.
443///
444/// # Parameters
445///
446/// * `chain` — the target [`SupportedChainId`].
447/// * `env` — the [`Env`] (production or staging).
448///
449/// # Returns
450///
451/// A `&'static str` base URL.
452///
453/// # Example
454///
455/// ```
456/// use cow_chains::{Env, SupportedChainId, api_base_url};
457///
458/// let url = api_base_url(SupportedChainId::Mainnet, Env::Prod);
459/// assert_eq!(url, "https://api.cow.fi/mainnet");
460///
461/// let barn = api_base_url(SupportedChainId::Sepolia, Env::Staging);
462/// assert!(barn.contains("barn.api.cow.fi"));
463/// ```
464#[must_use]
465pub const fn api_base_url(chain: SupportedChainId, env: Env) -> &'static str {
466    match (chain, env) {
467        (SupportedChainId::Mainnet, Env::Prod) => "https://api.cow.fi/mainnet",
468        (SupportedChainId::GnosisChain, Env::Prod) => "https://api.cow.fi/xdai",
469        (SupportedChainId::ArbitrumOne, Env::Prod) => "https://api.cow.fi/arbitrum_one",
470        (SupportedChainId::Base, Env::Prod) => "https://api.cow.fi/base",
471        (SupportedChainId::Sepolia, Env::Prod) => "https://api.cow.fi/sepolia",
472        (SupportedChainId::Polygon, Env::Prod) => "https://api.cow.fi/polygon",
473        (SupportedChainId::Avalanche, Env::Prod) => "https://api.cow.fi/avalanche",
474        (SupportedChainId::BnbChain, Env::Prod) => "https://api.cow.fi/bnb",
475        (SupportedChainId::Linea, Env::Prod) => "https://api.cow.fi/linea",
476        (SupportedChainId::Plasma, Env::Prod) => "https://api.cow.fi/plasma",
477        (SupportedChainId::Ink, Env::Prod) => "https://api.cow.fi/ink",
478        (SupportedChainId::Mainnet, Env::Staging) => "https://barn.api.cow.fi/mainnet",
479        (SupportedChainId::GnosisChain, Env::Staging) => "https://barn.api.cow.fi/xdai",
480        (SupportedChainId::ArbitrumOne, Env::Staging) => "https://barn.api.cow.fi/arbitrum_one",
481        (SupportedChainId::Base, Env::Staging) => "https://barn.api.cow.fi/base",
482        (SupportedChainId::Sepolia, Env::Staging) => "https://barn.api.cow.fi/sepolia",
483        (SupportedChainId::Polygon, Env::Staging) => "https://barn.api.cow.fi/polygon",
484        (SupportedChainId::Avalanche, Env::Staging) => "https://barn.api.cow.fi/avalanche",
485        (SupportedChainId::BnbChain, Env::Staging) => "https://barn.api.cow.fi/bnb",
486        (SupportedChainId::Linea, Env::Staging) => "https://barn.api.cow.fi/linea",
487        (SupportedChainId::Plasma, Env::Staging) => "https://barn.api.cow.fi/plasma",
488        (SupportedChainId::Ink, Env::Staging) => "https://barn.api.cow.fi/ink",
489    }
490}
491
492/// Base URL of the `CoW` Protocol production partner API gateway.
493///
494/// Requests to this host require an `X-API-Key` header. See [`partner_api_base_url`].
495pub const PARTNER_PROD_BASE_URL: &str = "https://partners.cow.fi";
496
497/// Base URL of the `CoW` Protocol staging (barn) partner API gateway.
498///
499/// Requests to this host require an `X-API-Key` header. See [`partner_api_base_url`].
500pub const PARTNER_STAGING_BASE_URL: &str = "https://partners.barn.cow.fi";
501
502/// Return the partner API base URL (no trailing slash) for `chain` in `env`.
503///
504/// Partners route traffic through `partners.cow.fi` / `partners.barn.cow.fi`
505/// and must attach an `X-API-Key` header. The chain path segment matches
506/// the one used by [`api_base_url`], so
507/// `partner_api_base_url(Mainnet, Prod)` returns
508/// `"https://partners.cow.fi/mainnet"`.
509///
510/// # Parameters
511///
512/// * `chain` — the target [`SupportedChainId`].
513/// * `env` — the [`Env`] (production or staging).
514///
515/// # Returns
516///
517/// A `&'static str` base URL.
518///
519/// # Example
520///
521/// ```
522/// use cow_chains::{Env, SupportedChainId, partner_api_base_url};
523///
524/// assert_eq!(
525///     partner_api_base_url(SupportedChainId::Mainnet, Env::Prod),
526///     "https://partners.cow.fi/mainnet"
527/// );
528/// assert!(
529///     partner_api_base_url(SupportedChainId::Sepolia, Env::Staging).contains("partners.barn")
530/// );
531/// ```
532#[must_use]
533pub const fn partner_api_base_url(chain: SupportedChainId, env: Env) -> &'static str {
534    match (chain, env) {
535        (SupportedChainId::Mainnet, Env::Prod) => "https://partners.cow.fi/mainnet",
536        (SupportedChainId::GnosisChain, Env::Prod) => "https://partners.cow.fi/xdai",
537        (SupportedChainId::ArbitrumOne, Env::Prod) => "https://partners.cow.fi/arbitrum_one",
538        (SupportedChainId::Base, Env::Prod) => "https://partners.cow.fi/base",
539        (SupportedChainId::Sepolia, Env::Prod) => "https://partners.cow.fi/sepolia",
540        (SupportedChainId::Polygon, Env::Prod) => "https://partners.cow.fi/polygon",
541        (SupportedChainId::Avalanche, Env::Prod) => "https://partners.cow.fi/avalanche",
542        (SupportedChainId::BnbChain, Env::Prod) => "https://partners.cow.fi/bnb",
543        (SupportedChainId::Linea, Env::Prod) => "https://partners.cow.fi/linea",
544        (SupportedChainId::Plasma, Env::Prod) => "https://partners.cow.fi/plasma",
545        (SupportedChainId::Ink, Env::Prod) => "https://partners.cow.fi/ink",
546        (SupportedChainId::Mainnet, Env::Staging) => "https://partners.barn.cow.fi/mainnet",
547        (SupportedChainId::GnosisChain, Env::Staging) => "https://partners.barn.cow.fi/xdai",
548        (SupportedChainId::ArbitrumOne, Env::Staging) => {
549            "https://partners.barn.cow.fi/arbitrum_one"
550        }
551        (SupportedChainId::Base, Env::Staging) => "https://partners.barn.cow.fi/base",
552        (SupportedChainId::Sepolia, Env::Staging) => "https://partners.barn.cow.fi/sepolia",
553        (SupportedChainId::Polygon, Env::Staging) => "https://partners.barn.cow.fi/polygon",
554        (SupportedChainId::Avalanche, Env::Staging) => "https://partners.barn.cow.fi/avalanche",
555        (SupportedChainId::BnbChain, Env::Staging) => "https://partners.barn.cow.fi/bnb",
556        (SupportedChainId::Linea, Env::Staging) => "https://partners.barn.cow.fi/linea",
557        (SupportedChainId::Plasma, Env::Staging) => "https://partners.barn.cow.fi/plasma",
558        (SupportedChainId::Ink, Env::Staging) => "https://partners.barn.cow.fi/ink",
559    }
560}
561
562#[cfg(test)]
563mod tests {
564    use super::*;
565
566    // ── SupportedChainId ────────────────────────────────────────────────
567
568    #[test]
569    fn all_chains_roundtrip_u64() {
570        for &chain in SupportedChainId::all() {
571            let id = chain.as_u64();
572            assert_eq!(SupportedChainId::try_from_u64(id), Some(chain));
573            assert_eq!(u64::from(chain), id);
574            assert!(matches!(SupportedChainId::try_from(id), Ok(c) if c == chain));
575        }
576    }
577
578    #[test]
579    fn all_chains_roundtrip_str() {
580        for &chain in SupportedChainId::all() {
581            let s = chain.as_str();
582            assert!(!s.is_empty());
583            assert!(matches!(SupportedChainId::try_from(s), Ok(c) if c == chain));
584        }
585    }
586
587    #[test]
588    fn all_chains_have_display() {
589        for &chain in SupportedChainId::all() {
590            let display = format!("{chain}");
591            assert!(!display.is_empty());
592        }
593    }
594
595    #[test]
596    fn all_chains_have_explorer_network() {
597        for &chain in SupportedChainId::all() {
598            // Mainnet is empty, all others non-empty
599            let net = chain.explorer_network();
600            if chain == SupportedChainId::Mainnet {
601                assert!(net.is_empty());
602            } else {
603                assert!(!net.is_empty());
604            }
605        }
606    }
607
608    #[test]
609    fn unknown_chain_id_returns_none() {
610        assert_eq!(SupportedChainId::try_from_u64(9999), None);
611        assert!(SupportedChainId::try_from(0u64).is_err());
612    }
613
614    #[test]
615    fn unknown_chain_str_returns_err() {
616        assert!(SupportedChainId::try_from("unknown").is_err());
617    }
618
619    #[test]
620    fn only_sepolia_is_testnet() {
621        for &chain in SupportedChainId::all() {
622            if chain == SupportedChainId::Sepolia {
623                assert!(chain.is_testnet());
624                assert!(!chain.is_mainnet());
625            } else {
626                assert!(!chain.is_testnet());
627                assert!(chain.is_mainnet());
628            }
629        }
630    }
631
632    #[test]
633    fn layer2_chains() {
634        let l2s = [
635            SupportedChainId::ArbitrumOne,
636            SupportedChainId::Base,
637            SupportedChainId::Linea,
638            SupportedChainId::Ink,
639            SupportedChainId::Polygon,
640        ];
641        for &chain in &l2s {
642            assert!(chain.is_layer2(), "{chain:?} should be L2");
643        }
644        assert!(!SupportedChainId::Mainnet.is_layer2());
645        assert!(!SupportedChainId::GnosisChain.is_layer2());
646    }
647
648    #[test]
649    fn all_contains_every_variant() {
650        assert_eq!(SupportedChainId::all().len(), 11);
651    }
652
653    // ── Env ─────────────────────────────────────────────────────────────
654
655    #[test]
656    fn env_default_is_prod() {
657        assert_eq!(Env::default(), Env::Prod);
658    }
659
660    #[test]
661    fn env_predicates() {
662        assert!(Env::Prod.is_prod());
663        assert!(!Env::Prod.is_staging());
664        assert!(Env::Staging.is_staging());
665        assert!(!Env::Staging.is_prod());
666    }
667
668    #[test]
669    fn env_roundtrip_str() {
670        for &env in Env::all() {
671            assert!(matches!(Env::try_from(env.as_str()), Ok(e) if e == env));
672        }
673    }
674
675    #[test]
676    fn env_all_has_two() {
677        assert_eq!(Env::all().len(), 2);
678    }
679
680    #[test]
681    fn env_display() {
682        assert_eq!(format!("{}", Env::Prod), "prod");
683        assert_eq!(format!("{}", Env::Staging), "staging");
684    }
685
686    #[test]
687    fn env_invalid_str() {
688        assert!(Env::try_from("production").is_err());
689    }
690
691    // ── API URLs ────────────────────────────────────────────────────────
692
693    #[test]
694    fn api_base_url_all_chains_all_envs() {
695        for &chain in SupportedChainId::all() {
696            for &env in Env::all() {
697                let url = api_base_url(chain, env);
698                assert!(url.starts_with("https://"));
699                assert!(url.contains("cow.fi"));
700                if env.is_staging() {
701                    assert!(url.contains("barn."), "{url} should contain barn for staging");
702                }
703            }
704        }
705    }
706
707    #[test]
708    fn api_url_is_alias_for_api_base_url() {
709        let chain = SupportedChainId::Mainnet;
710        assert_eq!(api_url(chain, Env::Prod), api_base_url(chain, Env::Prod));
711    }
712
713    #[test]
714    fn partner_api_base_url_all_chains_all_envs() {
715        for &chain in SupportedChainId::all() {
716            for &env in Env::all() {
717                let url = partner_api_base_url(chain, env);
718                assert!(url.starts_with("https://"));
719                assert!(url.contains("partners."));
720                assert!(url.contains("cow.fi"));
721                if env.is_staging() {
722                    assert!(url.contains("partners.barn"), "{url} should include partners.barn");
723                } else {
724                    assert!(!url.contains("partners.barn"));
725                }
726            }
727        }
728    }
729
730    #[test]
731    fn partner_url_path_segment_matches_api_base_url() {
732        // The chain path segment must be identical for both hosts.
733        for &chain in SupportedChainId::all() {
734            for &env in Env::all() {
735                let default = api_base_url(chain, env);
736                let partner = partner_api_base_url(chain, env);
737                let default_seg = default.rsplit('/').next().unwrap();
738                let partner_seg = partner.rsplit('/').next().unwrap();
739                assert_eq!(default_seg, partner_seg, "chain path segment mismatch for {chain:?}");
740            }
741        }
742    }
743
744    // ── Explorer links ──────────────────────────────────────────────────
745
746    #[test]
747    fn explorer_link_mainnet_no_network_prefix() {
748        let url = order_explorer_link(SupportedChainId::Mainnet, "0xabc");
749        assert_eq!(url, "https://explorer.cow.fi/orders/0xabc");
750    }
751
752    #[test]
753    fn explorer_link_non_mainnet_has_network() {
754        let url = order_explorer_link(SupportedChainId::Sepolia, "0xabc");
755        assert_eq!(url, "https://explorer.cow.fi/sepolia/orders/0xabc");
756    }
757}