Skip to main content

wp_evm_base/
chain.rs

1//! Curated `Chain` enum for waterpump-evm facade selection.
2//!
3//! Lists only the EVM chains where at least one facade has a known
4//! `V3ProtocolConfig`. `TryFrom<u64>` enables auto-detection from RPC
5//! `eth_chainId` (Slice B). `clap::ValueEnum` derive is gated behind the
6//! `clap` feature so the base crate stays lean for library-only consumers.
7
8use alloy_primitives::{address, Address};
9use core::fmt;
10
11/// EVM chains supported by waterpump-evm facades.
12///
13/// Variants are curated to chains where at least one facade has a
14/// `V3ProtocolConfig`. Chain IDs match the canonical EIP-155 values.
15#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
16#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
17pub enum Chain {
18    /// Ethereum mainnet (chain ID 1).
19    #[cfg_attr(feature = "clap", value(name = "ethereum"))]
20    Ethereum,
21    /// Arbitrum One (chain ID 42161).
22    #[cfg_attr(feature = "clap", value(name = "arbitrum"))]
23    Arbitrum,
24    /// Optimism (chain ID 10).
25    #[cfg_attr(feature = "clap", value(name = "optimism"))]
26    Optimism,
27    /// Polygon PoS (chain ID 137).
28    #[cfg_attr(feature = "clap", value(name = "polygon"))]
29    Polygon,
30    /// Base (chain ID 8453).
31    #[cfg_attr(feature = "clap", value(name = "base"))]
32    Base,
33    /// BNB Smart Chain (chain ID 56).
34    #[cfg_attr(feature = "clap", value(name = "bsc"))]
35    Bsc,
36    /// Sonic (chain ID 146). Home of Shadow Exchange (R14 / R7 Slice C).
37    #[cfg_attr(feature = "clap", value(name = "sonic"))]
38    Sonic,
39    /// Avalanche C-Chain (chain ID 43114).
40    #[cfg_attr(feature = "clap", value(name = "avalanche"))]
41    Avalanche,
42    /// Celo (chain ID 42220).
43    #[cfg_attr(feature = "clap", value(name = "celo"))]
44    Celo,
45    /// HyperEVM mainnet (chain ID 999). Home of Project X — R26 / spec
46    /// `docs/superpowers/specs/2026-05-12-r26-hyperevm-prjx-design.md`.
47    #[cfg_attr(feature = "clap", value(name = "hyperevm"))]
48    HyperEvm,
49}
50
51impl Chain {
52    /// Exhaustive runtime-iteration array of all supported chains.
53    pub const ALL: &'static [Self] = &[
54        Self::Ethereum,
55        Self::Arbitrum,
56        Self::Optimism,
57        Self::Polygon,
58        Self::Base,
59        Self::Bsc,
60        Self::Sonic,
61        Self::Avalanche,
62        Self::Celo,
63        Self::HyperEvm,
64    ];
65
66    /// Canonical EIP-155 chain ID.
67    pub const fn id(self) -> u64 {
68        match self {
69            Self::Ethereum => 1,
70            Self::Optimism => 10,
71            Self::Bsc => 56,
72            Self::Polygon => 137,
73            Self::Sonic => 146,
74            Self::HyperEvm => 999,
75            Self::Base => 8_453,
76            Self::Arbitrum => 42_161,
77            Self::Celo => 42_220,
78            Self::Avalanche => 43_114,
79        }
80    }
81
82    /// Human-readable lower-snake-case name (matches CLI value name).
83    pub const fn name(self) -> &'static str {
84        match self {
85            Self::Ethereum => "ethereum",
86            Self::Arbitrum => "arbitrum",
87            Self::Optimism => "optimism",
88            Self::Polygon => "polygon",
89            Self::Base => "base",
90            Self::Bsc => "bsc",
91            Self::Sonic => "sonic",
92            Self::Avalanche => "avalanche",
93            Self::Celo => "celo",
94            Self::HyperEvm => "hyperevm",
95        }
96    }
97
98    /// Canonical wrapped-native ERC20 for this chain.
99    ///
100    /// Returns `None` for chains where the native token is already
101    /// ERC20-compatible (e.g., Celo's CELO).
102    ///
103    /// Each `Some(...)` value is source-verified — see commit body for the
104    /// cast-call / on-chain-fixture provenance trail. Spec §2 documents
105    /// why wrapped-native is a chain property (not a `V3ProtocolConfig`
106    /// property): a single facade CONFIG serves multiple chains with
107    /// different WETH addresses (Uniswap V3 mainnet CONFIG → 5 chains →
108    /// 5 different WETHs).
109    pub const fn wrapped_native(self) -> Option<Address> {
110        match self {
111            // Verified 2026-05-12 via cast call WETH9() on Uniswap V3 SwapRouter (0xE592...1564).
112            Self::Ethereum => Some(address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")),
113            // Verified 2026-05-12 via cast call WETH9() on Uniswap V3 SwapRouter (0xE592...1564).
114            Self::Arbitrum => Some(address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1")),
115            // Verified 2026-05-12 via cast call WETH9() on Uniswap V3 SwapRouter (0xE592...1564).
116            Self::Optimism => Some(address!("4200000000000000000000000000000000000006")),
117            // Verified 2026-05-12 via cast call WETH9() on Uniswap V3 SwapRouter (0xE592...1564).
118            // Native is MATIC; wrapped = WMATIC.
119            Self::Polygon => Some(address!("0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270")),
120            // Verified 2026-05-12 via cast call WETH9() on Uniswap V3 SwapRouter (0x2626...e481).
121            Self::Base => Some(address!("4200000000000000000000000000000000000006")),
122            // Verified 2026-05-12 via cast call WETH9() on PancakeSwap V3 SwapRouter (0x1b81...eB14).
123            // Native is BNB; wrapped = WBNB.
124            Self::Bsc => Some(address!("bb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c")),
125            // Cited from existing on-chain fixture: wp-evm-ramses-interfaces/src/gauge.rs:131.
126            // Native is S; wrapped = wS.
127            Self::Sonic => Some(address!("039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38")),
128            // Verified 2026-05-12 via cast call WETH9() on Uniswap V3 SwapRouter (0xbb00...78cE).
129            // Native is AVAX; wrapped = WAVAX.
130            Self::Avalanche => Some(address!("B31f66AA3C1e785363F0875A1B74E27b85FD66c7")),
131            // Celo's native CELO is itself ERC20-compatible; no separate wrap.
132            Self::Celo => None,
133            // Cited from R26 spec §2 V7 (verified 2026-05-12 via Router.WETH9()
134            // on https://hyperliquid.drpc.org).
135            // Native is HYPE; wrapped = WHYPE.
136            Self::HyperEvm => Some(address!("5555555555555555555555555555555555555555")),
137        }
138    }
139}
140
141impl TryFrom<u64> for Chain {
142    type Error = u64;
143
144    fn try_from(id: u64) -> Result<Self, u64> {
145        Ok(match id {
146            1 => Self::Ethereum,
147            10 => Self::Optimism,
148            56 => Self::Bsc,
149            137 => Self::Polygon,
150            146 => Self::Sonic,
151            999 => Self::HyperEvm,
152            8_453 => Self::Base,
153            42_161 => Self::Arbitrum,
154            42_220 => Self::Celo,
155            43_114 => Self::Avalanche,
156            other => return Err(other),
157        })
158    }
159}
160
161impl fmt::Display for Chain {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        f.write_str(self.name())
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn id_round_trip() {
173        for &chain in Chain::ALL {
174            let id = chain.id();
175            let back = Chain::try_from(id).expect("known chain id");
176            assert_eq!(back, chain, "round-trip failed for {chain}");
177        }
178    }
179
180    #[test]
181    fn avalanche_chain_id_is_43114() {
182        assert_eq!(Chain::Avalanche.id(), 43_114);
183        assert_eq!(Chain::try_from(43_114).unwrap(), Chain::Avalanche);
184    }
185
186    #[test]
187    fn sonic_chain_id_is_146() {
188        assert_eq!(Chain::Sonic.id(), 146);
189        assert_eq!(Chain::try_from(146).unwrap(), Chain::Sonic);
190    }
191
192    #[test]
193    fn hyperevm_chain_id_is_999() {
194        assert_eq!(Chain::HyperEvm.id(), 999);
195        assert_eq!(Chain::try_from(999).unwrap(), Chain::HyperEvm);
196    }
197
198    #[test]
199    fn try_from_unknown_returns_id() {
200        let unknown = 999_999u64;
201        assert_eq!(Chain::try_from(unknown), Err(unknown));
202    }
203
204    #[test]
205    fn name_is_lowercase() {
206        assert_eq!(Chain::Ethereum.name(), "ethereum");
207        assert_eq!(Chain::Bsc.name(), "bsc");
208        assert_eq!(Chain::Arbitrum.name(), "arbitrum");
209    }
210
211    #[test]
212    fn display_matches_name() {
213        assert_eq!(format!("{}", Chain::Ethereum), "ethereum");
214        assert_eq!(format!("{}", Chain::Celo), "celo");
215    }
216
217    #[test]
218    fn wrapped_native_round_trip() {
219        // Exhaustive over Chain::ALL — every variant has an explicit
220        // wrapped_native() result. New Chain variants will compile-error
221        // until they're added to wrapped_native() (desired forcing function
222        // per spec §2 forward-compat note).
223        for chain in Chain::ALL {
224            let _ = chain.wrapped_native(); // exercises the match arm
225        }
226    }
227
228    #[test]
229    fn wrapped_native_celo_is_none() {
230        assert_eq!(Chain::Celo.wrapped_native(), None);
231    }
232
233    #[test]
234    fn wrapped_native_hyperevm_is_whype() {
235        assert_eq!(
236            Chain::HyperEvm.wrapped_native(),
237            Some(address!("5555555555555555555555555555555555555555"))
238        );
239    }
240
241    #[test]
242    fn wrapped_native_distinct_per_evm_chain() {
243        // Anti-test: WETH addresses differ across chains.
244        // Locks against accidental copy-paste regression (e.g., Polygon
245        // accidentally returning Ethereum WETH).
246        assert_ne!(Chain::Ethereum.wrapped_native(), Chain::Polygon.wrapped_native(),);
247        assert_ne!(Chain::Ethereum.wrapped_native(), Chain::Avalanche.wrapped_native(),);
248        assert_ne!(Chain::Polygon.wrapped_native(), Chain::Bsc.wrapped_native(),);
249    }
250}