newton-core 0.4.16

newton protocol core sdk
//! Network mode for Newton Prover AVS.
//!
//! Maps high-level network names (`local`, `testnet`, `mainnet`) to their
//! complete chain topologies (source + destination chains), eliminating the
//! need to pass explicit chain ID lists on the CLI.

use std::{fmt, str::FromStr};

use super::chain::{
    is_local, is_mainnet, is_source_chain, is_testnet, BASE_MAINNET, BASE_SEPOLIA, ETHEREUM_MAINNET, LOCAL_ANVIL,
    LOCAL_ANVIL_DESTINATION, SEPOLIA,
};

/// High-level network selector for Newton Prover AVS deployments.
///
/// Each variant maps to a fixed chain topology: one source chain (where
/// EigenLayer contracts live) and zero or more destination chains (L2s).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "config", derive(clap::ValueEnum))]
#[serde(rename_all = "lowercase")]
pub enum NetworkMode {
    /// Local development: dual-anvil (31337 source, 31338 destination)
    Local,
    /// Testnet: Sepolia source, Base Sepolia destination
    Testnet,
    /// Mainnet: Ethereum source, Base destination
    Mainnet,
}

impl NetworkMode {
    /// Returns the source chain ID for this network (where EigenLayer is deployed).
    pub const fn source_chain_id(&self) -> u64 {
        match self {
            Self::Local => LOCAL_ANVIL,
            Self::Testnet => SEPOLIA,
            Self::Mainnet => ETHEREUM_MAINNET,
        }
    }

    /// Returns all destination chain IDs for this network.
    pub fn destination_chain_ids(&self) -> Vec<u64> {
        match self {
            Self::Local => vec![LOCAL_ANVIL_DESTINATION],
            Self::Testnet => vec![BASE_SEPOLIA],
            Self::Mainnet => vec![BASE_MAINNET],
        }
    }

    /// Returns source + all destination chain IDs for this network.
    pub fn all_chain_ids(&self) -> Vec<u64> {
        let mut ids = vec![self.source_chain_id()];
        ids.extend(self.destination_chain_ids());
        ids
    }

    /// Returns the deployment environment name used for config/contract loading.
    pub const fn deployment_env(&self) -> &'static str {
        match self {
            Self::Local | Self::Testnet => "stagef",
            Self::Mainnet => "prod",
        }
    }

    /// Infer the network mode from any supported chain ID.
    ///
    /// Returns `None` for unsupported chain IDs.
    pub const fn from_chain_id(chain_id: u64) -> Option<Self> {
        if is_local(chain_id) {
            Some(Self::Local)
        } else if is_testnet(chain_id) {
            Some(Self::Testnet)
        } else if is_mainnet(chain_id) {
            Some(Self::Mainnet)
        } else {
            None
        }
    }

    /// Returns true if this network's source chain matches the given chain ID.
    pub const fn is_source(&self, chain_id: u64) -> bool {
        self.source_chain_id() == chain_id
    }
}

impl fmt::Display for NetworkMode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Local => write!(f, "local"),
            Self::Testnet => write!(f, "testnet"),
            Self::Mainnet => write!(f, "mainnet"),
        }
    }
}

impl FromStr for NetworkMode {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "local" => Ok(Self::Local),
            "testnet" => Ok(Self::Testnet),
            "mainnet" => Ok(Self::Mainnet),
            _ => Err(format!(
                "invalid network mode '{}': expected 'local', 'testnet', or 'mainnet'",
                s
            )),
        }
    }
}

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

    #[test]
    fn source_chain_ids_are_correct() {
        assert_eq!(NetworkMode::Local.source_chain_id(), 31337);
        assert_eq!(NetworkMode::Testnet.source_chain_id(), 11155111);
        assert_eq!(NetworkMode::Mainnet.source_chain_id(), 1);
    }

    #[test]
    fn destination_chain_ids_are_correct() {
        assert_eq!(NetworkMode::Local.destination_chain_ids(), vec![31338]);
        assert_eq!(NetworkMode::Testnet.destination_chain_ids(), vec![84532]);
        assert_eq!(NetworkMode::Mainnet.destination_chain_ids(), vec![8453]);
    }

    #[test]
    fn all_chain_ids_includes_source_and_destinations() {
        let local = NetworkMode::Local.all_chain_ids();
        assert_eq!(local, vec![31337, 31338]);

        let testnet = NetworkMode::Testnet.all_chain_ids();
        assert_eq!(testnet, vec![11155111, 84532]);

        let mainnet = NetworkMode::Mainnet.all_chain_ids();
        assert_eq!(mainnet, vec![1, 8453]);
    }

    #[test]
    fn from_chain_id_round_trip() {
        // Source chains
        assert_eq!(NetworkMode::from_chain_id(31337), Some(NetworkMode::Local));
        assert_eq!(NetworkMode::from_chain_id(11155111), Some(NetworkMode::Testnet));
        assert_eq!(NetworkMode::from_chain_id(1), Some(NetworkMode::Mainnet));

        // Destination chains
        assert_eq!(NetworkMode::from_chain_id(31338), Some(NetworkMode::Local));
        assert_eq!(NetworkMode::from_chain_id(84532), Some(NetworkMode::Testnet));
        assert_eq!(NetworkMode::from_chain_id(8453), Some(NetworkMode::Mainnet));

        // Other supported chains still resolve to correct network
        assert_eq!(NetworkMode::from_chain_id(11155420), Some(NetworkMode::Testnet));
        assert_eq!(NetworkMode::from_chain_id(421614), Some(NetworkMode::Testnet));
        assert_eq!(NetworkMode::from_chain_id(10), Some(NetworkMode::Mainnet));
        assert_eq!(NetworkMode::from_chain_id(42161), Some(NetworkMode::Mainnet));

        // Unsupported
        assert_eq!(NetworkMode::from_chain_id(999999), None);
    }

    #[test]
    fn deployment_env_mapping() {
        assert_eq!(NetworkMode::Local.deployment_env(), "stagef");
        assert_eq!(NetworkMode::Testnet.deployment_env(), "stagef");
        assert_eq!(NetworkMode::Mainnet.deployment_env(), "prod");
    }

    #[test]
    fn from_str_parsing() {
        assert_eq!("local".parse::<NetworkMode>(), Ok(NetworkMode::Local));
        assert_eq!("testnet".parse::<NetworkMode>(), Ok(NetworkMode::Testnet));
        assert_eq!("mainnet".parse::<NetworkMode>(), Ok(NetworkMode::Mainnet));
        assert_eq!("LOCAL".parse::<NetworkMode>(), Ok(NetworkMode::Local));
        assert!("invalid".parse::<NetworkMode>().is_err());
    }

    #[test]
    fn display_format() {
        assert_eq!(NetworkMode::Local.to_string(), "local");
        assert_eq!(NetworkMode::Testnet.to_string(), "testnet");
        assert_eq!(NetworkMode::Mainnet.to_string(), "mainnet");
    }

    #[test]
    fn source_chains_are_source() {
        for network in [NetworkMode::Local, NetworkMode::Testnet, NetworkMode::Mainnet] {
            assert!(
                is_source_chain(network.source_chain_id()),
                "{} source chain {} should be classified as source",
                network,
                network.source_chain_id()
            );
        }
    }
}