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::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    genesis: Option<Genesis>,
218    mode: NodeMode,
219    clique_private_key: Option<SigningKey>,
220    args: Vec<OsString>,
221}
222
223impl Geth {
224    /// Creates a Geth builder in dev mode.
225    pub fn new() -> Self {
226        Self::default()
227    }
228
229    /// Creates a Geth builder which will execute `geth` at the given path.
230    ///
231    /// # Example
232    ///
233    /// ```
234    /// use alloy_node_bindings::Geth;
235    /// # fn a() {
236    /// let geth = Geth::at("../go-ethereum/build/bin/geth").spawn();
237    ///
238    /// println!("Geth running at `{}`", geth.endpoint());
239    /// # }
240    /// ```
241    pub fn at(path: impl Into<PathBuf>) -> Self {
242        Self::new().path(path)
243    }
244
245    /// Sets the `path` to the `geth` executable
246    ///
247    /// By default, it's expected that `geth` is in `$PATH`, see also
248    /// [`std::process::Command::new()`]
249    pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
250        self.program = Some(path.into());
251        self
252    }
253
254    /// Puts the `geth` instance in `dev` mode.
255    pub fn dev(mut self) -> Self {
256        self.mode = NodeMode::Dev(Default::default());
257        self
258    }
259
260    /// Returns whether the node is launched in Clique consensus mode.
261    pub const fn is_clique(&self) -> bool {
262        self.clique_private_key.is_some()
263    }
264
265    /// Calculates the address of the Clique consensus address.
266    pub fn clique_address(&self) -> Option<Address> {
267        self.clique_private_key.as_ref().map(|pk| Address::from_public_key(pk.verifying_key()))
268    }
269
270    /// Sets the Clique Private Key to the `geth` executable, which will be later
271    /// loaded on the node.
272    ///
273    /// The address derived from this private key will be used to set the `miner.etherbase` field
274    /// on the node.
275    #[deprecated = "clique support was removed in geth >=1.14"]
276    pub fn set_clique_private_key<T: Into<SigningKey>>(mut self, private_key: T) -> Self {
277        self.clique_private_key = Some(private_key.into());
278        self
279    }
280
281    /// Sets the port which will be used when the `geth-cli` instance is launched.
282    ///
283    /// If port is 0 then the OS will choose a random port.
284    /// [GethInstance::port] will return the port that was chosen.
285    pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
286        self.port = Some(port.into());
287        self
288    }
289
290    /// Sets the host which will be used when the `geth` instance is launched.
291    ///
292    /// Defaults to `localhost`.
293    pub fn host<T: Into<String>>(mut self, host: T) -> Self {
294        self.host = Some(host.into());
295        self
296    }
297
298    /// Sets the port which will be used for incoming p2p connections.
299    ///
300    /// This will put the geth instance into non-dev mode, discarding any previously set dev-mode
301    /// options.
302    pub fn p2p_port(mut self, port: u16) -> Self {
303        match &mut self.mode {
304            NodeMode::Dev(_) => {
305                self.mode = NodeMode::NonDev(PrivateNetOptions {
306                    p2p_port: Some(port),
307                    ..Default::default()
308                })
309            }
310            NodeMode::NonDev(opts) => opts.p2p_port = Some(port),
311        }
312        self
313    }
314
315    /// Sets the dev-chain block interval in seconds.
316    ///
317    /// This will put the geth instance in `dev` mode, discarding any previously set options that
318    /// cannot be used in dev mode.
319    pub const fn block_time(mut self, block_time: u64) -> Self {
320        self.mode = NodeMode::Dev(DevOptions { block_time: Some(block_time) });
321        self
322    }
323
324    /// Sets the chain id for the geth instance.
325    pub const fn chain_id(mut self, chain_id: u64) -> Self {
326        self.chain_id = Some(chain_id);
327        self
328    }
329
330    /// Allow geth to unlock accounts when rpc apis are open.
331    pub const fn insecure_unlock(mut self) -> Self {
332        self.insecure_unlock = true;
333        self
334    }
335
336    /// Enable IPC for the geth instance.
337    pub const fn enable_ipc(mut self) -> Self {
338        self.ipc_enabled = true;
339        self
340    }
341
342    /// Disable discovery for the geth instance.
343    ///
344    /// This will put the geth instance into non-dev mode, discarding any previously set dev-mode
345    /// options.
346    pub fn disable_discovery(mut self) -> Self {
347        self.inner_disable_discovery();
348        self
349    }
350
351    fn inner_disable_discovery(&mut self) {
352        match &mut self.mode {
353            NodeMode::Dev(_) => {
354                self.mode =
355                    NodeMode::NonDev(PrivateNetOptions { discovery: false, ..Default::default() })
356            }
357            NodeMode::NonDev(opts) => opts.discovery = false,
358        }
359    }
360
361    /// Sets the IPC path for the socket.
362    ///
363    /// This also enables IPC, as setting a path implies the intent to use IPC.
364    pub fn ipc_path<T: Into<PathBuf>>(mut self, path: T) -> Self {
365        self.ipc_path = Some(path.into());
366        self.ipc_enabled = true;
367        self
368    }
369
370    /// Sets the data directory for geth.
371    pub fn data_dir<T: Into<PathBuf>>(mut self, path: T) -> Self {
372        self.data_dir = Some(path.into());
373        self
374    }
375
376    /// Sets the `genesis.json` for the geth instance.
377    ///
378    /// If this is set, geth will be initialized with `geth init` and the `--datadir` option will be
379    /// set to the same value as `data_dir`.
380    ///
381    /// This is destructive and will overwrite any existing data in the data directory.
382    pub fn genesis(mut self, genesis: Genesis) -> Self {
383        self.genesis = Some(genesis);
384        self
385    }
386
387    /// Sets the port for authenticated RPC connections.
388    pub const fn authrpc_port(mut self, port: u16) -> Self {
389        self.authrpc_port = Some(port);
390        self
391    }
392
393    /// Keep the handle to geth's stderr in order to read from it.
394    ///
395    /// This is required by [`GethInstance::stderr`] and [`GethInstance::wait_to_add_peer`].
396    /// Caution: if the stderr handle is not continuously consumed, the child process can block.
397    pub const fn keep_stderr(mut self) -> Self {
398        self.keep_err = true;
399        self
400    }
401
402    /// Adds an argument to pass to the `geth`.
403    pub fn push_arg<T: Into<OsString>>(&mut self, arg: T) {
404        self.args.push(arg.into());
405    }
406
407    /// Adds multiple arguments to pass to the `geth`.
408    pub fn extend_args<I, S>(&mut self, args: I)
409    where
410        I: IntoIterator<Item = S>,
411        S: Into<OsString>,
412    {
413        for arg in args {
414            self.push_arg(arg);
415        }
416    }
417
418    /// Adds an argument to pass to `geth`.
419    ///
420    /// Pass any arg that is not supported by the builder.
421    pub fn arg<T: Into<OsString>>(mut self, arg: T) -> Self {
422        self.args.push(arg.into());
423        self
424    }
425
426    /// Adds multiple arguments to pass to `geth`.
427    ///
428    /// Pass any args that is not supported by the builder.
429    pub fn args<I, S>(mut self, args: I) -> Self
430    where
431        I: IntoIterator<Item = S>,
432        S: Into<OsString>,
433    {
434        for arg in args {
435            self = self.arg(arg);
436        }
437        self
438    }
439
440    /// Consumes the builder and spawns `geth`.
441    ///
442    /// # Panics
443    ///
444    /// If spawning the instance fails at any point.
445    #[track_caller]
446    pub fn spawn(self) -> GethInstance {
447        self.try_spawn().unwrap()
448    }
449
450    /// Consumes the builder, spawns `geth`, and waits for its HTTP and active networking services
451    /// to report ready.
452    ///
453    /// Returns an error if the process cannot be started, reports a fatal startup error, or does
454    /// not become ready before [`NODE_STARTUP_TIMEOUT`] is observed. The deadline is checked
455    /// between complete stderr lines; a live process that emits no newline can block this call
456    /// past the deadline.
457    pub fn try_spawn(mut self) -> Result<GethInstance, NodeError> {
458        let bin_path = self
459            .program
460            .as_ref()
461            .map_or_else(|| GETH.as_ref(), |bin| bin.as_os_str())
462            .to_os_string();
463        let mut cmd = Command::new(&bin_path);
464        // `geth` uses stderr for its logs
465        cmd.stderr(Stdio::piped());
466
467        // If no port provided, let the os chose it for us
468        let mut port = self.port.unwrap_or(0);
469        let port_s = port.to_string();
470
471        // If IPC is not enabled on the builder, disable it.
472        if !self.ipc_enabled {
473            cmd.arg("--ipcdisable");
474        }
475
476        // Open the HTTP API
477        cmd.arg("--http");
478        cmd.arg("--http.port").arg(&port_s);
479        cmd.arg("--http.api").arg(API);
480
481        if let Some(ref host) = self.host {
482            cmd.arg("--http.addr").arg(host);
483        }
484
485        // Open the WS API
486        cmd.arg("--ws");
487        cmd.arg("--ws.port").arg(port_s);
488        cmd.arg("--ws.api").arg(API);
489
490        if let Some(ref host) = self.host {
491            cmd.arg("--ws.addr").arg(host);
492        }
493
494        // pass insecure unlock flag if set
495        let is_clique = self.is_clique();
496        if self.insecure_unlock || is_clique {
497            cmd.arg("--allow-insecure-unlock");
498        }
499
500        if is_clique {
501            self.inner_disable_discovery();
502        }
503
504        // Set the port for authenticated APIs
505        let authrpc_port = self.authrpc_port.unwrap_or_else(&mut unused_port);
506        cmd.arg("--authrpc.port").arg(authrpc_port.to_string());
507
508        // use geth init to initialize the datadir if the genesis exists
509        if is_clique {
510            let clique_addr = self.clique_address();
511            if let Some(genesis) = &mut self.genesis {
512                // set up a clique config with an instant sealing period and short (8 block) epoch
513                let clique_config = CliqueConfig { period: Some(0), epoch: Some(8) };
514                genesis.config.clique = Some(clique_config);
515
516                let clique_addr = clique_addr.ok_or_else(|| {
517                    NodeError::CliqueAddressError(
518                        "could not calculates the address of the Clique consensus address."
519                            .to_string(),
520                    )
521                })?;
522
523                // set the extraData field
524                let extra_data_bytes =
525                    [&[0u8; 32][..], clique_addr.as_ref(), &[0u8; 65][..]].concat();
526                genesis.extra_data = extra_data_bytes.into();
527            }
528
529            let clique_addr = self.clique_address().ok_or_else(|| {
530                NodeError::CliqueAddressError(
531                    "could not calculates the address of the Clique consensus address.".to_string(),
532                )
533            })?;
534
535            self.genesis = Some(Genesis::clique_genesis(
536                self.chain_id.ok_or(NodeError::ChainIdNotSet)?,
537                clique_addr,
538            ));
539
540            // we must set the etherbase if using clique
541            // need to use format! / Debug here because the Address Display impl doesn't show the
542            // entire address
543            cmd.arg("--miner.etherbase").arg(format!("{clique_addr:?}"));
544        }
545
546        if let Some(genesis) = &self.genesis {
547            // create a temp dir to store the genesis file
548            let temp_genesis_dir = tempdir().map_err(NodeError::CreateDirError)?;
549            let temp_genesis_path = temp_genesis_dir.path().join("genesis.json");
550
551            // create the genesis file
552            let mut file = File::create(&temp_genesis_path).map_err(|_| {
553                NodeError::GenesisError("could not create genesis file".to_string())
554            })?;
555
556            // serialize genesis and write to file
557            serde_json::to_writer_pretty(&mut file, &genesis).map_err(|_| {
558                NodeError::GenesisError("could not write genesis to file".to_string())
559            })?;
560
561            let mut init_cmd = Command::new(bin_path);
562            if let Some(data_dir) = &self.data_dir {
563                init_cmd.arg("--datadir").arg(data_dir);
564            }
565
566            // set the stderr to null so we don't pollute the test output
567            init_cmd.stderr(Stdio::null());
568
569            init_cmd.arg("init").arg(temp_genesis_path);
570            let res = init_cmd
571                .spawn()
572                .map_err(NodeError::SpawnError)?
573                .wait()
574                .map_err(NodeError::WaitError)?;
575            // .expect("failed to wait for geth init to exit");
576            if !res.success() {
577                return Err(NodeError::InitError);
578            }
579
580            // temp_genesis_dir is dropped here, automatically cleaning up
581        }
582
583        if let Some(data_dir) = &self.data_dir {
584            cmd.arg("--datadir").arg(data_dir);
585
586            // create the directory if it doesn't exist
587            if !data_dir.exists() {
588                create_dir_all(data_dir).map_err(NodeError::CreateDirError)?;
589            }
590        }
591
592        // Dev mode with custom block time
593        let mut p2p_port = match self.mode {
594            NodeMode::Dev(DevOptions { block_time }) => {
595                cmd.arg("--dev");
596                if let Some(block_time) = block_time {
597                    cmd.arg("--dev.period").arg(block_time.to_string());
598                }
599                None
600            }
601            NodeMode::NonDev(PrivateNetOptions { p2p_port, discovery }) => {
602                // if no port provided, let the os chose it for us
603                let port = p2p_port.unwrap_or(0);
604                cmd.arg("--port").arg(port.to_string());
605
606                // disable discovery if the flag is set
607                if !discovery {
608                    cmd.arg("--nodiscover");
609                }
610                Some(port)
611            }
612        };
613
614        if let Some(chain_id) = self.chain_id {
615            cmd.arg("--networkid").arg(chain_id.to_string());
616        }
617
618        // debug verbosity is needed to check when peers are added
619        cmd.arg("--verbosity").arg("4");
620
621        if let Some(ipc) = &self.ipc_path {
622            cmd.arg("--ipcpath").arg(ipc);
623        }
624
625        cmd.args(self.args);
626
627        let mut child = cmd.spawn().map_err(NodeError::SpawnError)?;
628
629        let stderr = child.stderr.take().ok_or(NodeError::NoStderr)?;
630
631        let start = Instant::now();
632        let mut reader = BufReader::new(stderr);
633
634        // we shouldn't need to wait for p2p to start if geth is in dev mode - p2p is disabled in
635        // dev mode
636        let mut p2p_started = matches!(self.mode, NodeMode::Dev(_));
637        let mut ports_started = false;
638
639        loop {
640            if start + NODE_STARTUP_TIMEOUT <= Instant::now() {
641                let _ = child.kill();
642                return Err(NodeError::Timeout);
643            }
644
645            let mut line = String::with_capacity(120);
646            reader.read_line(&mut line).map_err(NodeError::ReadLineError)?;
647
648            if matches!(self.mode, NodeMode::NonDev(_)) && line.contains("Started P2P networking") {
649                p2p_started = true;
650            }
651
652            if !matches!(self.mode, NodeMode::Dev(_)) {
653                // try to find the p2p port, if not in dev mode
654                if line.contains("New local node record") {
655                    if let Some(port) = extract_value("tcp=", &line) {
656                        p2p_port = port.parse::<u16>().ok();
657                    }
658                }
659            }
660
661            // geth 1.9.23 uses "server started" while 1.9.18 uses "endpoint opened"
662            // the unauthenticated api is used for regular non-engine API requests
663            if line.contains("HTTP endpoint opened")
664                || (line.contains("HTTP server started") && !line.contains("auth=true"))
665            {
666                // Extracts the address from the output
667                if let Some(addr) = extract_endpoint("endpoint=", &line) {
668                    // use the actual http port
669                    port = addr.port();
670                }
671
672                ports_started = true;
673            }
674
675            // Encountered an error such as Fatal: Error starting protocol stack: listen tcp
676            // 127.0.0.1:8545: bind: address already in use
677            if line.contains("Fatal:") {
678                let _ = child.kill();
679                return Err(NodeError::Fatal(line));
680            }
681
682            // If all ports have started we are ready to be queried.
683            if ports_started && p2p_started {
684                break;
685            }
686        }
687
688        if self.keep_err {
689            // re-attach the stderr handle if requested
690            child.stderr = Some(reader.into_inner());
691        } else {
692            // We need to consume the stderr otherwise geth is non-responsive and RPC server results
693            // in connection refused.
694            // See: <https://github.com/alloy-rs/alloy/issues/2091#issuecomment-2676134147>
695            std::thread::spawn(move || {
696                let mut buf = String::new();
697                loop {
698                    buf.clear();
699                    match reader.read_line(&mut buf) {
700                        Ok(0) | Err(_) => break,
701                        Ok(_) => {}
702                    }
703                }
704            });
705        }
706
707        Ok(GethInstance {
708            pid: child,
709            host: self.host.unwrap_or_else(|| "localhost".to_string()),
710            port,
711            ipc: self.ipc_path,
712            data_dir: self.data_dir,
713            p2p_port,
714            auth_port: self.authrpc_port,
715            genesis: self.genesis,
716            clique_private_key: self.clique_private_key,
717        })
718    }
719}
720
721#[cfg(test)]
722mod tests {
723    use super::*;
724
725    #[test]
726    fn can_set_host() {
727        let geth = Geth::new().host("0.0.0.0").dev().try_spawn();
728        if let Ok(geth) = geth {
729            assert_eq!(geth.host(), "0.0.0.0");
730            assert!(geth.endpoint().starts_with("http://0.0.0.0:"));
731            assert!(geth.ws_endpoint().starts_with("ws://0.0.0.0:"));
732        }
733    }
734
735    #[test]
736    fn default_host_is_localhost() {
737        let geth = Geth::new().dev().try_spawn();
738        if let Ok(geth) = geth {
739            assert_eq!(geth.host(), "localhost");
740            assert!(geth.endpoint().starts_with("http://localhost:"));
741            assert!(geth.ws_endpoint().starts_with("ws://localhost:"));
742        }
743    }
744}