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 Lens = 232,
59 Plasma = 9_745,
61 Ink = 57_073,
63}
64
65impl SupportedChainId {
66 #[must_use]
72 pub const fn as_u64(self) -> u64 {
73 self as u64
74 }
75
76 #[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 #[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 #[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 #[must_use]
169 pub const fn is_testnet(self) -> bool {
170 matches!(self, Self::Sepolia)
171 }
172
173 #[must_use]
183 pub const fn is_mainnet(self) -> bool {
184 !self.is_testnet()
185 }
186
187 #[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 #[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#[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
348pub enum Env {
349 #[default]
351 Prod,
352 Staging,
354}
355
356impl Env {
357 #[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 #[must_use]
377 pub const fn all() -> &'static [Self] {
378 &[Self::Prod, Self::Staging]
379 }
380
381 #[must_use]
387 pub const fn is_prod(self) -> bool {
388 matches!(self, Self::Prod)
389 }
390
391 #[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 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#[must_use]
440pub const fn api_url(chain: SupportedChainId, env: Env) -> &'static str {
441 api_base_url(chain, env)
442}
443
444#[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 #[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 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 #[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 #[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 #[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}