wp-evm-base 0.1.2

Base layer (math, evm helpers, base types) for waterpump-evm
Documentation
//! Curated `Chain` enum for waterpump-evm facade selection.
//!
//! Lists only the EVM chains where at least one facade has a known
//! `V3ProtocolConfig`. `TryFrom<u64>` enables auto-detection from RPC
//! `eth_chainId` (Slice B). `clap::ValueEnum` derive is gated behind the
//! `clap` feature so the base crate stays lean for library-only consumers.

use alloy_primitives::{address, Address};
use core::fmt;

/// EVM chains supported by waterpump-evm facades.
///
/// Variants are curated to chains where at least one facade has a
/// `V3ProtocolConfig`. Chain IDs match the canonical EIP-155 values.
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Chain {
    /// Ethereum mainnet (chain ID 1).
    #[cfg_attr(feature = "clap", value(name = "ethereum"))]
    Ethereum,
    /// Arbitrum One (chain ID 42161).
    #[cfg_attr(feature = "clap", value(name = "arbitrum"))]
    Arbitrum,
    /// Optimism (chain ID 10).
    #[cfg_attr(feature = "clap", value(name = "optimism"))]
    Optimism,
    /// Polygon PoS (chain ID 137).
    #[cfg_attr(feature = "clap", value(name = "polygon"))]
    Polygon,
    /// Base (chain ID 8453).
    #[cfg_attr(feature = "clap", value(name = "base"))]
    Base,
    /// BNB Smart Chain (chain ID 56).
    #[cfg_attr(feature = "clap", value(name = "bsc"))]
    Bsc,
    /// Sonic (chain ID 146). Home of Shadow Exchange (R14 / R7 Slice C).
    #[cfg_attr(feature = "clap", value(name = "sonic"))]
    Sonic,
    /// Avalanche C-Chain (chain ID 43114).
    #[cfg_attr(feature = "clap", value(name = "avalanche"))]
    Avalanche,
    /// Celo (chain ID 42220).
    #[cfg_attr(feature = "clap", value(name = "celo"))]
    Celo,
    /// HyperEVM mainnet (chain ID 999). Home of Project X — R26 / spec
    /// `docs/superpowers/specs/2026-05-12-r26-hyperevm-prjx-design.md`.
    #[cfg_attr(feature = "clap", value(name = "hyperevm"))]
    HyperEvm,
}

