Skip to main content

alloy_node_bindings/nodes/
geth.rs

1//! Utilities for launching a Geth dev-mode instance.
2
3use crate::{
4    utils::{extract_endpoint, extract_value, unused_port, GracefulShutdown},
5    NodeError, NODE_DIAL_LOOP_TIMEOUT, NODE_STARTUP_TIMEOUT,
6};
7use alloy_genesis::{CliqueConfig, Genesis};
8use alloy_primitives::Address;
9use k256::ecdsa::SigningKey;
10use std::{
11    ffi::OsString,
12    fs::{create_dir_all, File},
13    io::{BufRead, BufReader},
14    path::PathBuf,
15    process::{Child, ChildStderr, Command, Stdio},
16    time::{Duration, Instant},
17};
18use tempfile::tempdir;
19use url::Url;
20
21/// The exposed APIs
22const API: &str = "eth,net,web3,txpool,admin,personal,miner,debug";
23
24/// The geth command
25const GETH: &str = "geth";
26
27/// Whether or not node is in `dev` mode and configuration options that depend on the mode.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum NodeMode {
30    /// Options that can be set in dev mode
31    Dev(DevOptions),
32    /// Options that cannot be set in dev mode
33    NonDev(PrivateNetOptions),
34}
35
36impl Default for NodeMode {
37    fn default() -> Self {
38        Self::Dev(Default::default())
39    }
40}
41
42/// Configuration options that can be set in dev mode.
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44pub struct DevOptions {
45    /// The interval at which the dev chain will mine new blocks.
46    pub block_time: Option<u64>,
47}
48
49/// Configuration options that cannot be set in dev mode.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct PrivateNetOptions {
52    /// The p2p port to use.
53    pub p2p_port: Option<u16>,
54
55    /// Whether or not peer discovery is enabled.
56    pub discovery: bool,
57}
58
59impl Default for PrivateNetOptions {
60    fn default() -> Self {
61        Self { p2p_port: None, discovery: true }
62    }
63}
64
65/// A geth instance. Will close the instance when dropped.
66///
67/// Construct this using [`Geth`].
68#[derive(Debug)]
69pub struct GethInstance {
70    pid: Child,
71    host: String,
72    port: u16,
73    p2p_port: Option<u16>,
74    auth_port: Option<u16>,
75    ipc: Option<PathBuf>,
76    data_dir: Option<PathBuf>,
77    genesis: Option<Genesis>,
78    clique_private_key: Option<SigningKey>,
79}
80
81impl GethInstance {
82    /// Returns the host of this instance
83    pub fn host(&self) -> &str {
84        &self.host
85    }
86
87    /// Returns the port of this instance
88    pub const fn port(&self) -> u16 {
89        self.port
90    }
91
92    /// Returns the p2p port of this instance
93    pub const fn p2p_port(&self) -> Option<u16> {
94        self.p2p_port
95    }
96
97    /// Returns the auth port of this instance
98    pub const fn auth_port(&self) -> Option<u16> {
99        self.auth_port
100    }
101
102    /// Returns the HTTP endpoint of this instance
103    #[doc(alias = "http_endpoint")]
104    pub fn endpoint(&self) -> String {
105        format!("http://{}:{}", self.host, self.port)
106    }
107
108    /// Returns the Websocket endpoint of this instance
109    pub fn ws_endpoint(&self) -> String {
110        format!("ws://{}:{}", self.host, self.port)
111    }
112
113    /// Returns the IPC endpoint of this instance
114    pub fn ipc_endpoint(&self) -> String {
115        self.ipc.clone().map_or_else(|| "geth.ipc".to_string(), |ipc| ipc.display().to_string())
116    }
117
118    /// Returns the HTTP endpoint url of this instance
119    #[doc(alias = "http_endpoint_url")]
120    pub fn endpoint_url(&self) -> Url {
121        Url::parse(&self.endpoint()).unwrap()
122    }
123
124    /// Returns the Websocket endpoint url of this instance
125    pub fn ws_endpoint_url(&self) -> Url {
126        Url::parse(&self.ws_endpoint()).unwrap()
127    }
128
129    /// Returns the path to this instances' data directory
130    pub const fn data_dir(&self) -> Option<&PathBuf> {
131        self.data_dir.as_ref()
132    }
133
134    /// Returns the genesis configuration used to configure this instance
135    pub const fn genesis(&self) -> Option<&Genesis> {
136        self.genesis.as_ref()
137    }
138
139    /// Returns the private key used to configure clique on this instance
140    #[deprecated = "clique support was removed in geth >=1.14"]
141    pub const fn clique_private_key(&self) -> Option<&SigningKey> {
142        self.clique_private_key.as_ref()
143    }
144
145    /// Takes the stderr contained in the child process.
146    ///
147    /// Stderr is available only when [`Geth::keep_stderr`] was set. This leaves `None` in its
148    /// place, so later calls that require stderr return [`NodeError::NoStderr`].
149    pub fn stderr(&mut self) -> Result<ChildStderr, NodeError> {
150        self.pid.stderr.take().ok_or(NodeError::NoStderr)
151    }
152
153    /// Blocks until geth adds the specified peer, using a 20-second deadline checked between
154    /// complete stderr lines. A live process that emits no newline can block past the deadline.
155    ///
156    /// Requires [`Geth::keep_stderr`] to have been set and [`Self::stderr`] not to have taken the
157    /// handle.
158    pub fn wait_to_add_peer(&mut self, id: &str) -> Result<(), NodeError> {
159        let mut stderr = self.pid.stderr.as_mut().ok_or(NodeError::NoStderr)?;
160        let mut err_reader = BufReader::new(&mut stderr);
161        let mut line = String::new();
162        let start = Instant::now();
163
164        while start.elapsed() < NODE_DIAL_LOOP_TIMEOUT {
165            line.clear();
166            err_reader.read_line(&mut line).map_err(NodeError::ReadLineError)?;
167
168            // geth ids are truncated
169            let truncated_id = if id.len() > 16 { &id[..16] } else { id };
170            if line.contains("Adding p2p peer") && line.contains(truncated_id) {
171                return Ok(());
172            }
173        }
174        Err(NodeError::Timeout)
175    }
176}
177
178impl Drop for GethInstance {
179    fn drop(&mut self) {
180        GracefulShutdown::shutdown(&mut self.pid, 10, "geth");
181    }
182}
183
184/// Builder for launching `geth`.
185///
186/// The default mode is an isolated development chain. [`Geth::p2p_port`] and
187/// [`Geth::disable_discovery`] switch to non-dev mode; [`Geth::dev`] and [`Geth::block_time`]
188/// switch back to dev mode. [`Geth::spawn`] panics on any startup failure; use
189/// [`Geth::try_spawn`] to handle errors.
190///
191/// # Example
192///
193/// ```no_run
194/// use alloy_node_bindings::Geth;
195///
196/// # fn main() -> Result<(), alloy_node_bindings::NodeError> {
197/// let geth = Geth::new().block_time(1).try_spawn()?;
198/// println!("Geth is listening at {}", geth.endpoint());
199///
200/// drop(geth); // this will kill the instance
201/// # Ok(())
202/// # }
203/// ```
204#[derive(Clone, Debug, Default)]
205#[must_use = "This Builder struct does nothing unless it is `spawn`ed"]
206pub struct Geth {
207    program: Option<PathBuf>,
208    host: Option<String>,
209    port: Option<u16>,
210    authrpc_port: Option<u16>,
211    ipc_path: Option<PathBuf>,
212    ipc_enabled: bool,
213    data_dir: Option<PathBuf>,
214    chain_id: Option<u64>,
215    insecure_unlock: bool,
216    keep_err: bool,
217    timeout: Option<u64>,
218    genesis: Option<Genesis>,
219    mode: NodeMode,
220    clique_private_key: Option<SigningKey>,
221    args: Vec<OsString>,
222}
223
224impl Geth {
225    /// Creates a Geth builder in dev mode.
226    pub fn new() -> Self {
227        Self::default()
228    }
229
230    /// Creates a Geth builder which will execute `geth` at the given path.
231    ///
232    /// # Example
233    ///
234    /// ```
235    /// use alloy_node_bindings::Geth;
236    /// # fn a() {
237    /// let geth = Geth::at("../go-ethereum/build/bin/geth").spawn();
238    ///
239    /// println!("Geth running at `{}`", geth.endpoint());
240    /// # }
241    /// ```
242    pub fn at(path: impl Into<PathBuf>) -> Self {
243        Self::new().path(path)
244    }
245
246    /// Sets the `path` to the `geth` executable
247    ///
248    /// By default, it's expected that `geth` is in `$PATH`, see also
249    /// [`std::process::Command::new()`]
250    pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
251        self.program = Some(path.into());
252        self
253    }
254
255    /// Puts the `geth` instance in `dev` mode.
256    pub fn dev(mut self) -> Self {
257        self.mode = NodeMode::Dev(Default::default());
258        self
259    }
260
261    /// Returns whether the node is launched in Clique consensus mode.
262    pub const fn is_clique(&self) -> bool {
263        self.clique_private_key.is_some()
264    }
265
266    /// Calculates the address of the Clique consensus address.
267    pub fn clique_address(&self) -> Option<Address> {
268        self.clique_private_key.as_ref().map(|pk| Address::from_public_key(pk.verifying_key()))
269    }
270
271    /// Sets the Clique Private Key to the `geth` executable, which will be later
272    /// loaded on the node.
273    ///
274    /// The address derived from this private key will be used to set the `miner.etherbase` field
275    /// on the node.
276    #[deprecated = "clique support was removed in geth >=1.14"]
277    pub fn set_clique_private_key<T: Into<SigningKey>>(mut self, private_key: T) -> Self {
278        self.clique_private_key = Some(private_key.into());
279        self
280    }
281
282    /// Sets the port which will be used when the `geth-cli` instance is launched.
283    ///
284    /// If port is 0 then the OS will choose a random port.
285    /// [GethInstance::port] will return the port that was chosen.
286    pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
287        self.port = Some(port.into());
288        self
289    }
290
291    /// Sets the host which will be used when the `geth` instance is launched.
292    ///
293    /// Defaults to `localhost`.
294    pub fn host<T: Into<String>>(mut self, host: T) -> Self {
295        self.host = Some(host.into());
296        self
297    }
298
299    /// Sets the port which will be used for incoming p2p connections.
300    ///
301    /// This will put the geth instance into non-dev mode, discarding any previously set dev-mode
302    /// options.
303    pub fn p2p_port(mut self, port: u16) -> Self {
304        match &mut self.mode {
305            NodeMode::Dev(_) => {
306                self.mode = NodeMode::NonDev(PrivateNetOptions {
307                    p2p_port: Some(port),
308                    ..Default::default()
309                })
310            }
311            NodeMode::NonDev(opts) => opts.p2p_port = Some(port),
312        }
313        self
314    }
315
316    /// Sets the dev-chain block interval in seconds.
317    ///
318    /// This will put the geth instance in `dev` mode, discarding any previously set options that
319    /// cannot be used in dev mode.
320    pub const fn block_time(mut self, block_time: u64) -> Self {
321        self.mode = NodeMode::Dev(DevOptions { block_time: Some(block_time) });
322        self
323    }
324
325    /// Sets the chain id for the geth instance.
326    pub const fn chain_id(mut self, chain_id: u64) -> Self {
327        self.chain_id = Some(chain_id);
328        self
329    }
330
331    /// Allow geth to unlock accounts when rpc apis are open.
332    pub const fn insecure_unlock(mut self) -> Self {
333        self.insecure_unlock = true;
334        self
335    }
336
337    /// Enable IPC for the geth instance.
338    pub const fn enable_ipc(mut self) -> Self {
339        self.ipc_enabled = true;
340        self
341    }
342
343    /// Disable discovery for the geth instance.
344    ///
345    /// This will put the geth instance into non-dev mode, discarding any previously set dev-mode
346    /// options.
347    pub fn disable_discovery(mut self) -> Self {
348        self.inner_disable_discovery();
349        self
350    }
351
352    fn inner_disable_discovery(&mut self) {
353        match &mut self.mode {
354            NodeMode::Dev(_) => {
355                self.mode =
356                    NodeMode::NonDev(PrivateNetOptions { discovery: false, ..Default::default() })
357            }
358            NodeMode::NonDev(opts) => opts.discovery = false,
359        }
360    }
361
362    /// Sets the IPC path for the socket.
363    ///
364    /// This also enables IPC, as setting a path implies the intent to use IPC.
365    pub fn ipc_path<T: Into<PathBuf>>(mut self, path: T) -> Self {
366        self.ipc_path = Some(path.into());
367        self.ipc_enabled = true;
368        self
369    }
370
371    /// Sets the data directory for geth.
372    pub fn data_dir<T: Into<PathBuf>>(mut self, path: T) -> Self {
373        self.data_dir = Some(path.into());
374        self
375    }
376
377    /// Sets the `genesis.json` for the geth instance.
378    ///
379    /// If this is set, geth will be initialized with `geth init` and the `--datadir` option will be
380    /// set to the same value as `data_dir`.
381    ///
382    /// This is destructive and will overwrite any existing data in the data directory.
383    pub fn genesis(mut self, genesis: Genesis) -> Self {
384        self.genesis = Some(genesis);
385        self
386    }
387
388    /// Sets the port for authenticated RPC connections.
389    pub const fn authrpc_port(mut self, port: u16) -> Self {
390        self.authrpc_port = Some(port);
391        self
392    }
393
394    /// Sets the startup timeout in milliseconds.
395    ///
396    /// Defaults to [`NODE_STARTUP_TIMEOUT`]. The deadline is checked between complete stderr
397    /// lines, so a live process that emits no newline can block past the deadline.
398    pub const fn timeout(mut self, timeout: u64) -> Self {
399        self.timeout = Some(timeout);
400        self
401    }
402
403    /// Keep the handle to geth's stderr in order to read from it.
404    ///
405    /// This is required by [`GethInstance::stderr`] and [`GethInstance::wait_to_add_peer`].
406    /// Caution: if the stderr handle is not continuously consumed, the child process can block.
407    pub const fn keep_stderr(mut self) -> Self {
408        self.keep_err = true;
409        self
410    }
411
412    /// Adds an argument to pass to the `geth`.
413    pub fn push_arg<T: Into<OsString>>(&mut self, arg: T) {
414        self.args.push(arg.into());
415    }
416
417    /// Adds multiple arguments to pass to the `geth`.
418    pub fn extend_args<I, S>(&mut self, args: I)
419    where
420        I: IntoIterator<Item = S>,
421        S: Into<OsString>,
422    {
423        for arg in args {
424            self.push_arg(arg);
425        }
426    }
427
428    /// Adds an argument to pass to `geth`.
429    ///
430    /// Pass any arg that is not supported by the builder.
431    pub fn arg<T: Into<OsString>>(mut self, arg: T) -> Self {
432        self.args.push(arg.into());
433        self
434    }
435
436    /// Adds multiple arguments to pass to `geth`.
437    ///
438    /// Pass any args that is not supported by the builder.
439    pub fn args<I, S>(mut self, args: I) -> Self
440    where
441        I: IntoIterator<Item = S>,
442        S: Into<OsString>,
443    {
444        for arg in args {
445            self = self.arg(arg);
446        }
447        self
448    }
449
450    /// Consumes the builder and spawns `geth`.
451    ///
452    /// # Panics
453    ///
454    /// If spawning the instance fails at any point.
455    #[track_caller]
456    pub fn spawn(self) -> GethInstance {
457        self.try_spawn().unwrap()
458    }
459
460    /// Consumes the builder, spawns `geth`, and waits for its HTTP and active networking services
461    /// to report ready.
462    ///
463    /// Returns an error if the process cannot be started, reports a fatal startup error, or does
464    /// not become ready before the configured [`Self::timeout`] (defaulting to
465    /// [`NODE_STARTUP_TIMEOUT`]) is observed. The deadline is checked
466    /// between complete stderr lines; a live process that emits no newline can block this call
467    /// past the deadline.
468    pub fn try_spawn(mut self) -> Result<GethInstance, NodeError> {
469        let bin_path = self
470            .program
471            .as_ref()
472            .map_or_else(|| GETH.as_ref(), |bin| bin.as_os_str())
473            .to_os_string();
474        let mut cmd = Command::new(&bin_path);
475        // `geth` uses stderr for its logs
476        cmd.stderr(Stdio::piped());
477
478        // If no port provided, let the os chose it for us
479        let mut port = self.port.unwrap_or(0);
480        let port_s = port.to_string();
481
482        // If IPC is not enabled on the builder, disable it.
483        if !self.ipc_enabled {
484            cmd.arg("--ipcdisable");
485        }
486
487        // Open the HTTP API
488        cmd.arg("--http");
489        cmd.arg("--http.port").arg(&port_s);
490        cmd.arg("--http.api").arg(API);
491
492        if let Some(ref host) = self.host {
493            cmd.arg("--http.addr").arg(host);
494        }
495
496        // Open the WS API
497        cmd.arg("--ws");
498        cmd.arg("--ws.port").arg(port_s);
499        cmd.arg("--ws.api").arg(API);
500
501        if let Some(ref host) = self.host {
502            cmd.arg("--ws.addr").arg(host);
503        }
504
505        // pass insecure unlock flag if set
506        let is_clique = self.is_clique();
507        if self.insecure_unlock || is_clique {
508            cmd.arg("--allow-insecure-unlock");
509        }
510
511        if is_clique {
512            self.inner_disable_discovery();
513        }
514
515        // Set the port for authenticated APIs
516        let authrpc_port = self.authrpc_port.unwrap_or_else(&mut unused_port);
517        cmd.arg("--authrpc.port").arg(authrpc_port.to_string());
518
519        // use geth init to initialize the datadir if the genesis exists
520        if is_clique {
521            let clique_addr = self.clique_address();
522            if let Some(genesis) = &mut self.genesis {
523                // set up a clique config with an instant sealing period and short (8 block) epoch
524                let clique_config = CliqueConfig { period: Some(0), epoch: Some(8) };
525                genesis.config.clique = Some(clique_config);
526
527                let clique_addr = clique_addr.ok_or_else(|| {
528                    NodeError::CliqueAddressError(
529                        "could not calculates the address of the Clique consensus address."
530                            .to_string(),
531                    )
532                })?;
533
534                // set the extraData field
535                let extra_data_bytes =
536                    [&[0u8; 32][..], clique_addr.as_ref(), &[0u8; 65][..]].concat();
537                genesis.extra_data = extra_data_bytes.into();
538            }
539
540            let clique_addr = self.clique_address().ok_or_else(|| {
541                NodeError::CliqueAddressError(
542                    "could not calculates the address of the Clique consensus address.".to_string(),
543                )
544            })?;
545
546            self.genesis = Some(Genesis::clique_genesis(
547                self.chain_id.ok_or(NodeError::ChainIdNotSet)?,
548                clique_addr,
549            ));
550
551            // we must set the etherbase if using clique
552            // need to use format! / Debug here because the Address Display impl doesn't show the
553            // entire address
554            cmd.arg("--miner.etherbase").arg(format!("{clique_addr:?}"));
555        }
556
557        if let Some(genesis) = &self.genesis {
558            // create a temp dir to store the genesis file
559            let temp_genesis_dir = tempdir().map_err(NodeError::CreateDirError)?;
560            let temp_genesis_path = temp_genesis_dir.path().join("genesis.json");
561
562            // create the genesis file
563            let mut file = File::create(&temp_genesis_path).map_err(|_| {
564                NodeError::GenesisError("could not create genesis file".to_string())
565            })?;
566
567            // serialize genesis and write to file
568            serde_json::to_writer_pretty(&mut file, &genesis).map_err(|_| {
569                NodeError::GenesisError("could not write genesis to file".to_string())
570            })?;
571
572            let mut init_cmd = Command::new(bin_path);
573            if let Some(data_dir) = &self.data_dir {
574                init_cmd.arg("--datadir").arg(data_dir);
575            }
576
577            // set the stderr to null so we don't pollute the test output
578            init_cmd.stderr(Stdio::null());
579
580            init_cmd.arg("init").arg(temp_genesis_path);
581            let res = init_cmd
582                .spawn()
583                .map_err(NodeError::SpawnError)?
584                .wait()
585                .map_err(NodeError::WaitError)?;
586            // .expect("failed to wait for geth init to exit");
587            if !res.success() {
588                return Err(NodeError::InitError);
589            }
590
591            // temp_genesis_dir is dropped here, automatically cleaning up
592        }
593
594        if let Some(data_dir) = &self.data_dir {
595            cmd.arg("--datadir").arg(data_dir);
596
597            // create the directory if it doesn't exist
598            if !data_dir.exists() {
599                create_dir_all(data_dir).map_err(NodeError::CreateDirError)?;
600            }
601        }
602
603        // Dev mode with custom block time
604        let mut p2p_port = match self.mode {
605            NodeMode::Dev(DevOptions { block_time }) => {
606                cmd.arg("--dev");
607                if let Some(block_time) = block_time {
608                    cmd.arg("--dev.period").arg(block_time.to_string());
609                }
610                None
611            }
612            NodeMode::NonDev(PrivateNetOptions { p2p_port, discovery }) => {
613                // if no port provided, let the os chose it for us
614                let port = p2p_port.unwrap_or(0);
615                cmd.arg("--port").arg(port.to_string());
616
617                // disable discovery if the flag is set
618                if !discovery {
619                    cmd.arg("--nodiscover");
620                }
621                Some(port)
622            }
623        };
624
625        if let Some(chain_id) = self.chain_id {
626            cmd.arg("--networkid").arg(chain_id.to_string());
627        }
628
629        // debug verbosity is needed to check when peers are added
630        cmd.arg("--verbosity").arg("4");
631
632        if let Some(ipc) = &self.ipc_path {
633            cmd.arg("--ipcpath").arg(ipc);
634        }
635
636        cmd.args(self.args);
637
638        let mut child = cmd.spawn().map_err(NodeError::SpawnError)?;
639
640        let stderr = child.stderr.take().ok_or(NodeError::NoStderr)?;
641
642        let timeout = self.timeout.map(Duration::from_millis).unwrap_or(NODE_STARTUP_TIMEOUT);
643        let start = Instant::now();
644        let mut reader = BufReader::new(stderr);
645
646        // we shouldn't need to wait for p2p to start if geth is in dev mode - p2p is disabled in
647        // dev mode
648        let mut p2p_started = matches!(self.mode, NodeMode::Dev(_));
649        let mut ports_started = false;
650
651        loop {
652            if start.elapsed() >= timeout {
653                let _ = child.kill();
654                return Err(NodeError::Timeout);
655            }
656
657            let mut line = String::with_capacity(120);
658            reader.read_line(&mut line).map_err(NodeError::ReadLineError)?;
659
660            if matches!(self.mode, NodeMode::NonDev(_)) && line.contains("Started P2P networking") {
661                p2p_started = true;
662            }
663
664            if !matches!(self.mode, NodeMode::Dev(_)) {
665                // try to find the p2p port, if not in dev mode
666                if line.contains("New local node record") {
667                    if let Some(port) = extract_value("tcp=", &line) {
668                        p2p_port = port.parse::<u16>().ok();
669                    }
670                }
671            }
672
673            // geth 1.9.23 uses "server started" while 1.9.18 uses "endpoint opened"
674            // the unauthenticated api is used for regular non-engine API requests
675            if line.contains("HTTP endpoint opened")
676                || (line.contains("HTTP server started") && !line.contains("auth=true"))
677            {
678                // Extracts the address from the output
679                if let Some(addr) = extract_endpoint("endpoint=", &line) {
680                    // use the actual http port
681                    port = addr.port();
682                }
683
684                ports_started = true;
685            }
686
687            // Encountered an error such as Fatal: Error starting protocol stack: listen tcp
688            // 127.0.0.1:8545: bind: address already in use
689            if line.contains("Fatal:") {
690                let _ = child.kill();
691                return Err(NodeError::Fatal(line));
692            }
693
694            // If all ports have started we are ready to be queried.
695            if ports_started && p2p_started {
696                break;
697            }
698        }
699
700        if self.keep_err {
701            // re-attach the stderr handle if requested
702            child.stderr = Some(reader.into_inner());
703        } else {
704            // We need to consume the stderr otherwise geth is non-responsive and RPC server results
705            // in connection refused.
706            // See: <https://github.com/alloy-rs/alloy/issues/2091#issuecomment-2676134147>
707            std::thread::spawn(move || {
708                let mut buf = String::new();
709                loop {
710                    buf.clear();
711                    match reader.read_line(&mut buf) {
712                        Ok(0) | Err(_) => break,
713                        Ok(_) => {}
714                    }
715                }
716            });
717        }
718
719        Ok(GethInstance {
720            pid: child,
721            host: self.host.unwrap_or_else(|| "localhost".to_string()),
722            port,
723            ipc: self.ipc_path,
724            data_dir: self.data_dir,
725            p2p_port,
726            auth_port: self.authrpc_port,
727            genesis: self.genesis,
728            clique_private_key: self.clique_private_key,
729        })
730    }
731}
732
733#[cfg(test)]
734mod tests {
735    use super::*;
736
737    #[cfg(unix)]
738    #[test]
739    fn respects_startup_timeout() {
740        use std::os::unix::fs::PermissionsExt;
741
742        let dir = tempdir().unwrap();
743        let program = dir.path().join("geth");
744        // Emit a progress line after the default deadline, before announcing HTTP readiness.
745        std::fs::write(
746            &program,
747            "#!/bin/sh\nsleep 11\necho 'Initializing dev chain' >&2\necho 'HTTP server started endpoint=127.0.0.1:8545 auth=false' >&2\n",
748        )
749        .unwrap();
750        std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755)).unwrap();
751
752        assert!(matches!(Geth::at(&program).timeout(0).try_spawn(), Err(NodeError::Timeout)));
753        let geth = Geth::at(&program).timeout(30_000).try_spawn().unwrap();
754        assert_eq!(geth.port(), 8545);
755        assert!(geth.p2p_port().is_none());
756    }
757
758    #[test]
759    fn can_set_host() {
760        let geth = Geth::new().host("0.0.0.0").dev().try_spawn();
761        if let Ok(geth) = geth {
762            assert_eq!(geth.host(), "0.0.0.0");
763            assert!(geth.endpoint().starts_with("http://0.0.0.0:"));
764            assert!(geth.ws_endpoint().starts_with("ws://0.0.0.0:"));
765        }
766    }
767
768    #[test]
769    fn default_host_is_localhost() {
770        let geth = Geth::new().dev().try_spawn();
771        if let Ok(geth) = geth {
772            assert_eq!(geth.host(), "localhost");
773            assert!(geth.endpoint().starts_with("http://localhost:"));
774            assert!(geth.ws_endpoint().starts_with("ws://localhost:"));
775        }
776    }
777}