diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef467b52..dcfeb989 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - name: Install ganache run: npm install -g ganache-cli - name: Install libusb (for Ledger) - run: sudo apt install pkg-config libudev-dev + run: sudo apt update && sudo apt install pkg-config libudev-dev - name: Install Solc run: | diff --git a/ethers-middleware/tests/stack.rs b/ethers-middleware/tests/stack.rs index 915e42ce..86339373 100644 --- a/ethers-middleware/tests/stack.rs +++ b/ethers-middleware/tests/stack.rs @@ -1,7 +1,6 @@ -#[tokio::test] #[cfg(not(feature = "celo"))] -async fn can_stack_middlewares() { - use ethers_core::{types::TransactionRequest, utils::Ganache}; +mod tests { + use ethers_core::{rand::thread_rng, types::TransactionRequest, utils::Ganache}; use ethers_middleware::{ gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice}, gas_oracle::{GasCategory, GasNow, GasOracleMiddleware}, @@ -12,49 +11,90 @@ async fn can_stack_middlewares() { use ethers_signers::LocalWallet; use std::convert::TryFrom; - let ganache = Ganache::new().block_time(5u64).spawn(); - let gas_oracle = GasNow::new().category(GasCategory::SafeLow); - let signer: LocalWallet = ganache.keys()[0].clone().into(); - let address = signer.address(); + #[tokio::test] + async fn mock_with_middleware() { + let (provider, mock) = Provider::mocked(); - // the base provider - let provider = Provider::::try_from(ganache.endpoint()).unwrap(); - let provider_clone = provider.clone(); + // add a bunch of middlewares + let gas_oracle = GasNow::new().category(GasCategory::SafeLow); + let signer = LocalWallet::new(&mut thread_rng()); + let address = signer.address(); + let escalator = GeometricGasPrice::new(1.125, 60u64, None::); + let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock); + let provider = GasOracleMiddleware::new(provider, gas_oracle); + let provider = SignerMiddleware::new(provider, signer); + let provider = NonceManagerMiddleware::new(provider, address); - // the Gas Price escalator middleware is the first middleware above the provider, - // so that it receives the transaction last, after all the other middleware - // have modified it accordingly - let escalator = GeometricGasPrice::new(1.125, 60u64, None::); - let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock); + // push a response + use ethers_core::types::U64; + mock.push(U64::from(12u64)).unwrap(); + let blk = provider.get_block_number().await.unwrap(); + assert_eq!(blk.as_u64(), 12); - // The gas price middleware MUST be below the signing middleware for things to work - let provider = GasOracleMiddleware::new(provider, gas_oracle); + // now that the response is gone, there's nothing left + // TODO: This returns: + // MiddlewareError( + // MiddlewareError( + // MiddlewareError( + // MiddlewareError( + // JsonRpcClientError(EmptyResponses) + // )))) + // Can we flatten it in any way? Maybe inherent to the middleware + // infrastructure + provider.get_block_number().await.unwrap_err(); - // The signing middleware signs txs - let provider = SignerMiddleware::new(provider, signer); - - // The nonce manager middleware MUST be above the signing middleware so that it overrides - // the nonce and the signer does not make any eth_getTransaction count calls - let provider = NonceManagerMiddleware::new(provider, address); - - let tx = TransactionRequest::new(); - let mut tx_hash = None; - for _ in 0..10 { - tx_hash = Some(provider.send_transaction(tx.clone(), None).await.unwrap()); - dbg!( - provider - .get_transaction(tx_hash.unwrap()) - .await - .unwrap() - .unwrap() - .gas_price - ); + // 2 calls were made + mock.assert_request("eth_blockNumber", ()).unwrap(); + mock.assert_request("eth_blockNumber", ()).unwrap(); + mock.assert_request("eth_blockNumber", ()).unwrap_err(); } - let receipt = provider_clone - .pending_transaction(tx_hash.unwrap()) - .await - .unwrap(); + #[tokio::test] + async fn can_stack_middlewares() { + let ganache = Ganache::new().block_time(5u64).spawn(); + let gas_oracle = GasNow::new().category(GasCategory::SafeLow); + let signer: LocalWallet = ganache.keys()[0].clone().into(); + let address = signer.address(); - dbg!(receipt); + // the base provider + let provider = Provider::::try_from(ganache.endpoint()).unwrap(); + let provider_clone = provider.clone(); + + // the Gas Price escalator middleware is the first middleware above the provider, + // so that it receives the transaction last, after all the other middleware + // have modified it accordingly + let escalator = GeometricGasPrice::new(1.125, 60u64, None::); + let provider = GasEscalatorMiddleware::new(provider, escalator, Frequency::PerBlock); + + // The gas price middleware MUST be below the signing middleware for things to work + let provider = GasOracleMiddleware::new(provider, gas_oracle); + + // The signing middleware signs txs + let provider = SignerMiddleware::new(provider, signer); + + // The nonce manager middleware MUST be above the signing middleware so that it overrides + // the nonce and the signer does not make any eth_getTransaction count calls + let provider = NonceManagerMiddleware::new(provider, address); + + let tx = TransactionRequest::new(); + let mut tx_hash = None; + for _ in 0..10 { + tx_hash = Some(provider.send_transaction(tx.clone(), None).await.unwrap()); + dbg!( + provider + .get_transaction(tx_hash.unwrap()) + .await + .unwrap() + .unwrap() + .gas_price + ); + } + + let receipt = provider_clone + .pending_transaction(tx_hash.unwrap()) + .await + .unwrap(); + + dbg!(receipt); + } } diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs index 361fea41..78e8c5e6 100644 --- a/ethers-providers/src/provider.rs +++ b/ethers-providers/src/provider.rs @@ -1,7 +1,7 @@ use crate::{ ens, stream::{FilterWatcher, DEFAULT_POLL_INTERVAL}, - FromErr, Http as HttpProvider, JsonRpcClient, PendingTransaction, + FromErr, Http as HttpProvider, JsonRpcClient, MockProvider, PendingTransaction, }; use ethers_core::{ @@ -47,6 +47,12 @@ use std::{convert::TryFrom, fmt::Debug, time::Duration}; // TODO: Convert to proper struct pub struct Provider