impl Chain {
    /// Exhaustive runtime-iteration array of all supported chains.
    pub const ALL: &'static [Self] = &[
        Self::Ethereum,
        Self::Arbitrum,
        Self::Optimism,
        Self::Polygon,
        Self::Base,
        Self::Bsc,
        Self::Sonic,
        Self::Avalanche,
        Self::Celo,
        Self::HyperEvm,
    ];

    /// Canonical EIP-155 chain ID.
    pub const fn id(self) -> u64 {
        match self {
            Self::Ethereum => 1,
            Self::Optimism => 10,
            Self::Bsc => 56,
            Self::Polygon => 137,
            Self::Sonic => 146,
            Self::HyperEvm => 999,
            Self::Base => 8_453,
            Self::Arbitrum => 42_161,
            Self::Celo => 42_220,
            Self::Avalanche => 43_114,
        }
    }

    /// Human-readable lower-snake-case name (matches CLI value name).
    pub const fn name(self) -> &'static str {
        match self {
            Self::Ethereum => "ethereum",
            Self::Arbitrum => "arbitrum",
            Self::Optimism => "optimism",
            Self::Polygon => "polygon",
            Self::Base => "base",
            Self::Bsc => "bsc",
            Self::Sonic => "sonic",
            Self::Avalanche => "avalanche",
            Self::Celo => "celo",
            Self::HyperEvm => "hyperevm",
        }
    }

    /// Canonical wrapped-native ERC20 for this chain.
    ///
    /// Returns `None` for chains where the native token is already
    /// ERC20-compatible (e.g., Celo's CELO).
    ///
    /// Each `Some(...)` value is source-verified — see commit body for the
    /// cast-call / on-chain-fixture provenance trail. Spec §2 documents
    /// why wrapped-native is a chain property (not a `V3ProtocolConfig`
    /// property): a single facade CONFIG serves multiple chains with
    /// different WETH addresses (Uniswap V3 mainnet CONFIG → 5 chains →
    /// 5 different WETHs).
    pub const fn wrapped_native(self) -> Option<Address> {
        match self {
            // Verified 2026-05-12 via cast call WETH9() on Uniswap V3 SwapRouter (0xE592...1564).
            Self::Ethereum => Some(address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")),
            // Verified 2026-05-12 via cast call WETH9() on Uniswap V3 SwapRouter (0xE592...1564).
            Self::Arbitrum => Some(address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1")),
            // Verified 2026-05-12 via cast call WETH9() on Uniswap V3 SwapRouter (0xE592...1564).
            Self::Optimism => Some(address!("4200000000000000000000000000000000000006")),
            // Verified 2026-05-12 via cast call WETH9() on Uniswap V3 SwapRouter (0xE592...1564).
            // Native is MATIC; wrapped = WMATIC.
            Self::Polygon => Some(address!("0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270")),
            // Verified 2026-05-12 via cast call WETH9() on Uniswap V3 SwapRouter (0x2626...e481).
            Self::Base => Some(address!("4200000000000000000000000000000000000006")),
            // Verified 2026-05-12 via cast call WETH9() on PancakeSwap V3 SwapRouter (0x1b81...eB14).
            // Native is BNB; wrapped = WBNB.
            Self::Bsc => Some(address!("bb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c")),
            // Cited from existing on-chain fixture: wp-evm-ramses-interfaces/src/gauge.rs:131.
            // Native is S; wrapped = wS.
            Self::Sonic => Some(address!("039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38")),
            // Verified 2026-05-12 via cast call WETH9() on Uniswap V3 SwapRouter (0xbb00...78cE).
            // Native is AVAX; wrapped = WAVAX.
            Self::Avalanche => Some(address!("B31f66AA3C1e785363F0875A1B74E27b85FD66c7")),
            // Celo's native CELO is itself ERC20-compatible; no separate wrap.
            Self::Celo => None,
            // Cited from R26 spec §2 V7 (verified 2026-05-12 via Router.WETH9()
            // on https://hyperliquid.drpc.org).
            // Native is HYPE; wrapped = WHYPE.
            Self::HyperEvm => Some(address!("5555555555555555555555555555555555555555")),
        }
    }
}

impl TryFrom<u64> for Chain {
    type Error = u64;

    fn try_from(id: u64) -> Result<Self, u64> {
        Ok(match id {
            1 => Self::Ethereum,
            10 => Self::Optimism,
            56 => Self::Bsc,
            137 => Self::Polygon,
            146 => Self::Sonic,
            999 => Self::HyperEvm,
            8_453 => Self::Base,
            42_161 => Self::Arbitrum,
            42_220 => Self::Celo,
            43_114 => Self::Avalanche,
            other => return Err(other),
        })
    }
}

impl fmt::Display for Chain {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.name())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn id_round_trip() {
        for &chain in Chain::ALL {
            let id = chain.id();
            let back = Chain::try_from(id).expect("known chain id");
            assert_eq!(back, chain, "round-trip failed for {chain}");
        }
    }

    #[test]
    fn avalanche_chain_id_is_43114() {
        assert_eq!(Chain::Avalanche.id(), 43_114);
        assert_eq!(Chain::try_from(43_114).unwrap(), Chain::Avalanche);
    }

    #[test]
    fn sonic_chain_id_is_146() {
        assert_eq!(Chain::Sonic.id(), 146);
        assert_eq!(Chain::try_from(146).unwrap(), Chain::Sonic);
    }

    #[test]
    fn hyperevm_chain_id_is_999() {
        assert_eq!(Chain::HyperEvm.id(), 999);
        assert_eq!(Chain::try_from(999).unwrap(), Chain::HyperEvm);
    }

    #[test]
    fn try_from_unknown_returns_id() {
        let unknown = 999_999u64;
        assert_eq!(Chain::try_from(unknown), Err(unknown));
    }

    #[test]
    fn name_is_lowercase() {
        assert_eq!(Chain::Ethereum.name(), "ethereum");
        assert_eq!(Chain::Bsc.name(), "bsc");
        assert_eq!(Chain::Arbitrum.name(), "arbitrum");
    }

    #[test]
    fn display_matches_name() {
        assert_eq!(format!("{}", Chain::Ethereum), "ethereum");
        assert_eq!(format!("{}", Chain::Celo), "celo");
    }

    #[test]
    fn wrapped_native_round_trip() {
        // Exhaustive over Chain::ALL — every variant has an explicit
        // wrapped_native() result. New Chain variants will compile-error
        // until they're added to wrapped_native() (desired forcing function
        // per spec §2 forward-compat note).
        for chain in Chain::ALL {
            let _ = chain.wrapped_native(); // exercises the match arm
        }
    }

    #[test]
    fn wrapped_native_celo_is_none() {
        assert_eq!(Chain::Celo.wrapped_native(), None);
    }

    #[test]
    fn wrapped_native_hyperevm_is_whype() {
        assert_eq!(
            Chain::HyperEvm.wrapped_native(),
            Some(address!("5555555555555555555555555555555555555555"))
        );
    }

    #[test]
    fn wrapped_native_distinct_per_evm_chain() {
        // Anti-test: WETH addresses differ across chains.
        // Locks against accidental copy-paste regression (e.g., Polygon
        // accidentally returning Ethereum WETH).
        assert_ne!(Chain::Ethereum.wrapped_native(), Chain::Polygon.wrapped_native(),);
        assert_ne!(Chain::Ethereum.wrapped_native(), Chain::Avalanche.wrapped_native(),);
        assert_ne!(Chain::Polygon.wrapped_native(), Chain::Bsc.wrapped_native(),);
    }
}