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