Improve Ganache Flexibility (#37)

* feat(core): add more features to ganache

* test(provider): choose endpoint dynamically

* test(signer): choose endpoint and accounts dynamically

* test(contract): choose endpoint and accounts dynamically

* fix: dynamic port / accounts in examples

* core(chore): fix doctest
This commit is contained in:
Georgios Konstantopoulos 2020-06-22 16:42:34 +03:00 committed by GitHub
parent 1cfbc7b3c3
commit bb1ac9c666
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 173 additions and 178 deletions

72
Cargo.lock generated
View File

@ -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"

View File

@ -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"]

View File

@ -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<Client<Http, Wallet>> {
let provider = Provider::<Http>::try_from("http://localhost:8545")
.unwrap()
.interval(Duration::from_millis(10u64));
Arc::new(private_key.parse::<Wallet>().unwrap().connect(provider))
pub fn connect(ganache: &GanacheInstance, idx: usize) -> Arc<Client<Http, Wallet>> {
let provider = Provider::<Http>::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<Client<Http, Wallet>>,
abi: Abi,
bytecode: Bytes,
) -> (GanacheInstance, Contract<Http, Wallet>) {
let ganache = Ganache::new()
.mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle")
.spawn();
) -> Contract<Http, Wallet> {
let factory = ContractFactory::new(abi, bytecode, client);
let contract = factory
factory
.deploy("initial value".to_string())
.unwrap()
.send()
.await
.unwrap();
(ganache, contract)
.unwrap()
}

View File

@ -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::<Http>::try_from("http://localhost:8545")
// spawn ganache
let ganache = Ganache::new().spawn();
// connect
let provider = Provider::<Http>::try_from(ganache.endpoint())
.unwrap()
.interval(Duration::from_millis(10u64));
let deployer = "3cDB3d9e1B74692Bb1E3bb5fc81938151cA64b02"
.parse::<Address>()
.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

View File

@ -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<PrivateKey>,
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<u64>,
port: Option<u16>,
block_time: Option<u64>,
mnemonic: Option<String>,
}
@ -52,7 +82,7 @@ impl Ganache {
}
/// Sets the port which will be used when the `ganache-cli` instance is launched.
pub fn port<T: Into<u64>>(mut self, port: T) -> Self {
pub fn port<T: Into<u16>>(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()
}

View File

@ -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 = [

View File

@ -534,6 +534,14 @@ impl TryFrom<&str> for Provider<HttpProvider> {
}
}
impl TryFrom<String> for Provider<HttpProvider> {
type Error = ParseError;
fn try_from(src: String) -> Result<Self, Self::Error> {
Provider::try_from(src.as_str())
}
}
#[cfg(test)]
mod ens_tests {
use super::*;

View File

@ -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::<Http>::try_from("http://localhost:8545")
let ganache = Ganache::new().block_time(2u64).spawn();
let provider = Provider::<Http>::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;
}

View File

@ -144,6 +144,12 @@ impl Wallet {
pub fn chain_id(&self) -> Option<u64> {
self.chain_id
}
/// Returns the wallet's address
// (we duplicate this method
pub fn address(&self) -> Address {
self.address
}
}
impl From<PrivateKey> for Wallet {

View File

@ -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::<Http>::try_from(url.as_str())
let provider = Provider::<Http>::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();

View File

@ -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::<Wallet>()?;
let wallet: Wallet = ganache.keys()[0].clone().into();
// 4. connect to the network
let provider = Provider::<Http>::try_from(url.as_str())?.interval(Duration::from_millis(10u64));
let provider =
Provider::<Http>::try_from(ganache.endpoint())?.interval(Duration::from_millis(10u64));
// 5. instantiate the client with the wallet
let client = wallet.connect(provider);

View File

@ -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::<Http>::try_from(url.as_str())?;
let provider = Provider::<Http>::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?;

View File

@ -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::<Http>::try_from(url.as_str())?;
let provider = Provider::<Http>::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?;