1use serde::{Deserialize, Serialize};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37#[repr(u64)]
38pub enum SupportedChainId {
39 Mainnet = 1,
41 GnosisChain = 100,
43 ArbitrumOne = 42_161,
45 Base = 8_453,
47 Sepolia = 11_155_111,
49 Polygon = 137,
51 Avalanche = 43_114,
53 BnbChain = 56,
55 Linea = 59_144,
57 Plasma = 9_745,
59 Ink = 57_073,
61}
62
63impl SupportedChainId {
64 #[must_use]
70 pub const fn as_u64(self) -> u64 {
71 self as u64
72 }
73
74 #[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 #[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 #[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 #[must_use]
164 pub const fn is_testnet(self) -> bool {
165 matches!(self, Self::Sepolia)
166 }
167
168 #[must_use]
178 pub const fn is_mainnet(self) -> bool {
179 !self.is_testnet()
180 }
181
182 #[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 #[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#[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
340pub enum Env {
341 #[default]
343 Prod,
344 Staging,
346}
347
348impl Env {
349 #[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 #[must_use]
369 pub const fn all() -> &'static [Self] {
370 &[Self::Prod, Self::Staging]
371 }
372
373 #[must_use]
379 pub const fn is_prod(self) -> bool {
380 matches!(self, Self::Prod)
381 }
382
383 #[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 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#[must_use]
432pub const fn api_url(chain: SupportedChainId, env: Env) -> &'static str {
433 api_base_url(chain, env)
434}
435
436#[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
492pub const PARTNER_PROD_BASE_URL: &str = "https://partners.cow.fi";
496
497pub const PARTNER_STAGING_BASE_URL: &str = "https://partners.barn.cow.fi";
501
502#[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 #[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 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 #[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 #[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 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 #[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}