ethers_core/utils/
ganache.rs1use 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
13const GANACHE_STARTUP_TIMEOUT_MILLIS: u64 = 10_000;
15
16pub struct GanacheInstance {
20 pid: Child,
21 private_keys: Vec<K256SecretKey>,
22 addresses: Vec<Address>,
23 port: u16,
24}
25
26impl GanacheInstance {
27 pub fn keys(&self) -> &[K256SecretKey] {
29 &self.private_keys
30 }
31
32 pub fn addresses(&self) -> &[Address] {
34 &self.addresses
35 }
36
37 pub fn port(&self) -> u16 {
39 self.port
40 }
41
42 pub fn endpoint(&self) -> String {
44 format!("http://localhost:{}", self.port)
45 }
46
47 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#[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 pub fn new() -> Self {
95 Self::default()
96 }
97
98 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 pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
107 self.port = Some(port.into());
108 self
109 }
110
111 pub fn mnemonic<T: Into<String>>(mut self, mnemonic: T) -> Self {
113 self.mnemonic = Some(mnemonic.into());
114 self
115 }
116
117 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 pub fn fork<T: Into<String>>(mut self, fork: T) -> Self {
128 self.fork = Some(fork.into());
129 self
130 }
131
132 pub fn arg<T: Into<String>>(mut self, arg: T) -> Self {
134 self.args.push(arg.into());
135 self
136 }
137
138 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 #[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}