Skip to main content

cow_chains/
chains.rs

1//! Extended chain enums, chain info types, and utility functions.
2//!
3//! Mirrors the `TypeScript` SDK `chains` package. This module is the
4//! authoritative source for chain metadata beyond what [`SupportedChainId`]
5//! provides — it covers bridge-only chains, non-EVM chains (BTC, Solana),
6//! rich per-chain metadata ([`ChainInfo`]), and classification helpers.
7//!
8//! # Key items
9//!
10//! | Item | Purpose |
11//! |---|---|
12//! | [`EvmChains`] | Superset of [`SupportedChainId`] including bridge-only EVM chains |
13//! | [`NonEvmChains`] | BTC and Solana chain IDs |
14//! | [`AdditionalTargetChainId`] | Bridge destination chains not in [`SupportedChainId`] |
15//! | [`ChainInfo`] | Rich per-chain metadata (RPC URLs, contracts, tokens, …) |
16//! | [`get_chain_info`] | Look up [`ChainInfo`] by numeric chain ID |
17//! | [`is_evm_chain`] / [`is_btc_chain`] | Chain classification helpers |
18//! | [`all_supported_chains`] / [`all_chain_ids`] | Iteration helpers |
19
20use alloy_primitives::{Address, U256};
21use serde::{Deserialize, Serialize};
22
23/// Per-chain address mapping for supported chains.
24pub type AddressPerChain = Vec<(super::chain::SupportedChainId, Address)>;
25
26/// Per-chain base URL mapping for supported chains.
27pub type ApiBaseUrls = Vec<(super::chain::SupportedChainId, String)>;
28
29use super::chain::SupportedChainId;
30
31// ── CDN paths ────────────────────────────────────────────────────────────────
32
33/// Base CDN path for SDK files.
34pub const RAW_FILES_PATH: &str = "https://files.cow.fi/cow-sdk";
35
36/// CDN path for chain data.
37pub const RAW_CHAINS_FILES_PATH: &str = "https://files.cow.fi/cow-sdk/chains";
38
39/// CDN path for token list images.
40pub const TOKEN_LIST_IMAGES_PATH: &str = "https://files.cow.fi/token-lists/images";
41
42// ── Chain enums ──────────────────────────────────────────────────────────────
43
44/// All EVM chains supported by `CoW` Protocol or available for bridging.
45///
46/// This is a superset of [`SupportedChainId`] -- it includes bridge-only chains
47/// like Optimism.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
49#[repr(u64)]
50pub enum EvmChains {
51    /// Ethereum mainnet (chain ID 1).
52    Mainnet = 1,
53    /// Optimism (chain ID 10).
54    Optimism = 10,
55    /// BNB Smart Chain (chain ID 56).
56    Bnb = 56,
57    /// Gnosis Chain (chain ID 100).
58    GnosisChain = 100,
59    /// Polygon `PoS` (chain ID 137).
60    Polygon = 137,
61    /// Base (chain ID 8453).
62    Base = 8_453,
63    /// Plasma (chain ID 9745).
64    Plasma = 9_745,
65    /// Arbitrum One (chain ID 42161).
66    ArbitrumOne = 42_161,
67    /// Avalanche C-Chain (chain ID 43114).
68    Avalanche = 43_114,
69    /// Ink (chain ID 57073).
70    Ink = 57_073,
71    /// Linea (chain ID 59144).
72    Linea = 59_144,
73    /// Ethereum Sepolia testnet (chain ID 11155111).
74    Sepolia = 11_155_111,
75}
76
77impl EvmChains {
78    /// Return the numeric EIP-155 chain ID.
79    ///
80    /// # Returns
81    ///
82    /// The `u64` chain ID.
83    #[must_use]
84    pub const fn as_u64(self) -> u64 {
85        self as u64
86    }
87
88    /// Try to construct an [`EvmChains`] from a raw chain ID.
89    ///
90    /// # Arguments
91    ///
92    /// * `chain_id` — the numeric EIP-155 chain ID.
93    ///
94    /// # Returns
95    ///
96    /// `Some(variant)` if `chain_id` is a known EVM chain, `None` otherwise.
97    #[must_use]
98    pub const fn try_from_u64(chain_id: u64) -> Option<Self> {
99        match chain_id {
100            1 => Some(Self::Mainnet),
101            10 => Some(Self::Optimism),
102            56 => Some(Self::Bnb),
103            100 => Some(Self::GnosisChain),
104            137 => Some(Self::Polygon),
105            8_453 => Some(Self::Base),
106            9_745 => Some(Self::Plasma),
107            42_161 => Some(Self::ArbitrumOne),
108            43_114 => Some(Self::Avalanche),
109            57_073 => Some(Self::Ink),
110            59_144 => Some(Self::Linea),
111            11_155_111 => Some(Self::Sepolia),
112            _ => None,
113        }
114    }
115}
116
117/// All non-EVM chains available for bridging only.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
119#[repr(u64)]
120pub enum NonEvmChains {
121    /// Bitcoin (custom internal ID `1_000_000_000`).
122    Bitcoin = 1_000_000_000,
123    /// Solana (custom internal ID `1_000_000_001`).
124    Solana = 1_000_000_001,
125}
126
127impl NonEvmChains {
128    /// Return the numeric chain ID.
129    ///
130    /// # Returns
131    ///
132    /// The `u64` chain ID.
133    #[must_use]
134    pub const fn as_u64(self) -> u64 {
135        self as u64
136    }
137
138    /// Try to construct a [`NonEvmChains`] from a raw chain ID.
139    ///
140    /// # Arguments
141    ///
142    /// * `chain_id` — the numeric chain ID.
143    ///
144    /// # Returns
145    ///
146    /// `Some(variant)` if `chain_id` is a known non-EVM chain, `None` otherwise.
147    #[must_use]
148    pub const fn try_from_u64(chain_id: u64) -> Option<Self> {
149        match chain_id {
150            1_000_000_000 => Some(Self::Bitcoin),
151            1_000_000_001 => Some(Self::Solana),
152            _ => None,
153        }
154    }
155}
156
157/// Chains where you can buy tokens using bridge functionality but that are not
158/// directly supported by `CoW` Protocol for selling.
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
160#[repr(u64)]
161pub enum AdditionalTargetChainId {
162    /// Optimism (chain ID 10).
163    Optimism = 10,
164    /// Bitcoin (custom internal ID `1_000_000_000`).
165    Bitcoin = 1_000_000_000,
166    /// Solana (custom internal ID `1_000_000_001`).
167    Solana = 1_000_000_001,
168}
169
170impl AdditionalTargetChainId {
171    /// Return the numeric chain ID.
172    ///
173    /// # Returns
174    ///
175    /// The `u64` chain ID.
176    #[must_use]
177    pub const fn as_u64(self) -> u64 {
178        self as u64
179    }
180
181    /// Try to construct an [`AdditionalTargetChainId`] from a raw chain ID.
182    ///
183    /// # Arguments
184    ///
185    /// * `chain_id` — the numeric chain ID.
186    ///
187    /// # Returns
188    ///
189    /// `Some(variant)` if `chain_id` is a known additional target, `None` otherwise.
190    #[must_use]
191    pub const fn try_from_u64(chain_id: u64) -> Option<Self> {
192        match chain_id {
193            10 => Some(Self::Optimism),
194            1_000_000_000 => Some(Self::Bitcoin),
195            1_000_000_001 => Some(Self::Solana),
196            _ => None,
197        }
198    }
199
200    /// Return all additional target chain IDs.
201    ///
202    /// # Returns
203    ///
204    /// A static slice of every [`AdditionalTargetChainId`] variant.
205    #[must_use]
206    pub const fn all() -> &'static [Self] {
207        &[Self::Optimism, Self::Bitcoin, Self::Solana]
208    }
209}
210
211/// A chain ID that is either a [`SupportedChainId`] or an
212/// [`AdditionalTargetChainId`].
213///
214/// This union covers all chains where you can either trade directly or bridge
215/// to.
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
217pub enum TargetChainId {
218    /// A chain supported directly by `CoW` Protocol.
219    Supported(SupportedChainId),
220    /// A bridge-only target chain.
221    Additional(AdditionalTargetChainId),
222}
223
224impl TargetChainId {
225    /// Return the numeric chain ID.
226    ///
227    /// # Returns
228    ///
229    /// The `u64` chain ID from the inner variant.
230    #[must_use]
231    pub const fn as_u64(&self) -> u64 {
232        match self {
233            Self::Supported(c) => c.as_u64(),
234            Self::Additional(c) => c.as_u64(),
235        }
236    }
237}
238
239// ── Chain info types ─────────────────────────────────────────────────────────
240
241/// A themed image with light and dark variants.
242#[derive(Debug, Clone)]
243pub struct ThemedImage {
244    /// URL for the light theme logo.
245    pub light: &'static str,
246    /// URL for the dark theme logo.
247    pub dark: &'static str,
248}
249
250/// A named URL.
251#[derive(Debug, Clone)]
252pub struct WebUrl {
253    /// Display name.
254    pub name: &'static str,
255    /// The URL.
256    pub url: &'static str,
257}
258
259/// An on-chain contract reference with an optional creation block.
260#[derive(Debug, Clone, Copy)]
261pub struct ChainContract {
262    /// The contract address.
263    pub address: Address,
264    /// The block at which the contract was created, if known.
265    pub block_created: Option<u64>,
266}
267
268/// Well-known contracts on an EVM chain.
269#[derive(Debug, Clone)]
270pub struct ChainContracts {
271    /// Multicall3 contract.
272    pub multicall3: Option<ChainContract>,
273    /// ENS registry contract.
274    pub ens_registry: Option<ChainContract>,
275    /// ENS universal resolver contract.
276    pub ens_universal_resolver: Option<ChainContract>,
277}
278
279/// RPC URL configuration for an EVM chain.
280#[derive(Debug, Clone)]
281pub struct ChainRpcUrls {
282    /// HTTP RPC endpoints.
283    pub http: &'static [&'static str],
284    /// WebSocket RPC endpoints (optional).
285    pub web_socket: Option<&'static [&'static str]>,
286}
287
288/// Token info used in chain metadata.
289///
290/// Uses a string address rather than `alloy_primitives::Address` because
291/// non-EVM chains (Bitcoin, Solana) have non-hex addresses.
292#[derive(Debug, Clone)]
293pub struct ChainTokenInfo {
294    /// Chain-specific ID.
295    pub chain_id: u64,
296    /// Token address as a string.
297    pub address: &'static str,
298    /// Decimal places.
299    pub decimals: u8,
300    /// Token name.
301    pub name: &'static str,
302    /// Token symbol.
303    pub symbol: &'static str,
304    /// Logo URL, if available.
305    pub logo_url: Option<&'static str>,
306}
307
308/// Metadata for an EVM chain.
309#[derive(Debug, Clone)]
310pub struct EvmChainInfo {
311    /// The EIP-155 chain ID.
312    pub id: u64,
313    /// Display label.
314    pub label: &'static str,
315    /// EIP-155 label (used for wallet connections).
316    pub eip155_label: &'static str,
317    /// ERC-3770 address prefix.
318    pub address_prefix: &'static str,
319    /// Native currency token info.
320    pub native_currency: ChainTokenInfo,
321    /// Whether this is a testnet.
322    pub is_testnet: bool,
323    /// Brand color for UI display.
324    pub color: &'static str,
325    /// Chain logo.
326    pub logo: ThemedImage,
327    /// Chain website.
328    pub website: WebUrl,
329    /// Chain documentation.
330    pub docs: WebUrl,
331    /// Block explorer.
332    pub block_explorer: WebUrl,
333    /// Bridges.
334    pub bridges: &'static [WebUrl],
335    /// Well-known contracts.
336    pub contracts: ChainContracts,
337    /// Default RPC URLs.
338    pub rpc_urls: ChainRpcUrls,
339    /// Whether this chain is zkSync-based.
340    pub is_zk_sync: bool,
341    /// Whether this chain is under development.
342    pub is_under_development: bool,
343    /// Whether this chain is deprecated (no new trading).
344    pub is_deprecated: bool,
345}
346
347/// Metadata for a non-EVM chain (e.g. Bitcoin, Solana).
348#[derive(Debug, Clone)]
349pub struct NonEvmChainInfo {
350    /// Internal chain ID.
351    pub id: u64,
352    /// Display label.
353    pub label: &'static str,
354    /// Address prefix.
355    pub address_prefix: &'static str,
356    /// Native currency info.
357    pub native_currency: ChainTokenInfo,
358    /// Whether this is a testnet.
359    pub is_testnet: bool,
360    /// Brand color for UI display.
361    pub color: &'static str,
362    /// Chain logo.
363    pub logo: ThemedImage,
364    /// Chain website.
365    pub website: WebUrl,
366    /// Chain documentation.
367    pub docs: WebUrl,
368    /// Block explorer.
369    pub block_explorer: WebUrl,
370    /// Whether this chain is under development.
371    pub is_under_development: bool,
372    /// Whether this chain is deprecated.
373    pub is_deprecated: bool,
374}
375
376/// A chain on the network -- either an EVM chain or a non-EVM chain.
377#[derive(Debug, Clone)]
378pub enum ChainInfo {
379    /// An EVM-compatible chain.
380    Evm(EvmChainInfo),
381    /// A non-EVM chain (e.g. Bitcoin, Solana).
382    NonEvm(NonEvmChainInfo),
383}
384
385impl ChainInfo {
386    /// Return the chain ID.
387    ///
388    /// # Returns
389    ///
390    /// The `u64` chain ID from the inner variant.
391    #[must_use]
392    pub const fn id(&self) -> u64 {
393        match self {
394            Self::Evm(info) => info.id,
395            Self::NonEvm(info) => info.id,
396        }
397    }
398
399    /// Return the display label.
400    ///
401    /// # Returns
402    ///
403    /// A human-readable chain name (e.g. `"Ethereum"`, `"Bitcoin"`).
404    #[must_use]
405    pub const fn label(&self) -> &'static str {
406        match self {
407            Self::Evm(info) => info.label,
408            Self::NonEvm(info) => info.label,
409        }
410    }
411
412    /// Returns `true` if this chain is under development.
413    ///
414    /// # Returns
415    ///
416    /// `true` when the chain's `is_under_development` flag is set.
417    #[must_use]
418    pub const fn is_under_development(&self) -> bool {
419        match self {
420            Self::Evm(info) => info.is_under_development,
421            Self::NonEvm(info) => info.is_under_development,
422        }
423    }
424
425    /// Returns `true` if this chain is deprecated.
426    ///
427    /// # Returns
428    ///
429    /// `true` when the chain's `is_deprecated` flag is set.
430    #[must_use]
431    pub const fn is_deprecated(&self) -> bool {
432        match self {
433            Self::Evm(info) => info.is_deprecated,
434            Self::NonEvm(info) => info.is_deprecated,
435        }
436    }
437
438    /// Returns `true` if this is an EVM chain.
439    ///
440    /// # Returns
441    ///
442    /// `true` for the [`Evm`](Self::Evm) variant.
443    #[must_use]
444    pub const fn is_evm(&self) -> bool {
445        matches!(self, Self::Evm(_))
446    }
447
448    /// Returns `true` if this is a non-EVM chain.
449    ///
450    /// # Returns
451    ///
452    /// `true` for the [`NonEvm`](Self::NonEvm) variant.
453    #[must_use]
454    pub const fn is_non_evm(&self) -> bool {
455        matches!(self, Self::NonEvm(_))
456    }
457
458    /// Returns the inner [`EvmChainInfo`] if this is an EVM chain.
459    ///
460    /// # Returns
461    ///
462    /// `Some(&info)` for EVM chains, `None` for non-EVM chains.
463    #[must_use]
464    pub const fn as_evm(&self) -> Option<&EvmChainInfo> {
465        match self {
466            Self::Evm(info) => Some(info),
467            Self::NonEvm(_) => None,
468        }
469    }
470
471    /// Returns the inner [`NonEvmChainInfo`] if this is a non-EVM chain.
472    ///
473    /// # Returns
474    ///
475    /// `Some(&info)` for non-EVM chains, `None` for EVM chains.
476    #[must_use]
477    pub const fn as_non_evm(&self) -> Option<&NonEvmChainInfo> {
478        match self {
479            Self::NonEvm(info) => Some(info),
480            Self::Evm(_) => None,
481        }
482    }
483
484    /// Return the native currency info for this chain.
485    ///
486    /// # Returns
487    ///
488    /// A reference to the [`ChainTokenInfo`] describing the chain's native currency.
489    #[must_use]
490    pub const fn native_currency(&self) -> &ChainTokenInfo {
491        match self {
492            Self::Evm(info) => &info.native_currency,
493            Self::NonEvm(info) => &info.native_currency,
494        }
495    }
496}
497
498// ── Chain classification functions ───────────────────────────────────────────
499
500/// Check if a chain ID represents an EVM chain (including bridge-only ones
501/// like Optimism).
502///
503/// # Arguments
504///
505/// * `chain_id` — the numeric chain ID to check.
506///
507/// # Returns
508///
509/// `true` if `chain_id` belongs to a known EVM chain.
510///
511/// ```
512/// use cow_chains::chains::is_evm_chain;
513///
514/// assert!(is_evm_chain(1)); // Mainnet
515/// assert!(is_evm_chain(10)); // Optimism
516/// assert!(!is_evm_chain(1_000_000_000)); // Bitcoin
517/// ```
518#[must_use]
519pub const fn is_evm_chain(chain_id: u64) -> bool {
520    EvmChains::try_from_u64(chain_id).is_some()
521}
522
523/// Check if a chain ID represents a non-EVM chain (Bitcoin, Solana).
524///
525/// # Arguments
526///
527/// * `chain_id` — the numeric chain ID to check.
528///
529/// # Returns
530///
531/// `true` if `chain_id` belongs to a known non-EVM chain.
532///
533/// ```
534/// use cow_chains::chains::is_non_evm_chain;
535///
536/// assert!(is_non_evm_chain(1_000_000_000)); // Bitcoin
537/// assert!(is_non_evm_chain(1_000_000_001)); // Solana
538/// assert!(!is_non_evm_chain(1)); // Mainnet
539/// ```
540#[must_use]
541pub const fn is_non_evm_chain(chain_id: u64) -> bool {
542    NonEvmChains::try_from_u64(chain_id).is_some()
543}
544
545/// Check if a [`ChainInfo`] represents an EVM chain.
546///
547/// Type guard equivalent of `isEvmChainInfo` from the `TypeScript` SDK.
548///
549/// # Arguments
550///
551/// * `chain_info` — the [`ChainInfo`] to inspect.
552///
553/// # Returns
554///
555/// `true` for the [`ChainInfo::Evm`] variant.
556///
557/// ```
558/// use cow_chains::{
559///     SupportedChainId,
560///     chains::{is_evm_chain_info, supported_chain_info},
561/// };
562///
563/// let info = supported_chain_info(SupportedChainId::Mainnet);
564/// assert!(is_evm_chain_info(&info));
565/// ```
566#[must_use]
567pub const fn is_evm_chain_info(chain_info: &ChainInfo) -> bool {
568    chain_info.is_evm()
569}
570
571/// Check if a [`ChainInfo`] represents a non-EVM chain.
572///
573/// Type guard equivalent of `isNonEvmChainInfo` from the `TypeScript` SDK.
574///
575/// # Arguments
576///
577/// * `chain_info` — the [`ChainInfo`] to inspect.
578///
579/// # Returns
580///
581/// `true` for the [`ChainInfo::NonEvm`] variant.
582///
583/// ```
584/// use cow_chains::{
585///     AdditionalTargetChainId,
586///     chains::{additional_target_chain_info, is_non_evm_chain_info},
587/// };
588///
589/// let info = additional_target_chain_info(AdditionalTargetChainId::Bitcoin);
590/// assert!(is_non_evm_chain_info(&info));
591/// ```
592#[must_use]
593pub const fn is_non_evm_chain_info(chain_info: &ChainInfo) -> bool {
594    chain_info.is_non_evm()
595}
596
597/// Check if a chain ID represents Bitcoin.
598///
599/// # Arguments
600///
601/// * `chain_id` — the numeric chain ID to check.
602///
603/// # Returns
604///
605/// `true` when `chain_id` equals [`NonEvmChains::Bitcoin`].
606///
607/// ```
608/// use cow_chains::chains::is_btc_chain;
609///
610/// assert!(is_btc_chain(1_000_000_000));
611/// assert!(!is_btc_chain(1));
612/// ```
613#[must_use]
614pub const fn is_btc_chain(chain_id: u64) -> bool {
615    chain_id == NonEvmChains::Bitcoin as u64
616}
617
618/// Check if a chain ID is the Solana non-EVM chain.
619///
620/// # Arguments
621///
622/// * `chain_id` — the numeric chain ID to check.
623///
624/// # Returns
625///
626/// `true` when `chain_id` equals [`NonEvmChains::Solana`].
627///
628/// ```
629/// use cow_chains::chains::is_solana_chain;
630///
631/// assert!(is_solana_chain(1_000_000_001));
632/// assert!(!is_solana_chain(1));
633/// ```
634#[must_use]
635pub const fn is_solana_chain(chain_id: u64) -> bool {
636    chain_id == NonEvmChains::Solana as u64
637}
638
639/// Check if a chain ID is directly supported by `CoW` Protocol for trading.
640///
641/// # Arguments
642///
643/// * `chain_id` — the numeric chain ID to check.
644///
645/// # Returns
646///
647/// `true` if `chain_id` is a [`SupportedChainId`] variant.
648///
649/// ```
650/// use cow_chains::chains::is_supported_chain;
651///
652/// assert!(is_supported_chain(1)); // Mainnet
653/// assert!(!is_supported_chain(10)); // Optimism (bridge-only)
654/// ```
655#[must_use]
656pub const fn is_supported_chain(chain_id: u64) -> bool {
657    SupportedChainId::try_from_u64(chain_id).is_some()
658}
659
660/// Check if a chain ID is a bridge-only target chain.
661///
662/// # Arguments
663///
664/// * `chain_id` — the numeric chain ID to check.
665///
666/// # Returns
667///
668/// `true` if `chain_id` is an [`AdditionalTargetChainId`] variant.
669///
670/// ```
671/// use cow_chains::chains::is_additional_target_chain;
672///
673/// assert!(is_additional_target_chain(10)); // Optimism
674/// assert!(is_additional_target_chain(1_000_000_000)); // Bitcoin
675/// assert!(!is_additional_target_chain(1)); // Mainnet
676/// ```
677#[must_use]
678pub const fn is_additional_target_chain(chain_id: u64) -> bool {
679    AdditionalTargetChainId::try_from_u64(chain_id).is_some()
680}
681
682/// Check if a chain ID is either a supported chain or a bridge target.
683///
684/// # Arguments
685///
686/// * `chain_id` — the numeric chain ID to check.
687///
688/// # Returns
689///
690/// `true` if `chain_id` is a supported or additional target chain.
691///
692/// ```
693/// use cow_chains::chains::is_target_chain_id;
694///
695/// assert!(is_target_chain_id(1)); // Mainnet (supported)
696/// assert!(is_target_chain_id(10)); // Optimism (bridge target)
697/// assert!(is_target_chain_id(1_000_000_000)); // Bitcoin (bridge target)
698/// assert!(!is_target_chain_id(999)); // Unknown
699/// ```
700#[must_use]
701pub const fn is_target_chain_id(chain_id: u64) -> bool {
702    is_supported_chain(chain_id) || is_additional_target_chain(chain_id)
703}
704
705/// Check if a chain is zkSync-based.
706///
707/// # Arguments
708///
709/// * `chain_id` — the numeric chain ID to check.
710///
711/// # Returns
712///
713/// `true` if the chain is an EVM chain with `is_zk_sync` set.
714///
715/// ```
716/// use cow_chains::chains::is_zk_sync_chain;
717///
718/// assert!(!is_zk_sync_chain(1)); // Mainnet
719/// ```
720#[must_use]
721pub fn is_zk_sync_chain(chain_id: u64) -> bool {
722    if !is_evm_chain(chain_id) {
723        return false;
724    }
725    get_chain_info(chain_id)
726        .and_then(|info| info.as_evm().cloned())
727        .is_some_and(|evm| evm.is_zk_sync)
728}
729
730/// Check if a chain is under development.
731///
732/// # Arguments
733///
734/// * `chain_id` — the numeric chain ID to check.
735///
736/// # Returns
737///
738/// `true` if the chain exists and its `is_under_development` flag is set.
739///
740/// ```
741/// use cow_chains::chains::is_chain_under_development;
742///
743/// assert!(!is_chain_under_development(1)); // Mainnet
744/// ```
745#[must_use]
746pub fn is_chain_under_development(chain_id: u64) -> bool {
747    get_chain_info(chain_id).is_some_and(|info| info.is_under_development())
748}
749
750/// Check if a chain is deprecated (no new trading; chain remains for
751/// history/Explorer).
752///
753/// # Arguments
754///
755/// * `chain_id` — the numeric chain ID to check.
756///
757/// # Returns
758///
759/// `true` if the chain exists and its `is_deprecated` flag is set.
760///
761/// ```
762/// use cow_chains::chains::is_chain_deprecated;
763///
764/// assert!(!is_chain_deprecated(1)); // Mainnet
765/// ```
766#[must_use]
767pub fn is_chain_deprecated(chain_id: u64) -> bool {
768    get_chain_info(chain_id).is_some_and(|info| info.is_deprecated())
769}
770
771/// Return the chain info for a given chain ID, or `None` if the chain is not
772/// known.
773///
774/// Looks up both supported chains and additional target chains.
775///
776/// # Arguments
777///
778/// * `chain_id` — the numeric chain ID to look up.
779///
780/// # Returns
781///
782/// `Some(chain_info)` if the chain is known, `None` otherwise.
783///
784/// ```
785/// use cow_chains::chains::get_chain_info;
786///
787/// let info = get_chain_info(1).unwrap();
788/// assert_eq!(info.label(), "Ethereum");
789///
790/// let btc = get_chain_info(1_000_000_000).unwrap();
791/// assert_eq!(btc.label(), "Bitcoin");
792///
793/// assert!(get_chain_info(9999).is_none());
794/// ```
795#[must_use]
796pub const fn get_chain_info(chain_id: u64) -> Option<ChainInfo> {
797    if let Some(supported) = SupportedChainId::try_from_u64(chain_id) {
798        return Some(supported_chain_info(supported));
799    }
800
801    if let Some(additional) = AdditionalTargetChainId::try_from_u64(chain_id) {
802        return Some(additional_target_chain_info(additional));
803    }
804
805    None
806}
807
808// ── Collection helpers ───────────────────────────────────────────────────────
809
810/// Map a value across all supported chains, returning a `Vec` of
811/// `(SupportedChainId, T)` pairs.
812///
813/// # Arguments
814///
815/// * `f` — a closure that maps each [`SupportedChainId`] to a value of type `T`.
816///
817/// # Returns
818///
819/// A `Vec` of `(chain, value)` pairs for every supported chain.
820///
821/// ```
822/// use cow_chains::chains::map_supported_networks;
823///
824/// let names = map_supported_networks(|chain| chain.to_string());
825/// assert!(!names.is_empty());
826/// ```
827#[must_use]
828pub fn map_supported_networks<T>(f: impl Fn(SupportedChainId) -> T) -> Vec<(SupportedChainId, T)> {
829    SupportedChainId::all().iter().map(|&chain| (chain, f(chain))).collect()
830}
831
832/// Map a value across all target chains (supported + additional), returning a
833/// `Vec` of `(TargetChainId, T)` pairs.
834///
835/// # Arguments
836///
837/// * `f` — a closure that maps each [`TargetChainId`] to a value of type `T`.
838///
839/// # Returns
840///
841/// A `Vec` of `(chain, value)` pairs for every known chain.
842#[must_use]
843pub fn map_all_networks<T>(f: impl Fn(TargetChainId) -> T) -> Vec<(TargetChainId, T)> {
844    all_chain_ids().into_iter().map(|id| (id, f(id))).collect()
845}
846
847/// Map an address to all supported networks.
848///
849/// Useful for contracts that have the same address on every chain.
850///
851/// # Arguments
852///
853/// * `address` — the [`Address`] to replicate across all chains.
854///
855/// # Returns
856///
857/// A `Vec` of `(chain, address)` pairs for every supported chain.
858#[must_use]
859pub fn map_address_to_supported_networks(address: Address) -> Vec<(SupportedChainId, Address)> {
860    map_supported_networks(|_| address)
861}
862
863/// Return all supported chain IDs as a `Vec`.
864///
865/// # Returns
866///
867/// A `Vec` containing every [`SupportedChainId`] variant.
868#[must_use]
869pub fn all_supported_chain_ids() -> Vec<SupportedChainId> {
870    SupportedChainId::all().to_vec()
871}
872
873/// Return chain info for all supported chains.
874///
875/// # Returns
876///
877/// A `Vec` of [`ChainInfo`] for every supported chain.
878#[must_use]
879pub fn all_supported_chains() -> Vec<ChainInfo> {
880    SupportedChainId::all().iter().map(|&c| supported_chain_info(c)).collect()
881}
882
883/// Return chain IDs where new trading is allowed (excludes deprecated chains).
884///
885/// # Returns
886///
887/// A `Vec` of [`SupportedChainId`] variants that are not deprecated.
888#[must_use]
889pub fn tradable_supported_chain_ids() -> Vec<SupportedChainId> {
890    SupportedChainId::all()
891        .iter()
892        .copied()
893        .filter(|&c| !supported_chain_info(c).is_deprecated())
894        .collect()
895}
896
897/// Return chain info for tradable supported chains (excludes deprecated).
898///
899/// # Returns
900///
901/// A `Vec` of [`ChainInfo`] for supported chains that are not deprecated.
902#[must_use]
903pub fn tradable_supported_chains() -> Vec<ChainInfo> {
904    SupportedChainId::all()
905        .iter()
906        .copied()
907        .filter(|&c| !supported_chain_info(c).is_deprecated())
908        .map(supported_chain_info)
909        .collect()
910}
911
912/// Return chain info for all additional target chains (bridge-only).
913///
914/// # Returns
915///
916/// A `Vec` of [`ChainInfo`] for every bridge-only target chain.
917#[must_use]
918pub fn all_additional_target_chains() -> Vec<ChainInfo> {
919    AdditionalTargetChainId::all().iter().map(|&c| additional_target_chain_info(c)).collect()
920}
921
922/// Return all chain IDs for additional target chains (bridge-only).
923///
924/// # Returns
925///
926/// A `Vec` containing every [`AdditionalTargetChainId`] variant.
927#[must_use]
928pub fn all_additional_target_chain_ids() -> Vec<AdditionalTargetChainId> {
929    AdditionalTargetChainId::all().to_vec()
930}
931
932/// Return chain info for all known chains (both supported and bridge-only).
933///
934/// # Returns
935///
936/// A `Vec` of [`ChainInfo`] for every known chain.
937#[must_use]
938pub fn all_chains() -> Vec<ChainInfo> {
939    let mut chains = all_supported_chains();
940    chains.extend(all_additional_target_chains());
941    chains
942}
943
944/// Return all known chain IDs as [`TargetChainId`] values.
945///
946/// # Returns
947///
948/// A `Vec` of [`TargetChainId`] covering both supported and additional target chains.
949#[must_use]
950pub fn all_chain_ids() -> Vec<TargetChainId> {
951    let mut ids: Vec<TargetChainId> =
952        SupportedChainId::all().iter().map(|&c| TargetChainId::Supported(c)).collect();
953    ids.extend(AdditionalTargetChainId::all().iter().map(|&c| TargetChainId::Additional(c)));
954    ids
955}
956
957/// Return a map of every known chain keyed by its numeric chain ID.
958///
959/// Contains entries for both `CoW` Protocol supported chains and
960/// bridge-only additional target chains. The `TypeScript` SDK equivalent
961/// is `ALL_CHAINS_MAP`; the Rust version returns a
962/// [`foldhash::HashMap`].
963///
964/// # Returns
965///
966/// A `HashMap<u64, ChainInfo>` covering every chain in [`all_chains`].
967#[must_use]
968pub fn all_chains_map() -> foldhash::HashMap<u64, ChainInfo> {
969    all_chains().into_iter().map(|info| (info.id(), info)).collect()
970}
971
972// ── Chain info data ──────────────────────────────────────────────────────────
973
974/// Return the [`ChainInfo`] for a [`SupportedChainId`].
975///
976/// # Arguments
977///
978/// * `chain` — the supported chain to look up.
979///
980/// # Returns
981///
982/// A [`ChainInfo::Evm`] containing the chain's metadata.
983#[must_use]
984pub const fn supported_chain_info(chain: SupportedChainId) -> ChainInfo {
985    ChainInfo::Evm(evm_chain_detail(chain))
986}
987
988/// Return the [`ChainInfo`] for an [`AdditionalTargetChainId`].
989///
990/// # Arguments
991///
992/// * `chain` — the additional target chain to look up.
993///
994/// # Returns
995///
996/// A [`ChainInfo`] (EVM for Optimism, non-EVM for Bitcoin/Solana).
997#[must_use]
998pub const fn additional_target_chain_info(chain: AdditionalTargetChainId) -> ChainInfo {
999    match chain {
1000        AdditionalTargetChainId::Optimism => ChainInfo::Evm(optimism_chain_info()),
1001        AdditionalTargetChainId::Bitcoin => ChainInfo::NonEvm(bitcoin_chain_info()),
1002        AdditionalTargetChainId::Solana => ChainInfo::NonEvm(solana_chain_info()),
1003    }
1004}
1005
1006const fn evm_chain_detail(chain: SupportedChainId) -> EvmChainInfo {
1007    match chain {
1008        SupportedChainId::Mainnet => mainnet_chain_info(),
1009        SupportedChainId::GnosisChain => gnosis_chain_info(),
1010        SupportedChainId::ArbitrumOne => arbitrum_chain_info(),
1011        SupportedChainId::Base => base_chain_info(),
1012        SupportedChainId::Sepolia => sepolia_chain_info(),
1013        SupportedChainId::Polygon => polygon_chain_info(),
1014        SupportedChainId::Avalanche => avalanche_chain_info(),
1015        SupportedChainId::BnbChain => bnb_chain_info(),
1016        SupportedChainId::Linea => linea_chain_info(),
1017        SupportedChainId::Plasma => plasma_chain_info(),
1018        SupportedChainId::Ink => ink_chain_info(),
1019    }
1020}
1021
1022/// The standard EVM native currency address.
1023const EVM_NATIVE_ADDR: &str = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
1024/// The BTC genesis address used as token address.
1025const BTC_ADDR: &str = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa";
1026/// The SOL default program address used as token address.
1027const SOL_ADDR: &str = "11111111111111111111111111111111";
1028
1029// Multicall3 address constant used by most chains.
1030const MULTICALL3: Address = Address::new([
1031    0xca, 0x11, 0xbd, 0xe0, 0x59, 0x77, 0xb3, 0x63, 0x11, 0x67, 0x02, 0x88, 0x62, 0xbe, 0x2a, 0x17,
1032    0x39, 0x76, 0xca, 0x11,
1033]);
1034
1035const fn default_native_currency(chain_id: u64) -> ChainTokenInfo {
1036    ChainTokenInfo {
1037        chain_id,
1038        address: EVM_NATIVE_ADDR,
1039        decimals: 18,
1040        name: "Ether",
1041        symbol: "ETH",
1042        logo_url: Some(
1043            "https://files.cow.fi/token-lists/images/1/0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/logo.png",
1044        ),
1045    }
1046}
1047
1048const fn multicall3_only(block_created: u64) -> ChainContracts {
1049    ChainContracts {
1050        multicall3: Some(ChainContract { address: MULTICALL3, block_created: Some(block_created) }),
1051        ens_registry: None,
1052        ens_universal_resolver: None,
1053    }
1054}
1055
1056const fn mainnet_chain_info() -> EvmChainInfo {
1057    EvmChainInfo {
1058        id: 1,
1059        label: "Ethereum",
1060        eip155_label: "Ethereum Mainnet",
1061        address_prefix: "eth",
1062        native_currency: default_native_currency(1),
1063        is_testnet: false,
1064        color: "#62688F",
1065        logo: ThemedImage {
1066            light: "https://files.cow.fi/cow-sdk/chains/images/mainnet-logo.svg",
1067            dark: "https://files.cow.fi/cow-sdk/chains/images/mainnet-logo.svg",
1068        },
1069        website: WebUrl { name: "Ethereum", url: "https://ethereum.org" },
1070        docs: WebUrl { name: "Ethereum Docs", url: "https://ethereum.org/en/developers/docs" },
1071        block_explorer: WebUrl { name: "Etherscan", url: "https://etherscan.io" },
1072        bridges: &[],
1073        contracts: ChainContracts {
1074            multicall3: Some(ChainContract {
1075                address: MULTICALL3,
1076                block_created: Some(14_353_601),
1077            }),
1078            ens_registry: Some(ChainContract {
1079                address: Address::new([
1080                    0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x2e, 0x07, 0x4e, 0xc6, 0x9a, 0x0d, 0xfb,
1081                    0x29, 0x97, 0xba, 0x6c, 0x7d, 0x2e, 0x1e,
1082                ]),
1083                block_created: None,
1084            }),
1085            ens_universal_resolver: Some(ChainContract {
1086                address: Address::new([
1087                    0xce, 0x01, 0xf8, 0xee, 0xe7, 0xe4, 0x79, 0xc9, 0x28, 0xf8, 0x91, 0x9a, 0xbd,
1088                    0x53, 0xe5, 0x53, 0xa3, 0x6c, 0xef, 0x67,
1089                ]),
1090                block_created: Some(19_258_213),
1091            }),
1092        },
1093        rpc_urls: ChainRpcUrls { http: &["https://eth.merkle.io"], web_socket: None },
1094        is_zk_sync: false,
1095        is_under_development: false,
1096        is_deprecated: false,
1097    }
1098}
1099
1100const fn gnosis_chain_info() -> EvmChainInfo {
1101    EvmChainInfo {
1102        id: 100,
1103        label: "Gnosis",
1104        eip155_label: "Gnosis",
1105        address_prefix: "gno",
1106        native_currency: ChainTokenInfo {
1107            chain_id: 100,
1108            address: EVM_NATIVE_ADDR,
1109            decimals: 18,
1110            name: "xDAI",
1111            symbol: "xDAI",
1112            logo_url: Some(
1113                "https://files.cow.fi/token-lists/images/100/0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/logo.png",
1114            ),
1115        },
1116        is_testnet: false,
1117        color: "#07795B",
1118        logo: ThemedImage {
1119            light: "https://files.cow.fi/cow-sdk/chains/images/gnosis-logo.svg",
1120            dark: "https://files.cow.fi/cow-sdk/chains/images/gnosis-logo.svg",
1121        },
1122        website: WebUrl { name: "Gnosis Chain", url: "https://www.gnosischain.com" },
1123        docs: WebUrl { name: "Gnosis Chain Docs", url: "https://docs.gnosischain.com" },
1124        block_explorer: WebUrl { name: "Gnosisscan", url: "https://gnosisscan.io" },
1125        bridges: &[WebUrl { name: "Gnosis Chain Bridge", url: "https://bridge.gnosischain.com" }],
1126        contracts: multicall3_only(21_022_491),
1127        rpc_urls: ChainRpcUrls {
1128            http: &["https://rpc.gnosischain.com"],
1129            web_socket: Some(&["wss://rpc.gnosischain.com/wss"]),
1130        },
1131        is_zk_sync: false,
1132        is_under_development: false,
1133        is_deprecated: false,
1134    }
1135}
1136
1137const fn arbitrum_chain_info() -> EvmChainInfo {
1138    EvmChainInfo {
1139        id: 42_161,
1140        label: "Arbitrum",
1141        eip155_label: "Arbitrum One",
1142        address_prefix: "arb1",
1143        native_currency: default_native_currency(42_161),
1144        is_testnet: false,
1145        color: "#1B4ADD",
1146        logo: ThemedImage {
1147            light: "https://files.cow.fi/cow-sdk/chains/images/arbitrum-logo.svg",
1148            dark: "https://files.cow.fi/cow-sdk/chains/images/arbitrum-logo.svg",
1149        },
1150        website: WebUrl { name: "Arbitrum", url: "https://arbitrum.io" },
1151        docs: WebUrl { name: "Arbitrum Docs", url: "https://docs.arbitrum.io" },
1152        block_explorer: WebUrl { name: "Arbiscan", url: "https://arbiscan.io" },
1153        bridges: &[WebUrl { name: "Arbitrum Bridge", url: "https://bridge.arbitrum.io" }],
1154        contracts: multicall3_only(7_654_707),
1155        rpc_urls: ChainRpcUrls { http: &["https://arb1.arbitrum.io/rpc"], web_socket: None },
1156        is_zk_sync: false,
1157        is_under_development: false,
1158        is_deprecated: false,
1159    }
1160}
1161
1162const fn base_chain_info() -> EvmChainInfo {
1163    EvmChainInfo {
1164        id: 8_453,
1165        label: "Base",
1166        eip155_label: "Base",
1167        address_prefix: "base",
1168        native_currency: default_native_currency(8_453),
1169        is_testnet: false,
1170        color: "#0052FF",
1171        logo: ThemedImage {
1172            light: "https://files.cow.fi/cow-sdk/chains/images/base-logo.svg",
1173            dark: "https://files.cow.fi/cow-sdk/chains/images/base-logo.svg",
1174        },
1175        website: WebUrl { name: "Base", url: "https://base.org" },
1176        docs: WebUrl { name: "Base Docs", url: "https://docs.base.org" },
1177        block_explorer: WebUrl { name: "Basescan", url: "https://basescan.org" },
1178        bridges: &[WebUrl { name: "Superchain Bridges", url: "https://bridge.base.org/deposit" }],
1179        contracts: multicall3_only(5022),
1180        rpc_urls: ChainRpcUrls { http: &["https://mainnet.base.org"], web_socket: None },
1181        is_zk_sync: false,
1182        is_under_development: false,
1183        is_deprecated: false,
1184    }
1185}
1186
1187const fn sepolia_chain_info() -> EvmChainInfo {
1188    EvmChainInfo {
1189        id: 11_155_111,
1190        label: "Sepolia",
1191        eip155_label: "Ethereum Sepolia",
1192        address_prefix: "sep",
1193        native_currency: default_native_currency(11_155_111),
1194        is_testnet: true,
1195        color: "#C12FF2",
1196        logo: ThemedImage {
1197            light: "https://files.cow.fi/cow-sdk/chains/images/sepolia-logo.svg",
1198            dark: "https://files.cow.fi/cow-sdk/chains/images/sepolia-logo.svg",
1199        },
1200        website: WebUrl { name: "Ethereum", url: "https://sepolia.dev" },
1201        docs: WebUrl {
1202            name: "Sepolia Docs",
1203            url: "https://ethereum.org/en/developers/docs/networks/#sepolia",
1204        },
1205        block_explorer: WebUrl { name: "Etherscan", url: "https://sepolia.etherscan.io" },
1206        bridges: &[],
1207        contracts: ChainContracts {
1208            multicall3: Some(ChainContract { address: MULTICALL3, block_created: Some(751_532) }),
1209            ens_registry: Some(ChainContract {
1210                address: Address::new([
1211                    0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x2e, 0x07, 0x4e, 0xc6, 0x9a, 0x0d, 0xfb,
1212                    0x29, 0x97, 0xba, 0x6c, 0x7d, 0x2e, 0x1e,
1213                ]),
1214                block_created: None,
1215            }),
1216            ens_universal_resolver: Some(ChainContract {
1217                address: Address::new([
1218                    0xc8, 0xaf, 0x99, 0x9e, 0x38, 0x27, 0x3d, 0x65, 0x8b, 0xe1, 0xb9, 0x21, 0xb8,
1219                    0x8a, 0x9d, 0xdf, 0x00, 0x57, 0x69, 0xcc,
1220                ]),
1221                block_created: Some(5_317_080),
1222            }),
1223        },
1224        rpc_urls: ChainRpcUrls { http: &["https://sepolia.drpc.org"], web_socket: None },
1225        is_zk_sync: false,
1226        is_under_development: false,
1227        is_deprecated: false,
1228    }
1229}
1230
1231const fn polygon_chain_info() -> EvmChainInfo {
1232    let logo_url = "https://files.cow.fi/cow-sdk/chains/images/polygon-logo.svg";
1233    EvmChainInfo {
1234        id: 137,
1235        label: "Polygon",
1236        eip155_label: "Polygon Mainnet",
1237        address_prefix: "matic",
1238        native_currency: ChainTokenInfo {
1239            chain_id: 137,
1240            address: EVM_NATIVE_ADDR,
1241            decimals: 18,
1242            name: "POL",
1243            symbol: "POL",
1244            logo_url: Some(logo_url),
1245        },
1246        is_testnet: false,
1247        color: "#8247e5",
1248        logo: ThemedImage { light: logo_url, dark: logo_url },
1249        website: WebUrl { name: "Polygon", url: "https://polygon.technology" },
1250        docs: WebUrl { name: "Polygon Docs", url: "https://docs.polygon.technology" },
1251        block_explorer: WebUrl { name: "Polygonscan", url: "https://polygonscan.com" },
1252        bridges: &[],
1253        contracts: multicall3_only(25_770_160),
1254        rpc_urls: ChainRpcUrls { http: &["https://polygon-rpc.com"], web_socket: None },
1255        is_zk_sync: false,
1256        is_under_development: false,
1257        is_deprecated: false,
1258    }
1259}
1260
1261const fn avalanche_chain_info() -> EvmChainInfo {
1262    let logo_url = "https://files.cow.fi/cow-sdk/chains/images/avax-logo.svg";
1263    EvmChainInfo {
1264        id: 43_114,
1265        label: "Avalanche",
1266        eip155_label: "Avalanche C-Chain",
1267        address_prefix: "avax",
1268        native_currency: ChainTokenInfo {
1269            chain_id: 43_114,
1270            address: EVM_NATIVE_ADDR,
1271            decimals: 18,
1272            name: "Avalanche",
1273            symbol: "AVAX",
1274            logo_url: Some(logo_url),
1275        },
1276        is_testnet: false,
1277        color: "#ff3944",
1278        logo: ThemedImage { light: logo_url, dark: logo_url },
1279        website: WebUrl { name: "Avalanche", url: "https://www.avax.network/" },
1280        docs: WebUrl { name: "Avalanche Docs", url: "https://build.avax.network/docs" },
1281        block_explorer: WebUrl { name: "Snowscan", url: "https://snowscan.xyz" },
1282        bridges: &[],
1283        contracts: multicall3_only(11_907_934),
1284        rpc_urls: ChainRpcUrls {
1285            http: &["https://api.avax.network/ext/bc/C/rpc"],
1286            web_socket: None,
1287        },
1288        is_zk_sync: false,
1289        is_under_development: false,
1290        is_deprecated: false,
1291    }
1292}
1293
1294const fn bnb_chain_info() -> EvmChainInfo {
1295    let logo_url = "https://files.cow.fi/cow-sdk/chains/images/bnb-logo.svg";
1296    EvmChainInfo {
1297        id: 56,
1298        label: "BNB",
1299        eip155_label: "BNB Chain Mainnet",
1300        address_prefix: "bnb",
1301        native_currency: ChainTokenInfo {
1302            chain_id: 56,
1303            address: EVM_NATIVE_ADDR,
1304            decimals: 18,
1305            name: "BNB Chain Native Token",
1306            symbol: "BNB",
1307            logo_url: Some(logo_url),
1308        },
1309        is_testnet: false,
1310        color: "#F0B90B",
1311        logo: ThemedImage { light: logo_url, dark: logo_url },
1312        website: WebUrl { name: "BNB Chain", url: "https://www.bnbchain.org" },
1313        docs: WebUrl { name: "BNB Chain Docs", url: "https://docs.bnbchain.org" },
1314        block_explorer: WebUrl { name: "Bscscan", url: "https://bscscan.com" },
1315        bridges: &[WebUrl {
1316            name: "BNB Chain Cross-Chain Bridge",
1317            url: "https://www.bnbchain.org/en/bnb-chain-bridge",
1318        }],
1319        contracts: multicall3_only(15_921_452),
1320        rpc_urls: ChainRpcUrls { http: &["https://bsc-dataseed1.bnbchain.org"], web_socket: None },
1321        is_zk_sync: false,
1322        is_under_development: false,
1323        is_deprecated: false,
1324    }
1325}
1326
1327const fn linea_chain_info() -> EvmChainInfo {
1328    let logo_url = "https://files.cow.fi/cow-sdk/chains/images/linea-logo.svg";
1329    EvmChainInfo {
1330        id: 59_144,
1331        label: "Linea",
1332        eip155_label: "Linea Mainnet",
1333        address_prefix: "linea",
1334        native_currency: default_native_currency(59_144),
1335        is_testnet: false,
1336        color: "#61dfff",
1337        logo: ThemedImage { light: logo_url, dark: logo_url },
1338        website: WebUrl { name: "Linea", url: "https://linea.build" },
1339        docs: WebUrl { name: "Linea Docs", url: "https://docs.linea.build" },
1340        block_explorer: WebUrl { name: "Lineascan", url: "https://lineascan.build" },
1341        bridges: &[WebUrl { name: "Linea Bridge", url: "https://linea.build/hub/bridge" }],
1342        contracts: multicall3_only(42),
1343        rpc_urls: ChainRpcUrls { http: &["https://rpc.linea.build"], web_socket: None },
1344        is_zk_sync: false,
1345        is_under_development: false,
1346        is_deprecated: false,
1347    }
1348}
1349
1350const fn plasma_chain_info() -> EvmChainInfo {
1351    let logo_url = "https://files.cow.fi/cow-sdk/chains/images/plasma-logo.svg";
1352    EvmChainInfo {
1353        id: 9_745,
1354        label: "Plasma",
1355        eip155_label: "Plasma Mainnet",
1356        address_prefix: "plasma",
1357        native_currency: ChainTokenInfo {
1358            chain_id: 9_745,
1359            address: EVM_NATIVE_ADDR,
1360            decimals: 18,
1361            name: "Plasma",
1362            symbol: "XPL",
1363            logo_url: Some(logo_url),
1364        },
1365        is_testnet: false,
1366        color: "#569F8C",
1367        logo: ThemedImage { light: logo_url, dark: logo_url },
1368        website: WebUrl { name: "Plasma", url: "https://www.plasma.to" },
1369        docs: WebUrl { name: "Plasma Docs", url: "https://docs.plasma.to" },
1370        block_explorer: WebUrl { name: "Plasmascan", url: "https://plasmascan.to" },
1371        bridges: &[],
1372        contracts: multicall3_only(0),
1373        rpc_urls: ChainRpcUrls { http: &["https://rpc.plasma.to"], web_socket: None },
1374        is_zk_sync: false,
1375        is_under_development: false,
1376        is_deprecated: false,
1377    }
1378}
1379
1380const fn ink_chain_info() -> EvmChainInfo {
1381    let logo_url = "https://files.cow.fi/cow-sdk/chains/images/ink-logo.svg";
1382    EvmChainInfo {
1383        id: 57_073,
1384        label: "Ink",
1385        eip155_label: "Ink Chain Mainnet",
1386        address_prefix: "ink",
1387        native_currency: default_native_currency(57_073),
1388        is_testnet: false,
1389        color: "#7132f5",
1390        logo: ThemedImage { light: logo_url, dark: logo_url },
1391        website: WebUrl { name: "Ink", url: "https://inkonchain.com/" },
1392        docs: WebUrl { name: "Ink Docs", url: "https://docs.inkonchain.com" },
1393        block_explorer: WebUrl { name: "Ink Explorer", url: "https://explorer.inkonchain.com" },
1394        bridges: &[WebUrl { name: "Ink Bridge", url: "https://inkonchain.com/bridge" }],
1395        contracts: multicall3_only(0),
1396        rpc_urls: ChainRpcUrls { http: &["https://rpc-ten.inkonchain.com"], web_socket: None },
1397        is_zk_sync: false,
1398        is_under_development: false,
1399        is_deprecated: false,
1400    }
1401}
1402
1403const fn optimism_chain_info() -> EvmChainInfo {
1404    let logo_url = "https://files.cow.fi/cow-sdk/chains/images/optimism-logo.svg";
1405    EvmChainInfo {
1406        id: 10,
1407        label: "Optimism",
1408        eip155_label: "OP Mainnet",
1409        address_prefix: "op",
1410        native_currency: default_native_currency(10),
1411        is_testnet: false,
1412        color: "#ff0420",
1413        logo: ThemedImage { light: logo_url, dark: logo_url },
1414        website: WebUrl { name: "Optimism", url: "https://optimism.io" },
1415        docs: WebUrl { name: "Optimism Docs", url: "https://docs.optimism.io" },
1416        block_explorer: WebUrl { name: "Etherscan", url: "https://optimistic.etherscan.io" },
1417        bridges: &[],
1418        contracts: multicall3_only(4_286_263),
1419        rpc_urls: ChainRpcUrls { http: &["https://mainnet.optimism.io"], web_socket: None },
1420        is_zk_sync: false,
1421        is_under_development: false,
1422        is_deprecated: false,
1423    }
1424}
1425
1426const fn bitcoin_chain_info() -> NonEvmChainInfo {
1427    let logo_url = "https://files.cow.fi/cow-sdk/chains/images/bitcoin-logo.svg";
1428    NonEvmChainInfo {
1429        id: 1_000_000_000,
1430        label: "Bitcoin",
1431        address_prefix: "btc",
1432        native_currency: ChainTokenInfo {
1433            chain_id: 1_000_000_000,
1434            address: BTC_ADDR,
1435            decimals: 8,
1436            name: "Bitcoin",
1437            symbol: "BTC",
1438            logo_url: Some(logo_url),
1439        },
1440        is_testnet: false,
1441        color: "#f7931a",
1442        logo: ThemedImage { light: logo_url, dark: logo_url },
1443        website: WebUrl { name: "Bitcoin", url: "https://bitcoin.org" },
1444        docs: WebUrl {
1445            name: "Bitcoin Docs",
1446            url: "https://bitcoin.org/en/developer-documentation",
1447        },
1448        block_explorer: WebUrl { name: "Blockstream Explorer", url: "https://blockstream.info" },
1449        is_under_development: false,
1450        is_deprecated: false,
1451    }
1452}
1453
1454const fn solana_chain_info() -> NonEvmChainInfo {
1455    let logo_url = "https://files.cow.fi/cow-sdk/chains/images/solana-logo.svg";
1456    NonEvmChainInfo {
1457        id: 1_000_000_001,
1458        label: "Solana",
1459        address_prefix: "sol",
1460        native_currency: ChainTokenInfo {
1461            chain_id: 1_000_000_001,
1462            address: SOL_ADDR,
1463            decimals: 9,
1464            name: "Solana",
1465            symbol: "SOL",
1466            logo_url: Some(logo_url),
1467        },
1468        is_testnet: false,
1469        color: "#9945FF",
1470        logo: ThemedImage { light: logo_url, dark: logo_url },
1471        website: WebUrl { name: "Solana", url: "https://solana.com" },
1472        docs: WebUrl { name: "Solana Docs", url: "https://docs.solana.com" },
1473        block_explorer: WebUrl { name: "Solana Explorer", url: "https://explorer.solana.com" },
1474        is_under_development: false,
1475        is_deprecated: false,
1476    }
1477}
1478
1479// ── API context types ────────────────────────────────────────────────────────
1480
1481/// IPFS configuration for reading and writing app data.
1482#[derive(Debug, Clone, Default)]
1483pub struct IpfsConfig {
1484    /// The URI of the IPFS node.
1485    pub uri: Option<String>,
1486    /// The URI of the IPFS node for writing.
1487    pub write_uri: Option<String>,
1488    /// The URI of the IPFS node for reading.
1489    pub read_uri: Option<String>,
1490    /// Pinata API key.
1491    pub pinata_api_key: Option<String>,
1492    /// Pinata API secret.
1493    pub pinata_api_secret: Option<String>,
1494}
1495
1496/// The `CoW` Protocol API context.
1497///
1498/// Defines the chain, environment, and optional overrides for connecting to the
1499/// `CoW` Protocol API.
1500#[derive(Debug, Clone)]
1501pub struct ApiContext {
1502    /// The target chain ID.
1503    pub chain_id: SupportedChainId,
1504    /// The API environment (`prod` or `staging`).
1505    pub env: super::chain::Env,
1506    /// Optional per-chain base URL overrides.
1507    pub base_urls: Option<ApiBaseUrls>,
1508    /// Optional API key for the partner API.
1509    pub api_key: Option<String>,
1510}
1511
1512impl Default for ApiContext {
1513    /// Returns the default API context (production, mainnet).
1514    fn default() -> Self {
1515        Self {
1516            chain_id: SupportedChainId::Mainnet,
1517            env: super::chain::Env::Prod,
1518            base_urls: None,
1519            api_key: None,
1520        }
1521    }
1522}
1523
1524/// Protocol-level options for overriding `CoW` Protocol contract addresses and
1525/// environment.
1526#[derive(Debug, Clone, Default)]
1527pub struct ProtocolOptions {
1528    /// The API environment.
1529    pub env: Option<super::chain::Env>,
1530    /// Per-chain settlement contract address overrides.
1531    pub settlement_contract_override: Option<AddressPerChain>,
1532    /// Per-chain `EthFlow` contract address overrides.
1533    pub eth_flow_contract_override: Option<AddressPerChain>,
1534}
1535
1536/// An EVM call with a target address, calldata, and value.
1537#[derive(Debug, Clone)]
1538pub struct EvmCall {
1539    /// The target contract address.
1540    pub to: Address,
1541    /// The encoded calldata.
1542    pub data: Vec<u8>,
1543    /// The value to send (in wei).
1544    pub value: U256,
1545}
1546
1547#[cfg(test)]
1548mod tests {
1549    use super::*;
1550
1551    // ── EvmChains ───────────────────────────────────────────────────────
1552
1553    #[test]
1554    fn evm_chains_roundtrip_u64() {
1555        let chains = [
1556            (EvmChains::Mainnet, 1),
1557            (EvmChains::Optimism, 10),
1558            (EvmChains::Bnb, 56),
1559            (EvmChains::GnosisChain, 100),
1560            (EvmChains::Polygon, 137),
1561            (EvmChains::Base, 8_453),
1562            (EvmChains::Plasma, 9_745),
1563            (EvmChains::ArbitrumOne, 42_161),
1564            (EvmChains::Avalanche, 43_114),
1565            (EvmChains::Ink, 57_073),
1566            (EvmChains::Linea, 59_144),
1567            (EvmChains::Sepolia, 11_155_111),
1568        ];
1569        for (chain, id) in chains {
1570            assert_eq!(chain.as_u64(), id);
1571            assert_eq!(EvmChains::try_from_u64(id), Some(chain));
1572        }
1573    }
1574
1575    #[test]
1576    fn evm_chains_unknown_returns_none() {
1577        assert_eq!(EvmChains::try_from_u64(9999), None);
1578    }
1579
1580    // ── NonEvmChains ────────────────────────────────────────────────────
1581
1582    #[test]
1583    fn non_evm_chains_roundtrip() {
1584        assert_eq!(NonEvmChains::Bitcoin.as_u64(), 1_000_000_000);
1585        assert_eq!(NonEvmChains::Solana.as_u64(), 1_000_000_001);
1586        assert_eq!(NonEvmChains::try_from_u64(1_000_000_000), Some(NonEvmChains::Bitcoin));
1587        assert_eq!(NonEvmChains::try_from_u64(1_000_000_001), Some(NonEvmChains::Solana));
1588        assert_eq!(NonEvmChains::try_from_u64(999), None);
1589    }
1590
1591    // ── AdditionalTargetChainId ─────────────────────────────────────────
1592
1593    #[test]
1594    fn additional_target_roundtrip() {
1595        for &chain in AdditionalTargetChainId::all() {
1596            let id = chain.as_u64();
1597            assert_eq!(AdditionalTargetChainId::try_from_u64(id), Some(chain));
1598        }
1599    }
1600
1601    #[test]
1602    fn additional_target_all_has_three() {
1603        assert_eq!(AdditionalTargetChainId::all().len(), 3);
1604    }
1605
1606    // ── TargetChainId ───────────────────────────────────────────────────
1607
1608    #[test]
1609    fn target_chain_id_as_u64() {
1610        let supported = TargetChainId::Supported(SupportedChainId::Mainnet);
1611        assert_eq!(supported.as_u64(), 1);
1612        let additional = TargetChainId::Additional(AdditionalTargetChainId::Bitcoin);
1613        assert_eq!(additional.as_u64(), 1_000_000_000);
1614    }
1615
1616    // ── Classification helpers ──────────────────────────────────────────
1617
1618    #[test]
1619    fn is_evm_chain_correct() {
1620        assert!(is_evm_chain(1));
1621        assert!(is_evm_chain(10));
1622        assert!(!is_evm_chain(1_000_000_000));
1623        assert!(!is_evm_chain(9999));
1624    }
1625
1626    #[test]
1627    fn is_non_evm_chain_correct() {
1628        assert!(is_non_evm_chain(1_000_000_000));
1629        assert!(is_non_evm_chain(1_000_000_001));
1630        assert!(!is_non_evm_chain(1));
1631    }
1632
1633    #[test]
1634    fn is_btc_chain_correct() {
1635        assert!(is_btc_chain(1_000_000_000));
1636        assert!(!is_btc_chain(1));
1637    }
1638
1639    #[test]
1640    fn is_solana_chain_correct() {
1641        assert!(is_solana_chain(1_000_000_001));
1642        assert!(!is_solana_chain(1_000_000_000));
1643        assert!(!is_solana_chain(1));
1644    }
1645
1646    #[test]
1647    fn all_chains_map_covers_every_chain() {
1648        let map = all_chains_map();
1649        assert_eq!(map.len(), all_chains().len());
1650        for id in all_chain_ids() {
1651            assert!(map.contains_key(&id.as_u64()), "missing chain id {}", id.as_u64());
1652        }
1653    }
1654
1655    #[test]
1656    fn is_supported_chain_correct() {
1657        assert!(is_supported_chain(1));
1658        assert!(is_supported_chain(100));
1659        assert!(!is_supported_chain(10));
1660        assert!(!is_supported_chain(9999));
1661    }
1662
1663    #[test]
1664    fn is_additional_target_chain_correct() {
1665        assert!(is_additional_target_chain(10));
1666        assert!(is_additional_target_chain(1_000_000_000));
1667        assert!(!is_additional_target_chain(1));
1668    }
1669
1670    #[test]
1671    fn is_target_chain_id_correct() {
1672        assert!(is_target_chain_id(1));
1673        assert!(is_target_chain_id(10));
1674        assert!(is_target_chain_id(1_000_000_000));
1675        assert!(!is_target_chain_id(9999));
1676    }
1677
1678    #[test]
1679    fn is_zk_sync_chain_is_false_for_all() {
1680        assert!(!is_zk_sync_chain(1));
1681        assert!(!is_zk_sync_chain(100));
1682    }
1683
1684    // ── ChainInfo ───────────────────────────────────────────────────────
1685
1686    #[test]
1687    fn get_chain_info_all_supported() {
1688        for &chain in SupportedChainId::all() {
1689            let chain_info = get_chain_info(chain.as_u64());
1690            assert!(chain_info.is_some(), "no chain info for {chain:?}");
1691            let info = chain_info.unwrap_or_else(|| supported_chain_info(chain));
1692            assert_eq!(info.id(), chain.as_u64());
1693            assert!(!info.label().is_empty());
1694        }
1695    }
1696
1697    #[test]
1698    fn get_chain_info_additional_targets() {
1699        for &chain in AdditionalTargetChainId::all() {
1700            let info = get_chain_info(chain.as_u64());
1701            assert!(info.is_some(), "no chain info for {chain:?}");
1702        }
1703    }
1704
1705    #[test]
1706    fn get_chain_info_unknown_returns_none() {
1707        assert!(get_chain_info(9999).is_none());
1708    }
1709
1710    #[test]
1711    fn chain_info_evm_predicates() {
1712        let info = supported_chain_info(SupportedChainId::Mainnet);
1713        assert!(info.is_evm());
1714        assert!(!info.is_non_evm());
1715        assert!(info.as_evm().is_some());
1716        assert!(info.as_non_evm().is_none());
1717    }
1718
1719    #[test]
1720    fn chain_info_non_evm_predicates() {
1721        let info = additional_target_chain_info(AdditionalTargetChainId::Bitcoin);
1722        assert!(!info.is_evm());
1723        assert!(info.is_non_evm());
1724        assert!(info.as_non_evm().is_some());
1725        // Covers the `NonEvm(_) => None` arm of `as_evm`.
1726        assert!(info.as_evm().is_none());
1727    }
1728
1729    #[test]
1730    fn chain_info_native_currency() {
1731        let info = supported_chain_info(SupportedChainId::Mainnet);
1732        let currency = info.native_currency();
1733        assert_eq!(currency.decimals, 18);
1734    }
1735
1736    #[test]
1737    fn chain_info_native_currency_non_evm() {
1738        // Covers the `NonEvm(info)` arm of `native_currency`.
1739        let info = additional_target_chain_info(AdditionalTargetChainId::Bitcoin);
1740        let currency = info.native_currency();
1741        assert_eq!(currency.symbol, "BTC");
1742    }
1743
1744    #[test]
1745    fn chain_info_is_under_development_evm() {
1746        // Covers the `Evm(info)` arm of `is_under_development` on a known
1747        // stable EVM chain (mainnet is never under development).
1748        let info = supported_chain_info(SupportedChainId::Mainnet);
1749        assert!(!info.is_under_development());
1750    }
1751
1752    // ── Iteration helpers ───────────────────────────────────────────────
1753
1754    #[test]
1755    fn all_supported_chain_ids_matches_all() {
1756        let ids = all_supported_chain_ids();
1757        assert_eq!(ids.len(), SupportedChainId::all().len());
1758    }
1759
1760    #[test]
1761    fn all_supported_chains_matches_all() {
1762        let chains = all_supported_chains();
1763        assert_eq!(chains.len(), SupportedChainId::all().len());
1764    }
1765
1766    #[test]
1767    fn tradable_chains_excludes_deprecated_and_dev() {
1768        let tradable = tradable_supported_chain_ids();
1769        assert!(!tradable.is_empty());
1770        assert!(tradable.len() <= SupportedChainId::all().len());
1771    }
1772
1773    #[test]
1774    fn all_additional_target_chain_ids_has_three() {
1775        assert_eq!(all_additional_target_chain_ids().len(), 3);
1776    }
1777
1778    #[test]
1779    fn all_chains_includes_supported_and_additional() {
1780        let all = all_chains();
1781        assert!(all.len() >= SupportedChainId::all().len());
1782    }
1783
1784    #[test]
1785    fn all_chain_ids_includes_both() {
1786        let ids = all_chain_ids();
1787        assert!(ids.len() >= SupportedChainId::all().len() + AdditionalTargetChainId::all().len());
1788    }
1789
1790    #[test]
1791    fn map_supported_networks_maps_all() {
1792        let mapped = map_supported_networks(|c| c.as_u64());
1793        assert_eq!(mapped.len(), SupportedChainId::all().len());
1794    }
1795
1796    #[test]
1797    fn map_address_to_supported_networks_produces_correct_count() {
1798        let mapped = map_address_to_supported_networks(Address::ZERO);
1799        assert_eq!(mapped.len(), SupportedChainId::all().len());
1800    }
1801
1802    // ── Additional coverage ────────────────────────────────────────────
1803
1804    #[test]
1805    fn is_zk_sync_chain_returns_false_for_non_evm() {
1806        assert!(!is_zk_sync_chain(1_000_000_000));
1807    }
1808
1809    #[test]
1810    fn is_chain_under_development_unknown_chain() {
1811        assert!(!is_chain_under_development(9999));
1812    }
1813
1814    #[test]
1815    fn is_chain_deprecated_unknown_chain() {
1816        assert!(!is_chain_deprecated(9999));
1817    }
1818
1819    #[test]
1820    fn chain_info_is_under_development_non_evm() {
1821        let info = additional_target_chain_info(AdditionalTargetChainId::Bitcoin);
1822        assert!(!info.is_under_development());
1823    }
1824
1825    #[test]
1826    fn chain_info_is_deprecated_non_evm() {
1827        let info = additional_target_chain_info(AdditionalTargetChainId::Bitcoin);
1828        assert!(!info.is_deprecated());
1829    }
1830
1831    #[test]
1832    fn evm_chain_info_type_guards() {
1833        let info = supported_chain_info(SupportedChainId::Mainnet);
1834        assert!(is_evm_chain_info(&info));
1835        assert!(!is_non_evm_chain_info(&info));
1836    }
1837
1838    #[test]
1839    fn non_evm_chain_info_type_guards() {
1840        let info = additional_target_chain_info(AdditionalTargetChainId::Bitcoin);
1841        assert!(is_non_evm_chain_info(&info));
1842        assert!(!is_evm_chain_info(&info));
1843    }
1844
1845    #[test]
1846    fn tradable_supported_chains_returns_chain_infos() {
1847        let chains = tradable_supported_chains();
1848        assert!(!chains.is_empty());
1849        for c in &chains {
1850            assert!(!c.is_deprecated());
1851        }
1852    }
1853
1854    #[test]
1855    fn all_additional_target_chains_returns_infos() {
1856        let chains = all_additional_target_chains();
1857        assert_eq!(chains.len(), 3);
1858    }
1859
1860    #[test]
1861    fn map_all_networks_covers_everything() {
1862        let mapped = map_all_networks(|t| t.as_u64());
1863        assert!(
1864            mapped.len() >= SupportedChainId::all().len() + AdditionalTargetChainId::all().len()
1865        );
1866    }
1867
1868    #[test]
1869    fn chain_info_label_non_evm() {
1870        let info = additional_target_chain_info(AdditionalTargetChainId::Solana);
1871        assert_eq!(info.label(), "Solana");
1872        assert_eq!(info.id(), 1_000_000_001);
1873    }
1874
1875    #[test]
1876    fn additional_target_optimism_is_evm() {
1877        let info = additional_target_chain_info(AdditionalTargetChainId::Optimism);
1878        assert!(info.is_evm());
1879        assert_eq!(info.label(), "Optimism");
1880    }
1881
1882    #[test]
1883    fn api_context_default() {
1884        let ctx = ApiContext::default();
1885        assert_eq!(ctx.chain_id, SupportedChainId::Mainnet);
1886        assert!(ctx.base_urls.is_none());
1887        assert!(ctx.api_key.is_none());
1888    }
1889
1890    #[test]
1891    fn additional_target_try_from_u64_unknown() {
1892        assert!(AdditionalTargetChainId::try_from_u64(999).is_none());
1893    }
1894}