newton-core 0.4.16

newton protocol core sdk
//! Multi-chain configuration model
//!
//! Defines `MultiChainConfig<T>` that loads per-chain contract addresses and RPC configs.
//! Multi-chain mode is activated via `--network` CLI flag (preferred) or `--chain-id` (single chain).

use std::collections::HashMap;

use tracing::info;

use super::{
    contracts::ContractsConfig, data_provider::DataProviderConfig, ipfs::IpfsConfig, loader::ConfigLoader,
    rpc::ChainRpcProviderConfig, NewtonAvsConfig,
};

/// Per-chain context holding contract addresses and chain metadata.
///
/// Each chain the gateway or operator serves gets one `ChainContext`
/// with its own contract addresses and role designation.
#[derive(Debug, Clone)]
pub struct ChainContext {
    /// chain id
    pub chain_id: u64,
    /// on-chain contract addresses for this chain
    pub contracts: ContractsConfig,
    /// whether this chain is a destination chain (L2)
    /// If false, this is the source chain (EigenLayer registration)
    pub is_destination_chain: bool,
}

/// Multi-chain configuration for services that span N chains.
///
/// Wraps the service-specific config `T` with per-chain contract addresses
/// and shared infrastructure config (RPC, IPFS, data provider).
///
/// # Backward Compatibility
///
/// Convert from `NewtonAvsConfig<T>` via `From` for single-chain deployments.
/// The single chain becomes the only entry in `chains`.
#[derive(Debug, Clone)]
pub struct MultiChainConfig<T: ConfigLoader> {
    /// deployment environment name (e.g. "local", "stage", "prod")
    pub env: String,
    /// source chain id where EigenLayer contracts live
    pub source_chain_id: u64,
    /// per-chain contexts keyed by chain id
    pub chains: HashMap<u64, ChainContext>,
    /// RPC provider configurations for all supported chains
    pub rpc: ChainRpcProviderConfig,
    /// service-specific configuration (e.g. GatewayConfig, OperatorConfig)
    pub service: T,
    /// IPFS configuration (shared across chains)
    pub ipfs: IpfsConfig,
    /// data provider configuration (shared across chains)
    pub data_provider: DataProviderConfig,
}

impl<T: ConfigLoader + PartialEq + Eq + Clone> From<super::NewtonAvsConfig<T>> for MultiChainConfig<T> {
    /// Convert a single-chain `NewtonAvsConfig` into a `MultiChainConfig`.
    ///
    /// The primary chain becomes the only entry in `chains`.
    /// If `source_chain_id` differs from `chain_id`, it is treated as a
    /// destination-chain deployment and marked accordingly.
    fn from(config: super::NewtonAvsConfig<T>) -> Self {
        let source_chain_id = config.source_chain_id.unwrap_or(config.chain_id);
        let is_destination = source_chain_id != config.chain_id;

        let mut chains = HashMap::new();
        chains.insert(
            config.chain_id,
            ChainContext {
                chain_id: config.chain_id,
                contracts: config.contracts,
                is_destination_chain: is_destination,
            },
        );

        Self {
            env: config.env,
            source_chain_id,
            chains,
            rpc: config.rpc,
            service: config.service,
            ipfs: config.ipfs,
            data_provider: config.data_provider,
        }
    }
}

impl<T: ConfigLoader> MultiChainConfig<T> {
    /// Get the chain context for a specific chain id.
    pub fn get_chain(&self, chain_id: u64) -> Option<&ChainContext> {
        self.chains.get(&chain_id)
    }

    /// Get all chain ids.
    pub fn chain_ids(&self) -> Vec<u64> {
        self.chains.keys().copied().collect()
    }

    /// Get the number of chains configured.
    pub fn chain_count(&self) -> usize {
        self.chains.len()
    }

    /// Check if this is a multi-chain configuration (more than one chain).
    pub fn is_multi_chain(&self) -> bool {
        self.chains.len() > 1
    }

    /// Add a chain to the multi-chain configuration.
    ///
    /// Loads the chain's contract addresses from deployment files using the
    /// same logic as `NewtonAvsConfig::load` — destination chains load source
    /// contracts first, then overlay destination-specific addresses.
    pub fn add_chain(&mut self, chain_id: u64) -> eyre::Result<()> {
        use crate::common::chain;

        info!(chain_id, "adding chain to multi-chain config");

        let is_destination = chain::is_destination_chain(chain_id);
        let contracts = if is_destination {
            let src_id = chain::get_source_chain_id(chain_id)
                .ok_or_else(|| eyre::eyre!("no source chain mapping for destination chain {}", chain_id))?;
            ContractsConfig::load(src_id, self.env.clone())?.with_destination_chain_id(chain_id, &self.env)?
        } else {
            ContractsConfig::load(chain_id, self.env.clone())?
        };

        self.chains.insert(
            chain_id,
            ChainContext {
                chain_id,
                contracts,
                is_destination_chain: is_destination,
            },
        );

        Ok(())
    }
}

impl<T: ConfigLoader + PartialEq + Eq + Clone> MultiChainConfig<T> {
    /// Build a complete multi-chain config from a `NetworkMode`.
    ///
    /// Loads the source chain as the primary config, then adds all destination
    /// chains for the network. Reuses `NewtonAvsConfigBuilder` and `add_chain()`
    /// so all contract address resolution follows the standard path.
    pub fn from_network(
        network: crate::common::NetworkMode,
        service_config_path: Option<&std::path::Path>,
        data_provider_path: Option<&std::path::Path>,
    ) -> eyre::Result<Self> {
        let source_chain_id = network.source_chain_id();

        info!(
            network = %network,
            source_chain_id,
            "building multi-chain config from network mode"
        );

        let mut builder = super::NewtonAvsConfigBuilder::new(source_chain_id);
        if let Some(p) = service_config_path {
            builder = builder.with_service_path(p.to_path_buf());
        }
        if let Some(p) = data_provider_path {
            builder = builder.with_data_provider_path(p.to_path_buf());
        }
        let primary_config = builder.build::<T>()?;
        let mut multi: MultiChainConfig<T> = primary_config.into();

        for dest_id in network.destination_chain_ids() {
            multi.add_chain(dest_id)?;
        }

        info!(
            network = %network,
            chains = ?multi.chain_ids(),
            "multi-chain config loaded for {} chain(s)",
            multi.chain_count()
        );

        Ok(multi)
    }

    /// Extract a `NewtonAvsConfig` for a specific chain.
    ///
    /// Used by services that still accept `NewtonAvsConfig` rather than
    /// `MultiChainConfig` directly (e.g., `init_additional_chain` builds
    /// per-chain aggregator configs from this).
    pub fn to_single_chain_config(&self, chain_id: u64) -> Option<NewtonAvsConfig<T>> {
        let ctx = self.chains.get(&chain_id)?;
        Some(NewtonAvsConfig {
            env: self.env.clone(),
            chain_id,
            source_chain_id: if ctx.is_destination_chain {
                Some(self.source_chain_id)
            } else {
                None
            },
            rpc: self.rpc.clone(),
            ipfs: self.ipfs.clone(),
            contracts: ctx.contracts.clone(),
            service: self.service.clone(),
            data_provider: self.data_provider.clone(),
        })
    }
}