diff --git a/Cargo.lock b/Cargo.lock index efa16188..cd08ff04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -341,6 +341,7 @@ dependencies = [ "bincode", "ethabi", "ethereum-types", + "ethers", "glob", "libsecp256k1", "rand", @@ -357,6 +358,7 @@ name = "ethers-providers" version = "0.1.0" dependencies = [ "async-trait", + "ethers", "ethers-core", "futures-core", "futures-util", @@ -374,6 +376,7 @@ dependencies = [ name = "ethers-signers" version = "0.1.0" dependencies = [ + "ethers", "ethers-core", "ethers-providers", "futures-util", diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml index f03e935d..7bd656d9 100644 --- a/ethers-core/Cargo.toml +++ b/ethers-core/Cargo.toml @@ -25,6 +25,7 @@ arrayvec = { version = "0.5.1", default-features = false, optional = true } glob = "0.3.0" [dev-dependencies] +ethers = { version = "0.1.0", path = "../ethers" } serde_json = { version = "1.0.53", default-features = false } bincode = "1.2.1" diff --git a/ethers-core/src/types/chainstate/transaction.rs b/ethers-core/src/types/chainstate/transaction.rs index fb906360..ce7d990e 100644 --- a/ethers-core/src/types/chainstate/transaction.rs +++ b/ethers-core/src/types/chainstate/transaction.rs @@ -8,6 +8,12 @@ use rlp::RlpStream; use serde::{Deserialize, Serialize}; use std::str::FromStr; +// Number of tx fields before signing +const UNSIGNED_TX_FIELDS: usize = 6; + +// Unsigned fields + signature [r s v] +const SIGNED_TX_FIELDS: usize = UNSIGNED_TX_FIELDS + 3; + /// Parameters for sending a transaction #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)] pub struct TransactionRequest { @@ -113,14 +119,30 @@ impl TransactionRequest { } /// Hashes the transaction's data with the provided chain id - pub fn hash>(&self, chain_id: Option) -> H256 { + pub fn sighash>(&self, chain_id: Option) -> H256 { let mut rlp = RlpStream::new(); - rlp.begin_list(9); + // "If [..] CHAIN_ID is available, then when computing the hash of a + // transaction for the purposes of signing, instead of hashing only + // six rlp encoded elements (nonce, gasprice, startgas, to, value, data), + // you SHOULD hash nine rlp encoded elements + // (nonce, gasprice, startgas, to, value, data, chainid, 0, 0)" + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md#specification + let num_els = if chain_id.is_some() { + UNSIGNED_TX_FIELDS + 3 + } else { + UNSIGNED_TX_FIELDS + }; + + rlp.begin_list(num_els); self.rlp_base(&mut rlp); - rlp.append(&chain_id.map(|c| c.into()).unwrap_or_else(U64::zero)); - rlp.append(&0u8); - rlp.append(&0u8); + // Only hash the 3 extra fields when preparing the + // data to sign if chain_id is present + if let Some(chain_id) = chain_id { + rlp.append(&chain_id.into()); + rlp.append(&0u8); + rlp.append(&0u8); + } keccak256(rlp.out().as_ref()).into() } @@ -128,7 +150,7 @@ impl TransactionRequest { /// Produces the RLP encoding of the transaction with the provided signature pub fn rlp_signed(&self, signature: &Signature) -> Bytes { let mut rlp = RlpStream::new(); - rlp.begin_list(9); + rlp.begin_list(SIGNED_TX_FIELDS); self.rlp_base(&mut rlp); rlp.append(&signature.v); @@ -217,7 +239,7 @@ impl Transaction { pub fn rlp(&self) -> Bytes { let mut rlp = RlpStream::new(); - rlp.begin_list(9); + rlp.begin_list(SIGNED_TX_FIELDS); rlp.append(&self.nonce); rlp.append(&self.gas_price); rlp.append(&self.gas); diff --git a/ethers-core/src/types/crypto/keys.rs b/ethers-core/src/types/crypto/keys.rs index 5dafc41b..1e66b9ad 100644 --- a/ethers-core/src/types/crypto/keys.rs +++ b/ethers-core/src/types/crypto/keys.rs @@ -120,13 +120,15 @@ impl PrivateKey { let gas_price = tx.gas_price.ok_or(TxError::GasPriceMissing)?; let gas = tx.gas.ok_or(TxError::GasMissing)?; - // Hash the transaction's RLP encoding - let hash = tx.hash(chain_id); + // Get the transaction's sighash + let sighash = tx.sighash(chain_id); let message = - Message::parse_slice(hash.as_bytes()).expect("hash is non-zero 32-bytes; qed"); + Message::parse_slice(sighash.as_bytes()).expect("hash is non-zero 32-bytes; qed"); // Sign it (with replay protection if applicable) let signature = self.sign_with_eip155(&message, chain_id); + + // Get the actual transaction hash let rlp = tx.rlp_signed(&signature); let hash = keccak256(&rlp.0); diff --git a/ethers-core/src/utils/ganache.rs b/ethers-core/src/utils/ganache.rs index fd2a2594..9f9ee61a 100644 --- a/ethers-core/src/utils/ganache.rs +++ b/ethers-core/src/utils/ganache.rs @@ -25,7 +25,7 @@ impl Drop for GanacheInstance { /// # Example /// /// ```no_run -/// use ethers_core::utils::Ganache; +/// use ethers::utils::Ganache; /// /// let port = 8545u64; /// let url = format!("http://localhost:{}", port).to_string(); @@ -40,6 +40,7 @@ impl Drop for GanacheInstance { #[derive(Clone, Default)] pub struct Ganache { port: Option, + block_time: Option, mnemonic: Option, } @@ -62,6 +63,12 @@ impl Ganache { self } + /// Sets the block-time which will be used when the `ganache-cli` instance is launched. + pub fn block_time>(mut self, block_time: T) -> Self { + self.block_time = Some(block_time.into()); + self + } + /// Consumes the builder and spawns `ganache-cli` with stdout redirected /// to /dev/null. This takes ~2 seconds to execute as it blocks while /// waiting for `ganache-cli` to launch. @@ -76,6 +83,10 @@ impl Ganache { cmd.arg("-m").arg(mnemonic); } + if let Some(block_time) = self.block_time { + cmd.arg("-b").arg(block_time.to_string()); + } + let ganache_pid = cmd.spawn().expect("couldnt start ganache-cli"); // wait a couple of seconds for ganache to boot up diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs index e874dc39..bb5356f2 100644 --- a/ethers-core/src/utils/mod.rs +++ b/ethers-core/src/utils/mod.rs @@ -13,16 +13,17 @@ pub use hash::{hash_message, id, keccak256, serialize}; pub use rlp; use crate::types::{Address, Bytes, U256}; +use std::convert::TryInto; /// 1 Ether = 1e18 Wei -pub const WEI: usize = 1000000000000000000; +pub const WEI_IN_ETHER: usize = 1000000000000000000; /// Format the output for the user which prefer to see values /// in ether (instead of wei) /// /// Divides the input by 1e18 pub fn format_ether>(amount: T) -> U256 { - amount.into() / WEI + amount.into() / WEI_IN_ETHER } /// Divides with the number of decimals @@ -30,16 +31,28 @@ pub fn format_units>(amount: T, decimals: usize) -> U256 { amount.into() / decimals } -/// Converts a string to a U256 and converts from Ether to Wei. +/// Converts the input to a U256 and converts from Ether to Wei. /// -/// Multiplies the input by 1e18 -pub fn parse_ether(eth: &str) -> Result { - Ok(eth.parse::()? * WEI) +/// ``` +/// use ethers::{types::U256, utils::{parse_ether, WEI_IN_ETHER}}; +/// +/// let eth = U256::from(WEI_IN_ETHER); +/// assert_eq!(eth, parse_ether(1u8).unwrap()); +/// assert_eq!(eth, parse_ether(1usize).unwrap()); +/// assert_eq!(eth, parse_ether("1").unwrap()); +pub fn parse_ether(eth: S) -> Result +where + S: TryInto, +{ + Ok(eth.try_into()? * WEI_IN_ETHER) } /// Multiplies with the number of decimals -pub fn parse_units(eth: &str, decimals: usize) -> Result { - Ok(eth.parse::()? * decimals) +pub fn parse_units(eth: S, decimals: usize) -> Result +where + S: TryInto, +{ + Ok(eth.try_into()? * decimals) } /// The address for an Ethereum contract is deterministically computed from the diff --git a/ethers-core/src/utils/solc.rs b/ethers-core/src/utils/solc.rs index 71adecee..56288579 100644 --- a/ethers-core/src/utils/solc.rs +++ b/ethers-core/src/utils/solc.rs @@ -39,7 +39,7 @@ pub struct CompiledContract { /// # Examples /// /// ```no_run -/// use ethers_core::utils::Solc; +/// use ethers::utils::Solc; /// /// # fn main() -> Result<(), Box> { /// // Give it a glob diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml index e65ad4aa..ea102572 100644 --- a/ethers-providers/Cargo.toml +++ b/ethers-providers/Cargo.toml @@ -21,5 +21,7 @@ pin-project = { version = "0.4.20", default-features = false } tokio = { version = "0.2.21", default-features = false, features = ["time"] } [dev-dependencies] +ethers = { version = "0.1.0", path = "../ethers" } + rustc-hex = "2.1.0" tokio = { version = "0.2.21", default-features = false, features = ["rt-core", "macros"] } diff --git a/ethers-providers/src/http.rs b/ethers-providers/src/http.rs index 2f17ad03..d72a0ca7 100644 --- a/ethers-providers/src/http.rs +++ b/ethers-providers/src/http.rs @@ -17,8 +17,7 @@ use url::Url; /// # Example /// /// ```no_run -/// use ethers_providers::{JsonRpcClient, Http}; -/// use ethers_core::types::U64; +/// use ethers::{types::U64, providers::{JsonRpcClient, Http}}; /// use std::str::FromStr; /// /// # async fn foo() -> Result<(), Box> { @@ -85,7 +84,7 @@ impl Provider { /// # Example /// /// ``` - /// use ethers_providers::Http; + /// use ethers::providers::Http; /// use url::Url; /// /// let url = Url::parse("http://localhost:8545").unwrap(); diff --git a/ethers-providers/src/pending_transaction.rs b/ethers-providers/src/pending_transaction.rs index 23d3d622..78db7ff5 100644 --- a/ethers-providers/src/pending_transaction.rs +++ b/ethers-providers/src/pending_transaction.rs @@ -50,8 +50,12 @@ impl<'a, P: JsonRpcClient> Future for PendingTransaction<'a, P> { match this.state { PendingTxState::GettingReceipt(fut) => { - let receipt = futures_util::ready!(fut.as_mut().poll(ctx))?; - *this.state = PendingTxState::CheckingReceipt(Box::new(receipt)) + if let Ok(receipt) = futures_util::ready!(fut.as_mut().poll(ctx)) { + *this.state = PendingTxState::CheckingReceipt(Box::new(receipt)) + } else { + let fut = Box::pin(this.provider.get_transaction_receipt(*this.tx_hash)); + *this.state = PendingTxState::GettingReceipt(fut) + } } PendingTxState::CheckingReceipt(receipt) => { // If we requested more than 1 confirmation, we need to compare the receipt's @@ -59,7 +63,10 @@ impl<'a, P: JsonRpcClient> Future for PendingTransaction<'a, P> { if *this.confirmations > 1 { let fut = Box::pin(this.provider.get_block_number()); *this.state = - PendingTxState::GettingBlockNumber(fut, Box::new(*receipt.clone())) + PendingTxState::GettingBlockNumber(fut, Box::new(*receipt.clone())); + + // Schedule the waker to poll again + ctx.waker().wake_by_ref(); } else { let receipt = *receipt.clone(); *this.state = PendingTxState::Completed; @@ -162,30 +169,3 @@ impl<'a> fmt::Debug for PendingTxState<'a> { .finish() } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::Http; - use ethers_core::{types::TransactionRequest, utils::Ganache}; - use std::convert::TryFrom; - - #[tokio::test] - async fn test_pending_tx() { - let _ganache = Ganache::new().spawn(); - let provider = Provider::::try_from("http://localhost:8545").unwrap(); - let accounts = provider.get_accounts().await.unwrap(); - let tx = TransactionRequest::pay(accounts[0], 1000).from(accounts[0]); - - let pending_tx = provider.send_transaction(tx).await.unwrap(); - - let receipt = provider - .get_transaction_receipt(pending_tx.tx_hash) - .await - .unwrap(); - - // the pending tx resolves to the same receipt - let tx_receipt = pending_tx.confirmations(1).await.unwrap(); - assert_eq!(receipt, tx_receipt); - } -} diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs index 9b471bb8..194a0076 100644 --- a/ethers-providers/src/provider.rs +++ b/ethers-providers/src/provider.rs @@ -28,8 +28,7 @@ use std::{convert::TryFrom, fmt::Debug}; /// # Example /// /// ```no_run -/// # use ethers_providers::JsonRpcClient; -/// use ethers_providers::{Provider, Http}; +/// use ethers::providers::{JsonRpcClient, Provider, Http}; /// use std::convert::TryFrom; /// /// let provider = Provider::::try_from( diff --git a/ethers-providers/tests/provider.rs b/ethers-providers/tests/provider.rs new file mode 100644 index 00000000..9fcf11fc --- /dev/null +++ b/ethers-providers/tests/provider.rs @@ -0,0 +1,21 @@ +use ethers::{ + providers::{Http, Provider}, + types::TransactionRequest, + utils::{parse_ether, Ganache}, +}; +use std::convert::TryFrom; + +#[tokio::test] +async fn pending_txs_with_confirmations_ganache() { + let _ganache = Ganache::new().block_time(2u64).spawn(); + let provider = Provider::::try_from("http://localhost:8545").unwrap(); + let accounts = provider.get_accounts().await.unwrap(); + + let tx = TransactionRequest::pay(accounts[1], parse_ether(1u64).unwrap()).from(accounts[0]); + let pending_tx = provider.send_transaction(tx).await.unwrap(); + let hash = *pending_tx; + let receipt = pending_tx.confirmations(5).await.unwrap(); + + // got the correct receipt + assert_eq!(receipt.transaction_hash, hash); +} diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml index 1b44e020..b2dd29f0 100644 --- a/ethers-signers/Cargo.toml +++ b/ethers-signers/Cargo.toml @@ -12,4 +12,6 @@ futures-util = { version = "0.3.5", default-features = false } serde = "1.0.112" [dev-dependencies] +ethers = { version = "0.1.0", path = "../ethers" } + tokio = { version = "0.2.21", features = ["macros"] } diff --git a/ethers-signers/src/wallet.rs b/ethers-signers/src/wallet.rs index a76aeaad..2ea2e8b9 100644 --- a/ethers-signers/src/wallet.rs +++ b/ethers-signers/src/wallet.rs @@ -72,7 +72,7 @@ pub struct Wallet { /// The wallet's address address: Address, /// The wallet's chain id (for EIP-155), signs w/o replay protection if left unset - chain_id: u64, + chain_id: Option, } impl Signer for Wallet { @@ -83,7 +83,7 @@ impl Signer for Wallet { } fn sign_transaction(&self, tx: TransactionRequest) -> Result { - self.private_key.sign_transaction(tx, Some(self.chain_id)) + self.private_key.sign_transaction(tx, self.chain_id) } fn address(&self) -> Address { @@ -110,7 +110,7 @@ impl Wallet { private_key, public_key, address, - chain_id: 1, + chain_id: None, } } @@ -126,7 +126,7 @@ impl Wallet { /// Sets the wallet's chain_id, used in conjunction with EIP-155 signing pub fn set_chain_id>(mut self, chain_id: T) -> Self { - self.chain_id = chain_id.into(); + self.chain_id = Some(chain_id.into()); self } @@ -141,7 +141,7 @@ impl Wallet { } /// Gets the wallet's chain id - pub fn chain_id(&self) -> u64 { + pub fn chain_id(&self) -> Option { self.chain_id } } @@ -155,7 +155,7 @@ impl From for Wallet { private_key, public_key, address, - chain_id: 1, + chain_id: None, } } } diff --git a/ethers-signers/tests/send_eth.rs b/ethers-signers/tests/send_eth.rs index 43d7d53d..7edfc62c 100644 --- a/ethers-signers/tests/send_eth.rs +++ b/ethers-signers/tests/send_eth.rs @@ -1,8 +1,34 @@ -use ethers_core::{types::TransactionRequest, utils::Ganache}; -use ethers_providers::{Http, Provider}; -use ethers_signers::Wallet; +use ethers::{ + providers::{Http, Provider}, + signers::Wallet, + types::TransactionRequest, + utils::{parse_ether, Ganache}, +}; use std::convert::TryFrom; +#[tokio::test] +async fn pending_txs_with_confirmations_rinkeby_infura() { + let provider = + Provider::::try_from("https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27") + .unwrap(); + + // pls do not drain this key :) + // note: this works even if there's no EIP-155 configured! + let client = "FF7F80C6E9941865266ED1F481263D780169F1D98269C51167D20C630A5FDC8A" + .parse::() + .unwrap() + .connect(provider); + + let tx = TransactionRequest::pay(client.address(), parse_ether(1u64).unwrap()); + let pending_tx = client.send_transaction(tx, None).await.unwrap(); + let hash = *pending_tx; + dbg!(hash); + let receipt = pending_tx.confirmations(3).await.unwrap(); + + // got the correct receipt + assert_eq!(receipt.transaction_hash, hash); +} + #[tokio::test] async fn send_eth() { let port = 8545u64;