(P, Option

, Option, Option
); +impl

AsRef

for Provider

{ + fn as_ref(&self) -> &P { + &self.0 + } +} + impl FromErr for ProviderError { fn from(src: ProviderError) -> Self { src @@ -716,6 +722,34 @@ impl Provider

{ } } +impl Provider { + /// Returns a `Provider` instantiated with an internal "mock" transport. + /// + /// # Example + /// + /// ``` + /// # async fn foo() -> Result<(), Box> { + /// use ethers::{types::U64, providers::{Middleware, Provider}}; + /// // Instantiate the provider + /// let (provider, mock) = Provider::mocked(); + /// // Push the mock response + /// mock.push(U64::from(12))?; + /// // Make the call + /// let blk = provider.get_block_number().await.unwrap(); + /// // The response matches + /// assert_eq!(blk.as_u64(), 12); + /// // and the request as well! + /// mock.assert_request("eth_blockNumber", ()).unwrap(); + /// # Ok(()) + /// # } + /// ``` + pub fn mocked() -> (Self, MockProvider) { + let mock = MockProvider::new(); + let mock_clone = mock.clone(); + (Self::new(mock), mock_clone) + } +} + /// infallbile conversion of Bytes to Address/String /// /// # Panics diff --git a/ethers-providers/src/transports/mock.rs b/ethers-providers/src/transports/mock.rs new file mode 100644 index 00000000..b61ecd09 --- /dev/null +++ b/ethers-providers/src/transports/mock.rs @@ -0,0 +1,153 @@ +use crate::{JsonRpcClient, ProviderError}; + +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::{ + borrow::Borrow, + collections::VecDeque, + sync::{Arc, Mutex}, +}; +use thiserror::Error; + +#[derive(Clone, Debug)] +/// Mock transport used in test environments. +pub struct MockProvider { + requests: Arc>>, + responses: Arc>>, +} + +impl Default for MockProvider { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl JsonRpcClient for MockProvider { + type Error = MockError; + + /// Pushes the `(method, input)` to the back of the `requests` queue, + /// pops the responses from the back of the `responses` queue + async fn request Deserialize<'a>>( + &self, + method: &str, + input: T, + ) -> Result { + self.requests + .lock() + .unwrap() + .push_back((method.to_owned(), serde_json::to_value(input)?)); + let mut data = self.responses.lock().unwrap(); + let element = data.pop_back().ok_or(MockError::EmptyResponses)?; + let res: R = serde_json::from_value(element)?; + + Ok(res) + } +} + +impl MockProvider { + /// Checks that the provided request was submitted by the client + pub fn assert_request( + &self, + method: &str, + data: T, + ) -> Result<(), MockError> { + let (m, inp) = self + .requests + .lock() + .unwrap() + .pop_front() + .ok_or(MockError::EmptyRequests)?; + assert_eq!(m, method); + assert_eq!( + serde_json::to_value(data).expect("could not serialize data"), + inp + ); + Ok(()) + } + + /// Instantiates a mock transport + pub fn new() -> Self { + Self { + requests: Arc::new(Mutex::new(VecDeque::new())), + responses: Arc::new(Mutex::new(VecDeque::new())), + } + } + + /// Pushes the data to the responses + pub fn push>(&self, data: K) -> Result<(), MockError> { + let value = serde_json::to_value(data.borrow())?; + self.responses.lock().unwrap().push_back(value); + Ok(()) + } +} + +#[derive(Error, Debug)] +/// Errors for the `MockProvider` +pub enum MockError { + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), + + #[error("empty responses array, please push some requests")] + EmptyRequests, + + #[error("empty responses array, please push some responses")] + EmptyResponses, +} + +impl From for ProviderError { + fn from(src: MockError) -> Self { + ProviderError::JsonRpcClientError(Box::new(src)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Middleware; + use ethers_core::types::U64; + + #[tokio::test] + async fn pushes_request_and_response() { + let mock = MockProvider::new(); + mock.push(U64::from(12)).unwrap(); + let block: U64 = mock.request("eth_blockNumber", ()).await.unwrap(); + mock.assert_request("eth_blockNumber", ()).unwrap(); + assert_eq!(block.as_u64(), 12); + } + + #[tokio::test] + async fn empty_responses() { + let mock = MockProvider::new(); + // tries to get a response without pushing a response + let err = mock + .request::<_, ()>("eth_blockNumber", ()) + .await + .unwrap_err(); + match err { + MockError::EmptyResponses => {} + _ => panic!("expected empty responses"), + }; + } + + #[tokio::test] + async fn empty_requests() { + let mock = MockProvider::new(); + // tries to assert a request without making one + let err = mock.assert_request("eth_blockNumber", ()).unwrap_err(); + match err { + MockError::EmptyRequests => {} + _ => panic!("expected empty request"), + }; + } + + #[tokio::test] + async fn composes_with_provider() { + let (provider, mock) = crate::Provider::mocked(); + + mock.push(U64::from(12)).unwrap(); + let block = provider.get_block_number().await.unwrap(); + assert_eq!(block.as_u64(), 12); + } +} diff --git a/ethers-providers/src/transports/mod.rs b/ethers-providers/src/transports/mod.rs index 26d4bf00..3e46b58d 100644 --- a/ethers-providers/src/transports/mod.rs +++ b/ethers-providers/src/transports/mod.rs @@ -7,3 +7,6 @@ pub use http::Provider as Http; mod ws; #[cfg(feature = "ws")] pub use ws::Provider as Ws; + +mod mock; +pub use mock::{MockError, MockProvider};