diff --git a/Cargo.lock b/Cargo.lock index 29219ecd..85c40bf4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -244,15 +244,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" -[[package]] -name = "cloudabi" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -dependencies = [ - "bitflags", -] - [[package]] name = "concurrent-queue" version = "1.1.1" @@ -436,7 +427,6 @@ dependencies = [ "rustc-hex", "serde", "serde_json", - "serial_test", "thiserror", "tokio", ] @@ -508,7 +498,6 @@ dependencies = [ "rustc-hex", "serde", "serde_json", - "serial_test", "thiserror", "tokio", "tokio-native-tls", @@ -972,15 +961,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "lock_api" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" -dependencies = [ - "scopeguard", -] - [[package]] name = "log" version = "0.4.8" @@ -1151,30 +1131,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a7fad362df89617628a7508b3e9d588ade1b0ac31aa25de168193ad999c2dd4" -[[package]] -name = "parking_lot" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" -dependencies = [ - "cfg-if", - "cloudabi", - "libc", - "redox_syscall", - "smallvec", - "winapi 0.3.8", -] - [[package]] name = "percent-encoding" version = "2.1.0" @@ -1461,12 +1417,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - [[package]] name = "sct" version = "0.6.0" @@ -1543,28 +1493,6 @@ dependencies = [ "url", ] -[[package]] -name = "serial_test" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef5f7c7434b2f2c598adc6f9494648a1e41274a75c0ba4056f680ae0c117fd6" -dependencies = [ - "lazy_static", - "parking_lot", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08338d8024b227c62bd68a12c7c9883f5c66780abaef15c550dc56f46ee6515" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "sha-1" version = "0.8.2" diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index 98271113..81df360b 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -27,7 +27,6 @@ futures = "0.3.5" ethers = { version = "0.1.3", path = "../ethers" } tokio = { version = "0.2.21", default-features = false, features = ["macros"] } serde_json = "1.0.55" -serial_test = "0.4.0" [features] abigen = ["ethers-contract-abigen", "ethers-contract-derive"] diff --git a/ethers-contract/tests/common/mod.rs b/ethers-contract/tests/common/mod.rs index 0cb16c66..e62afb9d 100644 --- a/ethers-contract/tests/common/mod.rs +++ b/ethers-contract/tests/common/mod.rs @@ -4,7 +4,7 @@ use ethers_core::{ }; use ethers_contract::{Contract, ContractFactory}; -use ethers_core::utils::{Ganache, GanacheInstance, Solc}; +use ethers_core::utils::{GanacheInstance, Solc}; use ethers_providers::{Http, Provider}; use ethers_signers::{Client, Wallet}; use std::{convert::TryFrom, sync::Arc, time::Duration}; @@ -44,11 +44,14 @@ pub fn compile() -> (Abi, Bytes) { } /// connects the private key to http://localhost:8545 -pub fn connect(private_key: &str) -> Arc> { - let provider = Provider::::try_from("http://localhost:8545") - .unwrap() - .interval(Duration::from_millis(10u64)); - Arc::new(private_key.parse::().unwrap().connect(provider)) +pub fn connect(ganache: &GanacheInstance, idx: usize) -> Arc> { + let provider = Provider::::try_from(ganache.endpoint()).unwrap(); + let wallet: Wallet = ganache.keys()[idx].clone().into(); + Arc::new( + wallet + .connect(provider) + .interval(Duration::from_millis(10u64)), + ) } /// Launches a ganache instance and deploys the SimpleStorage contract @@ -56,18 +59,12 @@ pub async fn deploy( client: Arc>, abi: Abi, bytecode: Bytes, -) -> (GanacheInstance, Contract) { - let ganache = Ganache::new() - .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") - .spawn(); - +) -> Contract { let factory = ContractFactory::new(abi, bytecode, client); - let contract = factory + factory .deploy("initial value".to_string()) .unwrap() .send() .await - .unwrap(); - - (ganache, contract) + .unwrap() } diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs index bea466a6..a65af92d 100644 --- a/ethers-contract/tests/contract.rs +++ b/ethers-contract/tests/contract.rs @@ -12,23 +12,19 @@ mod eth_tests { types::Address, utils::Ganache, }; - use serial_test::serial; - use std::{convert::TryFrom, sync::Arc, time::Duration}; + use std::{convert::TryFrom, sync::Arc}; #[tokio::test] - #[serial] async fn deploy_and_call_contract() { let (abi, bytecode) = compile(); // launch ganache - let _ganache = Ganache::new() - .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") - .spawn(); + let ganache = Ganache::new().spawn(); // Instantiate the clients. We assume that clients consume the provider and the wallet // (which makes sense), so for multi-client tests, you must clone the provider. - let client = connect("380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"); - let client2 = connect("cc96601bc52293b53c4736a12af9130abf347669b3813f9ec4cafdf6991b087e"); + let client = connect(&ganache, 0); + let client2 = connect(&ganache, 1); // create a factory which will be used to deploy instances of the contract let factory = ContractFactory::new(abi, bytecode, client.clone()); @@ -80,11 +76,11 @@ mod eth_tests { } #[tokio::test] - #[serial] async fn get_past_events() { let (abi, bytecode) = compile(); - let client = connect("380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"); - let (_ganache, contract) = deploy(client.clone(), abi, bytecode).await; + let ganache = Ganache::new().spawn(); + let client = connect(&ganache, 0); + let contract = deploy(client.clone(), abi, bytecode).await; // make a call with `client2` let _tx_hash = contract @@ -109,11 +105,11 @@ mod eth_tests { } #[tokio::test] - #[serial] async fn watch_events() { let (abi, bytecode) = compile(); - let client = connect("380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"); - let (_ganache, contract) = deploy(client, abi, bytecode).await; + let ganache = Ganache::new().spawn(); + let client = connect(&ganache, 0); + let contract = deploy(client, abi, bytecode).await; // We spawn the event listener: let mut stream = contract @@ -144,17 +140,21 @@ mod eth_tests { } #[tokio::test] - #[serial] async fn signer_on_node() { let (abi, bytecode) = compile(); - let provider = Provider::::try_from("http://localhost:8545") + // spawn ganache + let ganache = Ganache::new().spawn(); + + // connect + let provider = Provider::::try_from(ganache.endpoint()) .unwrap() - .interval(Duration::from_millis(10u64)); - let deployer = "3cDB3d9e1B74692Bb1E3bb5fc81938151cA64b02" - .parse::
() - .unwrap(); + .interval(std::time::Duration::from_millis(50u64)); + + // get the first account + let deployer = provider.get_accounts().await.unwrap()[0]; let client = Arc::new(Client::from(provider).with_sender(deployer)); - let (_ganache, contract) = deploy(client, abi, bytecode).await; + + let contract = deploy(client, abi, bytecode).await; // make a call without the signer let tx_hash = contract diff --git a/ethers-core/src/utils/ganache.rs b/ethers-core/src/utils/ganache.rs index 6ed86d4d..32926ee6 100644 --- a/ethers-core/src/utils/ganache.rs +++ b/ethers-core/src/utils/ganache.rs @@ -1,18 +1,48 @@ +use crate::types::PrivateKey; use std::{ + io::{BufRead, BufReader}, + net::TcpListener, process::{Child, Command}, - time::Duration, + time::{Duration, Instant}, }; -const SLEEP_TIME: Duration = Duration::from_secs(3); +/// How long we will wait for ganache to indicate that it is ready. +const GANACHE_STARTUP_TIMEOUT_MILLIS: u64 = 10_000; /// A ganache CLI instance. Will close the instance when dropped. /// /// Construct this using [`Ganache`](crate::utils::Ganache) -pub struct GanacheInstance(Child); +pub struct GanacheInstance { + pid: Child, + private_keys: Vec, + port: u16, +} + +impl GanacheInstance { + /// Returns the private keys used to instantiate this instance + pub fn keys(&self) -> &[PrivateKey] { + &self.private_keys + } + + /// Returns the port of this instance + pub fn port(&self) -> u16 { + self.port + } + + /// Returns the HTTP endpoint of this instance + pub fn endpoint(&self) -> String { + format!("http://localhost:{}", self.port) + } + + /// Returns the Websocket endpoint of this instance + pub fn ws_endpoint(&self) -> String { + format!("ws://localhost:{}", self.port) + } +} impl Drop for GanacheInstance { fn drop(&mut self) { - self.0.kill().expect("could not kill ganache"); + let _ = self.pid.kill().expect("could not kill ganache"); } } @@ -27,7 +57,7 @@ impl Drop for GanacheInstance { /// ```no_run /// use ethers::utils::Ganache; /// -/// let port = 8545u64; +/// let port = 8545u16; /// let url = format!("http://localhost:{}", port).to_string(); /// /// let ganache = Ganache::new() @@ -39,7 +69,7 @@ impl Drop for GanacheInstance { /// ``` #[derive(Clone, Default)] pub struct Ganache { - port: Option, + port: Option, block_time: Option, mnemonic: Option, } @@ -52,7 +82,7 @@ impl Ganache { } /// Sets the port which will be used when the `ganache-cli` instance is launched. - pub fn port>(mut self, port: T) -> Self { + pub fn port>(mut self, port: T) -> Self { self.port = Some(port.into()); self } @@ -74,10 +104,13 @@ impl Ganache { /// waiting for `ganache-cli` to launch. pub fn spawn(self) -> GanacheInstance { let mut cmd = Command::new("ganache-cli"); - cmd.stdout(std::process::Stdio::null()); - if let Some(port) = self.port { - cmd.arg("-p").arg(port.to_string()); - } + cmd.stdout(std::process::Stdio::piped()); + let port = if let Some(port) = self.port { + port + } else { + unused_port() + }; + cmd.arg("-p").arg(port.to_string()); if let Some(mnemonic) = self.mnemonic { cmd.arg("-m").arg(mnemonic); @@ -87,10 +120,61 @@ impl Ganache { cmd.arg("-b").arg(block_time.to_string()); } - let ganache_pid = cmd.spawn().expect("couldnt start ganache-cli"); + let mut child = cmd.spawn().expect("couldnt start ganache-cli"); - // wait a couple of seconds for ganache to boot up - std::thread::sleep(SLEEP_TIME); - GanacheInstance(ganache_pid) + let stdout = child + .stdout + .expect("Unable to get stdout for ganache child process"); + + let start = Instant::now(); + let mut reader = BufReader::new(stdout); + + let mut private_keys = Vec::new(); + let mut is_private_key = false; + loop { + if start + Duration::from_millis(GANACHE_STARTUP_TIMEOUT_MILLIS) <= Instant::now() { + panic!("Timed out waiting for ganache to start. Is ganache-cli installed?") + } + + let mut line = String::new(); + reader + .read_line(&mut line) + .expect("Failed to read line from ganache process"); + if line.starts_with("Listening on") { + break; + } + + if line.starts_with("Private Keys") { + is_private_key = true; + } + + if is_private_key && line.starts_with('(') { + let key_str = &line[6..line.len() - 1]; + let key: PrivateKey = key_str.parse().expect("did not get private key"); + private_keys.push(key); + } + } + + child.stdout = Some(reader.into_inner()); + + GanacheInstance { + pid: child, + private_keys, + port, + } } } + +/// A bit of hack to find an unused TCP port. +/// +/// Does not guarantee that the given port is unused after the function exists, just that it was +/// unused before the function started (i.e., it does not reserve a port). +pub fn unused_port() -> u16 { + let listener = TcpListener::bind("127.0.0.1:0") + .expect("Failed to create TCP listener to find unused port"); + + let local_addr = listener + .local_addr() + .expect("Failed to read TCP listener local_addr to find unused port"); + local_addr.port() +} diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml index 2a762489..71010fc2 100644 --- a/ethers-providers/Cargo.toml +++ b/ethers-providers/Cargo.toml @@ -45,9 +45,11 @@ rustc-hex = "2.1.0" tokio = { version = "0.2.21", default-features = false, features = ["rt-core", "macros"] } async-std = { version = "1.6.2", default-features = false, features = ["attributes"] } async-tungstenite = { version = "0.6.0", features = ["tokio-runtime"] } -serial_test = "0.4.0" [features] +# slightly opinionated, but for convenience we default to tokio-tls +# to allow websockets w/ TLS support +default = ["tokio-tls"] celo = ["ethers-core/celo"] tokio-runtime = [ diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs index cbe020db..b21817ae 100644 --- a/ethers-providers/src/provider.rs +++ b/ethers-providers/src/provider.rs @@ -534,6 +534,14 @@ impl TryFrom<&str> for Provider { } } +impl TryFrom for Provider { + type Error = ParseError; + + fn try_from(src: String) -> Result { + Provider::try_from(src.as_str()) + } +} + #[cfg(test)] mod ens_tests { use super::*; diff --git a/ethers-providers/tests/provider.rs b/ethers-providers/tests/provider.rs index 58d5ee93..f6cebbb5 100644 --- a/ethers-providers/tests/provider.rs +++ b/ethers-providers/tests/provider.rs @@ -10,7 +10,6 @@ mod eth_tests { types::TransactionRequest, utils::{parse_ether, Ganache}, }; - use serial_test::serial; // Without TLS this would error with "TLS Support not compiled in" #[test] @@ -36,7 +35,6 @@ mod eth_tests { } #[tokio::test] - #[serial] #[cfg(feature = "tokio-runtime")] async fn watch_blocks_websocket() { use ethers::{ @@ -44,8 +42,8 @@ mod eth_tests { types::H256, }; - let _ganache = Ganache::new().block_time(2u64).spawn(); - let (ws, _) = async_tungstenite::tokio::connect_async("ws://localhost:8545") + let ganache = Ganache::new().block_time(2u64).spawn(); + let (ws, _) = async_tungstenite::tokio::connect_async(ganache.ws_endpoint()) .await .unwrap(); let provider = Provider::new(Ws::new(ws)).interval(Duration::from_millis(500u64)); @@ -57,22 +55,20 @@ mod eth_tests { } #[tokio::test] - #[serial] async fn pending_txs_with_confirmations_ganache() { - let _ganache = Ganache::new().block_time(2u64).spawn(); - let provider = Provider::::try_from("http://localhost:8545") + let ganache = Ganache::new().block_time(2u64).spawn(); + let provider = Provider::::try_from(ganache.endpoint()) .unwrap() .interval(Duration::from_millis(500u64)); generic_pending_txs_test(provider).await; } #[tokio::test] - #[serial] #[cfg(any(feature = "tokio-runtime", feature = "tokio-tls"))] async fn websocket_pending_txs_with_confirmations_ganache() { use ethers::providers::Ws; - let _ganache = Ganache::new().block_time(2u64).port(8546u64).spawn(); - let ws = Ws::connect("ws://localhost:8546").await.unwrap(); + let ganache = Ganache::new().block_time(2u64).spawn(); + let ws = Ws::connect(ganache.ws_endpoint()).await.unwrap(); let provider = Provider::new(ws); generic_pending_txs_test(provider).await; } diff --git a/ethers-signers/src/wallet.rs b/ethers-signers/src/wallet.rs index 794f19d6..5a2cddca 100644 --- a/ethers-signers/src/wallet.rs +++ b/ethers-signers/src/wallet.rs @@ -144,6 +144,12 @@ impl Wallet { pub fn chain_id(&self) -> Option { self.chain_id } + + /// Returns the wallet's address + // (we duplicate this method + pub fn address(&self) -> Address { + self.address + } } impl From for Wallet { diff --git a/ethers-signers/tests/signer.rs b/ethers-signers/tests/signer.rs index 62a9735d..6c624de4 100644 --- a/ethers-signers/tests/signer.rs +++ b/ethers-signers/tests/signer.rs @@ -45,20 +45,14 @@ mod eth_tests { #[tokio::test] async fn send_eth() { - let port = 8545u64; - let url = format!("http://localhost:{}", port).to_string(); - let _ganache = Ganache::new() - .port(port) - .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") - .spawn(); + let ganache = Ganache::new().spawn(); // this private key belongs to the above mnemonic - let wallet: Wallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" - .parse() - .unwrap(); + let wallet: Wallet = ganache.keys()[0].clone().into(); + let wallet2: Wallet = ganache.keys()[1].clone().into(); // connect to the network - let provider = Provider::::try_from(url.as_str()) + let provider = Provider::::try_from(ganache.endpoint()) .unwrap() .interval(Duration::from_millis(10u64)); @@ -66,10 +60,7 @@ mod eth_tests { let client = wallet.connect(provider); // craft the transaction - let tx = TransactionRequest::new() - .send_to_str("986eE0C8B91A58e490Ee59718Cca41056Cf55f24") - .unwrap() - .value(10000); + let tx = TransactionRequest::new().to(wallet2.address()).value(10000); let balance_before = client.get_balance(client.address(), None).await.unwrap(); diff --git a/ethers/examples/contract.rs b/ethers/examples/contract.rs index f08e875f..5ba5b6b5 100644 --- a/ethers/examples/contract.rs +++ b/ethers/examples/contract.rs @@ -21,18 +21,14 @@ async fn main() -> Result<()> { .expect("could not find contract"); // 2. launch ganache - let port = 8546u64; - let url = format!("http://localhost:{}", port).to_string(); - let _ganache = Ganache::new().port(port) - .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") - .spawn(); + let ganache = Ganache::new().spawn(); // 3. instantiate our wallet - let wallet = - "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc".parse::()?; + let wallet: Wallet = ganache.keys()[0].clone().into(); // 4. connect to the network - let provider = Provider::::try_from(url.as_str())?.interval(Duration::from_millis(10u64)); + let provider = + Provider::::try_from(ganache.endpoint())?.interval(Duration::from_millis(10u64)); // 5. instantiate the client with the wallet let client = wallet.connect(provider); diff --git a/ethers/examples/local_signer.rs b/ethers/examples/local_signer.rs index ba583a05..c4d09091 100644 --- a/ethers/examples/local_signer.rs +++ b/ethers/examples/local_signer.rs @@ -4,27 +4,19 @@ use std::convert::TryFrom; #[tokio::main] async fn main() -> Result<()> { - let port = 8545u64; - let url = format!("http://localhost:{}", port).to_string(); - let _ganache = Ganache::new() - .port(port) - .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") - .spawn(); + let ganache = Ganache::new().spawn(); - // this private key belongs to the above mnemonic - let wallet: Wallet = - "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc".parse()?; + let wallet: Wallet = ganache.keys()[0].clone().into(); + let wallet2: Wallet = ganache.keys()[1].clone().into(); // connect to the network - let provider = Provider::::try_from(url.as_str())?; + let provider = Provider::::try_from(ganache.endpoint())?; // connect the wallet to the provider let client = wallet.connect(provider); // craft the transaction - let tx = TransactionRequest::new() - .send_to_str("986eE0C8B91A58e490Ee59718Cca41056Cf55f24")? - .value(10000); + let tx = TransactionRequest::new().to(wallet2.address()).value(10000); // send it! let tx_hash = client.send_transaction(tx, None).await?; diff --git a/ethers/examples/transfer_eth.rs b/ethers/examples/transfer_eth.rs index b02dc414..2080509a 100644 --- a/ethers/examples/transfer_eth.rs +++ b/ethers/examples/transfer_eth.rs @@ -4,20 +4,16 @@ use std::convert::TryFrom; #[tokio::main] async fn main() -> Result<()> { - let port = 8546u64; - let url = format!("http://localhost:{}", port).to_string(); - let _ganache = Ganache::new().port(port).spawn(); + let ganache = Ganache::new().spawn(); // connect to the network - let provider = Provider::::try_from(url.as_str())?; + let provider = Provider::::try_from(ganache.endpoint())?; let accounts = provider.get_accounts().await?; let from = accounts[0]; + let to = accounts[1]; // craft the tx - let tx = TransactionRequest::new() - .send_to_str("9A7e5d4bcA656182e66e33340d776D1542143006")? - .value(1000) - .from(from); // specify the `from` field so that the client knows which account to use + let tx = TransactionRequest::new().to(to).value(1000).from(from); // specify the `from` field so that the client knows which account to use let balance_before = provider.get_balance(from, None).await?;