diff --git a/anvil/src/cmd.rs b/anvil/src/cmd.rs index 24e00f8773a2a..bd001e16fd787 100644 --- a/anvil/src/cmd.rs +++ b/anvil/src/cmd.rs @@ -83,9 +83,16 @@ pub struct NodeArgs { #[clap(long, visible_alias = "no-mine", conflicts_with = "block-time")] pub no_mining: bool, - /// The host the server will listen on. - #[clap(long, value_name = "IP_ADDR", env = "ANVIL_IP_ADDR", help_heading = "Server options")] - pub host: Option, + /// The hosts the server will listen on. + #[clap( + long, + value_name = "IP_ADDR", + env = "ANVIL_IP_ADDR", + default_value = "127.0.0.1", + help_heading = "Server options", + value_delimiter = ',' + )] + pub host: Vec, /// How transactions are sorted in the mempool. #[clap(long, default_value = "fees")] @@ -608,6 +615,8 @@ impl FromStr for ForkUrl { #[cfg(test)] mod tests { + use std::{env, net::Ipv4Addr}; + use super::*; #[test] @@ -664,4 +673,35 @@ mod tests { NodeArgs::try_parse_from(["anvil", "--disable-block-gas-limit", "--gas-limit", "100"]); assert!(args.is_err()); } + + #[test] + fn can_parse_host() { + let args = NodeArgs::parse_from(["anvil"]); + assert_eq!(args.host, vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]); + + let args = NodeArgs::parse_from([ + "anvil", "--host", "::1", "--host", "1.1.1.1", "--host", "2.2.2.2", + ]); + assert_eq!( + args.host, + ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::().unwrap()).to_vec() + ); + + let args = NodeArgs::parse_from(["anvil", "--host", "::1,1.1.1.1,2.2.2.2"]); + assert_eq!( + args.host, + ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::().unwrap()).to_vec() + ); + + env::set_var("ANVIL_IP_ADDR", "1.1.1.1"); + let args = NodeArgs::parse_from(["anvil"]); + assert_eq!(args.host, vec!["1.1.1.1".parse::().unwrap()]); + + env::set_var("ANVIL_IP_ADDR", "::1,1.1.1.1,2.2.2.2"); + let args = NodeArgs::parse_from(["anvil"]); + assert_eq!( + args.host, + ["::1", "1.1.1.1", "2.2.2.2"].map(|ip| ip.parse::().unwrap()).to_vec() + ); + } } diff --git a/anvil/src/config.rs b/anvil/src/config.rs index 8f487bb58f2a9..be1c321151e9a 100644 --- a/anvil/src/config.rs +++ b/anvil/src/config.rs @@ -41,7 +41,12 @@ use foundry_evm::{ use parking_lot::RwLock; use serde_json::{json, to_writer, Value}; use std::{ - collections::HashMap, fmt::Write as FmtWrite, fs::File, net::IpAddr, path::PathBuf, sync::Arc, + collections::HashMap, + fmt::Write as FmtWrite, + fs::File, + net::{IpAddr, Ipv4Addr}, + path::PathBuf, + sync::Arc, time::Duration, }; use yansi::Paint; @@ -128,7 +133,7 @@ pub struct NodeConfig { /// How to configure the server pub server_config: ServerConfig, /// The host the server will listen on - pub host: Option, + pub host: Vec, /// How transactions are sorted in the mempool pub transaction_order: TransactionOrder, /// Filename to write anvil output as json @@ -376,7 +381,7 @@ impl Default for NodeConfig { enable_steps_tracing: false, no_storage_caching: false, server_config: Default::default(), - host: None, + host: vec![IpAddr::V4(Ipv4Addr::LOCALHOST)], transaction_order: Default::default(), config_out: None, genesis: None, @@ -707,8 +712,8 @@ impl NodeConfig { /// Sets the host the server will listen on #[must_use] - pub fn with_host(mut self, host: Option) -> Self { - self.host = host; + pub fn with_host(mut self, host: Vec) -> Self { + self.host = if host.is_empty() { vec![IpAddr::V4(Ipv4Addr::LOCALHOST)] } else { host }; self } diff --git a/anvil/src/lib.rs b/anvil/src/lib.rs index 310d138590e3f..636d879fa7488 100644 --- a/anvil/src/lib.rs +++ b/anvil/src/lib.rs @@ -27,7 +27,7 @@ use parking_lot::Mutex; use std::{ future::Future, io, - net::{IpAddr, Ipv4Addr, SocketAddr}, + net::SocketAddr, pin::Pin, sync::Arc, task::{Context, Poll}, @@ -162,15 +162,19 @@ pub async fn spawn(mut config: NodeConfig) -> (EthApi, NodeHandle) { let node_service = tokio::task::spawn(NodeService::new(pool, backend, miner, fee_history_service, filters)); - let host = config.host.unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)); - let mut addr = SocketAddr::new(host, port); + let mut servers = Vec::new(); + let mut addresses = Vec::new(); - // configure the rpc server and use its actual local address - let server = server::serve(addr, api.clone(), server_config); - addr = server.local_addr(); + for addr in config.host.iter() { + let sock_addr = SocketAddr::new(addr.to_owned(), port); + let srv = server::serve(sock_addr, api.clone(), server_config.clone()); - // spawn the server on a new task - let serve = tokio::task::spawn(server.map_err(NodeError::from)); + addresses.push(srv.local_addr()); + + // spawn the server on a new task + let srv = tokio::task::spawn(srv.map_err(NodeError::from)); + servers.push(srv); + } let tokio_handle = Handle::current(); let (signal, on_shutdown) = shutdown::signal(); @@ -181,9 +185,9 @@ pub async fn spawn(mut config: NodeConfig) -> (EthApi, NodeHandle) { let handle = NodeHandle { config, node_service, - server: serve, + servers, ipc_task, - address: addr, + addresses, _signal: Some(signal), task_manager, }; @@ -201,11 +205,11 @@ type IpcTask = JoinHandle>; pub struct NodeHandle { config: NodeConfig, /// The address of the running rpc server - address: SocketAddr, + addresses: Vec, /// Join handle for the Node Service pub node_service: JoinHandle>, - /// Join handle for the Anvil server - pub server: JoinHandle>, + /// Join handles (one per socket) for the Anvil server. + pub servers: Vec>>, // The future that joins the ipc server, if any ipc_task: Option, /// A signal that fires the shutdown, fired on drop. @@ -224,7 +228,14 @@ impl NodeHandle { pub(crate) fn print(&self, fork: Option<&ClientFork>) { self.config.print(fork); if !self.config.silent { - println!("Listening on {}", self.socket_address()) + println!( + "Listening on {}", + self.addresses + .iter() + .map(|addr| { addr.to_string() }) + .collect::>() + .join(", ") + ) } } @@ -233,7 +244,7 @@ impl NodeHandle { /// **N.B.** this may not necessarily be the same `host + port` as configured in the /// `NodeConfig`, if port was set to 0, then the OS auto picks an available port pub fn socket_address(&self) -> &SocketAddr { - &self.address + &self.addresses[0] } /// Returns the http endpoint @@ -352,7 +363,14 @@ impl Future for NodeHandle { return Poll::Ready(res) } - pin.server.poll_unpin(cx) + // poll the axum server handles + for server in pin.servers.iter_mut() { + if let Poll::Ready(res) = server.poll_unpin(cx) { + return Poll::Ready(res) + } + } + + Poll::Pending } }