ethers_core/utils/
ganache.rs

1use crate::{
2    types::Address,
3    utils::{secret_key_to_address, unused_port},
4};
5use generic_array::GenericArray;
6use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey};
7use std::{
8    io::{BufRead, BufReader},
9    process::{Child, Command},
10    time::{Duration, Instant},
11};
12
13/// Default amount of time we will wait for ganache to indicate that it is ready.
14const GANACHE_STARTUP_TIMEOUT_MILLIS: u64 = 10_000;
15
16/// A ganache CLI instance. Will close the instance when dropped.
17///
18/// Construct this using [`Ganache`].
19pub struct GanacheInstance {
20    pid: Child,
21    private_keys: Vec<K256SecretKey>,
22    addresses: Vec<Address>,
23    port: u16,
24}
25
26impl GanacheInstance {
27    /// Returns the private keys used to instantiate this instance
28    pub fn keys(&self) -> &[K256SecretKey] {
29        &self.private_keys
30    }
31
32    /// Returns the addresses used to instantiate this instance
33    pub fn addresses(&self) -> &[Address] {
34        &self.addresses
35    }
36
37    /// Returns the port of this instance
38    pub fn port(&self) -> u16 {
39        self.port
40    }
41
42    /// Returns the HTTP endpoint of this instance
43    pub fn endpoint(&self) -> String {
44        format!("http://localhost:{}", self.port)
45    }
46
47    /// Returns the Websocket endpoint of this instance
48    pub fn ws_endpoint(&self) -> String {
49        format!("ws://localhost:{}", self.port)
50    }
51}
52
53impl Drop for GanacheInstance {
54    fn drop(&mut self) {
55        self.pid.kill().expect("could not kill ganache");
56    }
57}
58
59/// Builder for launching `ganache-cli`.
60///
61/// # Panics
62///
63/// If `spawn` is called without `ganache-cli` being available in the user's $PATH
64///
65/// # Example
66///
67/// ```no_run
68/// use ethers_core::utils::Ganache;
69///
70/// let port = 8545u16;
71/// let url = format!("http://localhost:{}", port).to_string();
72///
73/// let ganache = Ganache::new()
74///     .port(port)
75///     .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle")
76///     .spawn();
77///
78/// drop(ganache); // this will kill the instance
79/// ```
80#[derive(Clone, Default)]
81#[must_use = "This Builder struct does nothing unless it is `spawn`ed"]
82pub struct Ganache {
83    port: Option<u16>,
84    block_time: Option<u64>,
85    mnemonic: Option<String>,
86    fork: Option<String>,
87    args: Vec<String>,
88    startup_timeout: Option<u64>,
89}
90
91impl Ganache {
92    /// Creates an empty Ganache builder.
93    /// The default port is 8545. The mnemonic is chosen randomly.
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    /// Sets the startup timeout which will be used when the `ganache-cli` instance is launched in
99    /// miliseconds. 10_000 miliseconds by default).
100    pub fn startup_timeout_millis<T: Into<u64>>(mut self, timeout: T) -> Self {
101        self.startup_timeout = Some(timeout.into());
102        self
103    }
104
105    /// Sets the port which will be used when the `ganache-cli` instance is launched.
106    pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
107        self.port = Some(port.into());
108        self
109    }
110
111    /// Sets the mnemonic which will be used when the `ganache-cli` instance is launched.
112    pub fn mnemonic<T: Into<String>>(mut self, mnemonic: T) -> Self {
113        self.mnemonic = Some(mnemonic.into());
114        self
115    }
116
117    /// Sets the block-time which will be used when the `ganache-cli` instance is launched.
118    pub fn block_time<T: Into<u64>>(mut self, block_time: T) -> Self {
119        self.block_time = Some(block_time.into());
120        self
121    }
122
123    /// Sets the `fork` argument to fork from another currently running Ethereum client
124    /// at a given block. Input should be the HTTP location and port of the other client,
125    /// e.g. `http://localhost:8545`. You can optionally specify the block to fork from
126    /// using an @ sign: `http://localhost:8545@1599200`
127    pub fn fork<T: Into<String>>(mut self, fork: T) -> Self {
128        self.fork = Some(fork.into());
129        self
130    }
131
132    /// Adds an argument to pass to the `ganache-cli`.
133    pub fn arg<T: Into<String>>(mut self, arg: T) -> Self {
134        self.args.push(arg.into());
135        self
136    }
137
138    /// Adds multiple arguments to pass to the `ganache-cli`.
139    pub fn args<I, S>(mut self, args: I) -> Self
140    where
141        I: IntoIterator<Item = S>,
142        S: Into<String>,
143    {
144        for arg in args {
145            self = self.arg(arg);
146        }
147        self
148    }
149
150    /// Consumes the builder and spawns `ganache-cli`.
151    ///
152    /// # Panics
153    ///
154    /// If spawning the instance fails at any point.
155    #[track_caller]
156    pub fn spawn(self) -> GanacheInstance {
157        let mut cmd = Command::new("ganache-cli");
158        cmd.stdout(std::process::Stdio::piped());
159        let port = if let Some(port) = self.port { port } else { unused_port() };
160        cmd.arg("-p").arg(port.to_string());
161
162        if let Some(mnemonic) = self.mnemonic {
163            cmd.arg("-m").arg(mnemonic);
164        }
165
166        if let Some(block_time) = self.block_time {
167            cmd.arg("-b").arg(block_time.to_string());
168        }
169
170        if let Some(fork) = self.fork {
171            cmd.arg("-f").arg(fork);
172        }
173
174        cmd.args(self.args);
175
176        let mut child = cmd.spawn().expect("couldnt start ganache-cli");
177
178        let stdout = child.stdout.expect("Unable to get stdout for ganache child process");
179
180        let start = Instant::now();
181        let mut reader = BufReader::new(stdout);
182
183        let mut private_keys = Vec::new();
184        let mut addresses = Vec::new();
185        let mut is_private_key = false;
186
187        let startup_timeout =
188            Duration::from_millis(self.startup_timeout.unwrap_or(GANACHE_STARTUP_TIMEOUT_MILLIS));
189        loop {
190            if start + startup_timeout <= Instant::now() {
191                panic!("Timed out waiting for ganache to start. Is ganache-cli installed?")
192            }
193
194            let mut line = String::new();
195            reader.read_line(&mut line).expect("Failed to read line from ganache process");
196            if line.contains("Listening on") {
197                break
198            }
199
200            if line.starts_with("Private Keys") {
201                is_private_key = true;
202            }
203
204            if is_private_key && line.starts_with('(') {
205                let key_str = &line[6..line.len() - 1];
206                let key_hex = hex::decode(key_str).expect("could not parse as hex");
207                let key = K256SecretKey::from_bytes(&GenericArray::clone_from_slice(&key_hex))
208                    .expect("did not get private key");
209                addresses.push(secret_key_to_address(&SigningKey::from(&key)));
210                private_keys.push(key);
211            }
212        }
213
214        child.stdout = Some(reader.into_inner());
215
216        GanacheInstance { pid: child, private_keys, addresses, port }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    #[ignore]
226    fn configurable_startup_timeout() {
227        Ganache::new().startup_timeout_millis(100000_u64).spawn();
228    }
229
230    #[test]
231    #[ignore]
232    fn default_startup_works() {
233        Ganache::new().spawn();
234    }
235}