diff --git a/CHANGELOG.md b/CHANGELOG.md index a9002f81..b34a53d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -130,6 +130,13 @@ ## ethers-providers +### Unreleased + +- Add support for basic and bearer authentication in http and non-wasm websockets. + [829](https://github.com/gakonst/ethers-rs/pull/829) +- Export `ethers_providers::IpcError` and `ethers_providers::QuorumError` + [1012](https://github.com/gakonst/ethers-rs/pull/1012) + ### 0.6.0 - re-export error types for `Http` and `Ws` providers in @@ -144,11 +151,6 @@ - Add support for `evm_snapshot` and `evm_revert` dev RPC methods. [640](https://github.com/gakonst/ethers-rs/pull/640) -### Unreleased - -- Add support for basic and bearer authentication in http and non-wasm websockets. - [829](https://github.com/gakonst/ethers-rs/pull/829) - ### 0.5.3 - Expose `ens` module [#435](https://github.com/gakonst/ethers-rs/pull/435) diff --git a/Cargo.lock b/Cargo.lock index 8d09a912..d5d0faa4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1322,6 +1322,7 @@ dependencies = [ "futures-util", "hex", "http", + "once_cell", "parking_lot", "pin-project", "reqwest", @@ -2666,9 +2667,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "pretty_assertions" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d5b548b725018ab5496482b45cb8bef21e9fed1858a6d674e3a8a0f0bb5d50" +checksum = "57c038cb5319b9c704bf9c227c261d275bfec0ad438118a2787ce47944fb228b" dependencies = [ "ansi_term", "ctor", @@ -3575,7 +3576,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "svm-rs" version = "0.2.9" -source = "git+https://github.com/roynalnaruto/svm-rs#8e33f55fa2a2afb937749e31b2ffa42600bfe216" +source = "git+https://github.com/roynalnaruto/svm-rs#ae79a29f5bde08f1991f981456253fa5b6859047" dependencies = [ "anyhow", "cfg-if 1.0.0", diff --git a/ethers-contract/ethers-contract-abigen/src/multi.rs b/ethers-contract/ethers-contract-abigen/src/multi.rs index 7d35eef5..e57f10f8 100644 --- a/ethers-contract/ethers-contract-abigen/src/multi.rs +++ b/ethers-contract/ethers-contract-abigen/src/multi.rs @@ -709,8 +709,9 @@ fn check_file_in_dir(dir: &Path, file_name: &str, expected_contents: &[u8]) -> R let file_path = dir.join(file_name); eyre::ensure!(file_path.is_file(), "Not a file: {}", file_path.display()); - let contents = fs::read(file_path).expect("Unable to read file"); - eyre::ensure!(contents == expected_contents, "file contents do not match"); + let contents = fs::read(&file_path).expect("Unable to read file"); + eyre::ensure!(contents == expected_contents, format!("The contents of `{}` do not match the expected output of the newest `ethers::Abigen` version.\ +This indicates that the existing bindings are outdated and need to be generated again.", file_path.display())); Ok(()) } diff --git a/ethers-etherscan/src/contract.rs b/ethers-etherscan/src/contract.rs index 3f7885dd..a3d11146 100644 --- a/ethers-etherscan/src/contract.rs +++ b/ethers-etherscan/src/contract.rs @@ -105,6 +105,7 @@ impl VerifyContract { ) -> Self { self.constructor_arguments = constructor_arguments.map(|s| { s.into() + .trim() // TODO is this correct? .trim_start_matches("0x") .to_string() diff --git a/ethers-middleware/src/signer.rs b/ethers-middleware/src/signer.rs index 542df8ca..c4b72a2e 100644 --- a/ethers-middleware/src/signer.rs +++ b/ethers-middleware/src/signer.rs @@ -1,5 +1,6 @@ use ethers_core::types::{ - transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, Signature, + transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed}, + Address, BlockId, Bytes, Signature, U256, }; use ethers_providers::{maybe, FromErr, Middleware, PendingTransaction}; use ethers_signers::Signer; @@ -158,6 +159,14 @@ where this.signer = signer; this } + + fn set_tx_from_if_none(&self, tx: &TypedTransaction) -> TypedTransaction { + let mut tx = tx.clone(); + if tx.from().is_none() { + tx.set_from(self.address); + } + tx + } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -264,6 +273,32 @@ where ) -> Result { self.signer.sign_message(data.into()).await.map_err(SignerMiddlewareError::SignerError) } + + async fn estimate_gas(&self, tx: &TypedTransaction) -> Result { + let tx = self.set_tx_from_if_none(tx); + self.inner.estimate_gas(&tx).await.map_err(SignerMiddlewareError::MiddlewareError) + } + + async fn create_access_list( + &self, + tx: &TypedTransaction, + block: Option, + ) -> Result { + let tx = self.set_tx_from_if_none(tx); + self.inner + .create_access_list(&tx, block) + .await + .map_err(SignerMiddlewareError::MiddlewareError) + } + + async fn call( + &self, + tx: &TypedTransaction, + block: Option, + ) -> Result { + let tx = self.set_tx_from_if_none(tx); + self.inner().call(&tx, block).await.map_err(SignerMiddlewareError::MiddlewareError) + } } #[cfg(all(test, not(feature = "celo"), not(target_arch = "wasm32")))] diff --git a/ethers-middleware/tests/gas_escalator.rs b/ethers-middleware/tests/gas_escalator.rs index 9771a7bb..a9eff59c 100644 --- a/ethers-middleware/tests/gas_escalator.rs +++ b/ethers-middleware/tests/gas_escalator.rs @@ -4,7 +4,7 @@ use ethers_middleware::{ gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice}, signer::SignerMiddleware, }; -use ethers_providers::{Middleware, Provider, Ws}; +use ethers_providers::Middleware; use ethers_signers::{LocalWallet, Signer}; use std::time::Duration; @@ -12,10 +12,8 @@ use std::time::Duration; #[ignore] async fn gas_escalator_live() { // connect to ropsten for getting bad block times - let ws = Ws::connect("wss://ropsten.infura.io/ws/v3/fd8b88b56aa84f6da87b60f5441d6778") - .await - .unwrap(); - let provider = Provider::new(ws).interval(Duration::from_millis(2000u64)); + let provider = ethers_providers::ROPSTEN.ws().await; + let provider = provider.interval(Duration::from_millis(2000u64)); let wallet = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169" .parse::() .unwrap(); diff --git a/ethers-middleware/tests/nonce_manager.rs b/ethers-middleware/tests/nonce_manager.rs index 30b3fa03..5e0a23dc 100644 --- a/ethers-middleware/tests/nonce_manager.rs +++ b/ethers-middleware/tests/nonce_manager.rs @@ -4,14 +4,11 @@ async fn nonce_manager() { use ethers_core::types::*; use ethers_middleware::{nonce_manager::NonceManagerMiddleware, signer::SignerMiddleware}; - use ethers_providers::{Http, Middleware, Provider}; + use ethers_providers::Middleware; use ethers_signers::{LocalWallet, Signer}; - use std::{convert::TryFrom, time::Duration}; + use std::time::Duration; - let provider = - Provider::::try_from("https://rinkeby.infura.io/v3/fd8b88b56aa84f6da87b60f5441d6778") - .unwrap() - .interval(Duration::from_millis(2000u64)); + let provider = ethers_providers::RINKEBY.provider().interval(Duration::from_millis(2000u64)); let chain_id = provider.get_chainid().await.unwrap().as_u64(); let wallet = std::env::var("RINKEBY_PRIVATE_KEY") diff --git a/ethers-middleware/tests/signer.rs b/ethers-middleware/tests/signer.rs index e5cf3570..e2cca684 100644 --- a/ethers-middleware/tests/signer.rs +++ b/ethers-middleware/tests/signer.rs @@ -1,5 +1,5 @@ #![allow(unused)] -use ethers_providers::{Http, JsonRpcClient, Middleware, Provider}; +use ethers_providers::{Http, JsonRpcClient, Middleware, Provider, RINKEBY}; use ethers_core::{ types::{BlockNumber, TransactionRequest}, @@ -8,7 +8,7 @@ use ethers_core::{ use ethers_middleware::signer::SignerMiddleware; use ethers_signers::{coins_bip39::English, LocalWallet, MnemonicBuilder, Signer}; use once_cell::sync::Lazy; -use std::{convert::TryFrom, sync::atomic::AtomicU8, time::Duration}; +use std::{convert::TryFrom, iter::Cycle, sync::atomic::AtomicU8, time::Duration}; static WALLETS: Lazy = Lazy::new(|| { TestWallets { @@ -54,10 +54,7 @@ async fn send_eth() { #[tokio::test] #[cfg(not(feature = "celo"))] async fn pending_txs_with_confirmations_testnet() { - let provider = - Provider::::try_from("https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27") - .unwrap() - .interval(Duration::from_millis(3000)); + let provider = RINKEBY.provider().interval(Duration::from_millis(3000)); let chain_id = provider.get_chainid().await.unwrap(); let wallet = WALLETS.next().with_chain_id(chain_id.as_u64()); let address = wallet.address(); @@ -72,11 +69,7 @@ use ethers_core::types::{Address, Eip1559TransactionRequest}; #[tokio::test] #[cfg(not(feature = "celo"))] async fn websocket_pending_txs_with_confirmations_testnet() { - let provider = - Provider::connect("wss://rinkeby.infura.io/ws/v3/c60b0bb42f8a4c6481ecd229eddaca27") - .await - .unwrap() - .interval(Duration::from_millis(3000)); + let provider = RINKEBY.ws().await.interval(Duration::from_millis(3000)); let chain_id = provider.get_chainid().await.unwrap(); let wallet = WALLETS.next().with_chain_id(chain_id.as_u64()); let address = wallet.address(); @@ -97,9 +90,7 @@ async fn generic_pending_txs_test(provider: M, who: Address) { #[tokio::test] #[cfg(not(feature = "celo"))] async fn typed_txs() { - let provider = - Provider::::try_from("https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27") - .unwrap(); + let provider = RINKEBY.provider(); let chain_id = provider.get_chainid().await.unwrap(); let wallet = WALLETS.next().with_chain_id(chain_id.as_u64()); diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml index 0d921325..56090340 100644 --- a/ethers-providers/Cargo.toml +++ b/ethers-providers/Cargo.toml @@ -39,6 +39,7 @@ tracing = { version = "0.1.32", default-features = false } tracing-futures = { version = "0.2.5", default-features = false, features = ["std-future"] } bytes = { version = "1.1.0", default-features = false, optional = true } +once_cell = "1.10.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # tokio diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs index d169bcc2..e373e085 100644 --- a/ethers-providers/src/lib.rs +++ b/ethers-providers/src/lib.rs @@ -657,3 +657,63 @@ pub trait CeloMiddleware: Middleware { self.provider().get_validators_bls_public_keys(block_id).await.map_err(FromErr::from) } } + +pub use test_provider::{GOERLI, MAINNET, RINKEBY, ROPSTEN}; + +/// Pre-instantiated Infura HTTP clients which rotate through multiple API keys +/// to prevent rate limits +pub mod test_provider { + use super::*; + use crate::Http; + use once_cell::sync::Lazy; + use std::{convert::TryFrom, iter::Cycle, slice::Iter, sync::Mutex}; + + // List of infura keys to rotate through so we don't get rate limited + const INFURA_KEYS: &[&str] = &[ + "6770454bc6ea42c58aac12978531b93f", + "7a8769b798b642f6933f2ed52042bd70", + "631fd9a6539644088297dc605d35fff3", + "16a8be88795540b9b3903d8de0f7baa5", + "f4a0bdad42674adab5fc0ac077ffab2b", + "5c812e02193c4ba793f8c214317582bd", + ]; + + pub static RINKEBY: Lazy = + Lazy::new(|| TestProvider::new(INFURA_KEYS, "rinkeby")); + pub static MAINNET: Lazy = + Lazy::new(|| TestProvider::new(INFURA_KEYS, "mainnet")); + pub static GOERLI: Lazy = Lazy::new(|| TestProvider::new(INFURA_KEYS, "goerli")); + pub static ROPSTEN: Lazy = + Lazy::new(|| TestProvider::new(INFURA_KEYS, "ropsten")); + + #[derive(Debug)] + pub struct TestProvider { + network: String, + keys: Mutex>>, + } + + impl TestProvider { + pub fn new(keys: &'static [&'static str], network: &str) -> Self { + Self { keys: Mutex::new(keys.iter().cycle()), network: network.to_owned() } + } + + pub fn provider(&self) -> Provider { + let url = format!( + "https://{}.infura.io/v3/{}", + self.network, + self.keys.lock().unwrap().next().unwrap() + ); + Provider::try_from(url.as_str()).unwrap() + } + + #[cfg(feature = "ws")] + pub async fn ws(&self) -> Provider { + let url = format!( + "wss://{}.infura.io/ws/v3/{}", + self.network, + self.keys.lock().unwrap().next().unwrap() + ); + Provider::connect(url.as_str()).await.unwrap() + } + } +} diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs index dee01796..6f6c7ea8 100644 --- a/ethers-providers/src/provider.rs +++ b/ethers-providers/src/provider.rs @@ -1488,12 +1488,10 @@ mod tests { }; use futures_util::StreamExt; - const INFURA: &str = "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27"; - #[tokio::test] // Test vector from: https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#id2 async fn mainnet_resolve_name() { - let provider = Provider::::try_from(INFURA).unwrap(); + let provider = crate::test_provider::MAINNET.provider(); let addr = provider.resolve_name("registrar.firefly.eth").await.unwrap(); assert_eq!(addr, "6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap()); @@ -1508,7 +1506,7 @@ mod tests { #[tokio::test] // Test vector from: https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#id2 async fn mainnet_lookup_address() { - let provider = Provider::::try_from(INFURA).unwrap(); + let provider = crate::MAINNET.provider(); let name = provider .lookup_address("6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap()) @@ -1525,7 +1523,7 @@ mod tests { #[tokio::test] async fn mainnet_resolve_avatar() { - let provider = Provider::::try_from(INFURA).unwrap(); + let provider = crate::MAINNET.provider(); for (ens_name, res) in &[ // HTTPS diff --git a/ethers-providers/src/transports/mod.rs b/ethers-providers/src/transports/mod.rs index b7a6c64e..58233ab4 100644 --- a/ethers-providers/src/transports/mod.rs +++ b/ethers-providers/src/transports/mod.rs @@ -22,7 +22,7 @@ macro_rules! if_not_wasm { #[cfg(all(target_family = "unix", feature = "ipc"))] mod ipc; #[cfg(all(target_family = "unix", feature = "ipc"))] -pub use ipc::Ipc; +pub use ipc::{Ipc, IpcError}; mod http; pub use self::http::{ClientError as HttpClientError, Provider as Http}; @@ -34,7 +34,7 @@ pub use ws::{ClientError as WsClientError, Ws}; mod quorum; pub(crate) use quorum::JsonRpcClientWrapper; -pub use quorum::{Quorum, QuorumProvider, WeightedProvider}; +pub use quorum::{Quorum, QuorumError, QuorumProvider, WeightedProvider}; mod mock; pub use mock::{MockError, MockProvider}; diff --git a/ethers-providers/src/transports/ws.rs b/ethers-providers/src/transports/ws.rs index ba27729b..31306e54 100644 --- a/ethers-providers/src/transports/ws.rs +++ b/ethers-providers/src/transports/ws.rs @@ -62,8 +62,7 @@ if_not_wasm! { use super::Authorization; use tracing::{debug, error, warn}; use http::Request as HttpRequest; - use http::Uri; - use std::str::FromStr; + use tungstenite::client::IntoClientRequest; } type Pending = oneshot::Sender>; @@ -137,9 +136,7 @@ impl Ws { /// Initializes a new WebSocket Client #[cfg(not(target_arch = "wasm32"))] - pub async fn connect( - url: impl tungstenite::client::IntoClientRequest + Unpin, - ) -> Result { + pub async fn connect(url: impl IntoClientRequest + Unpin) -> Result { let (ws, _) = connect_async(url).await?; Ok(Self::new(ws)) } @@ -147,11 +144,10 @@ impl Ws { /// Initializes a new WebSocket Client with authentication #[cfg(not(target_arch = "wasm32"))] pub async fn connect_with_auth( - uri: impl AsRef + Unpin, + uri: impl IntoClientRequest + Unpin, auth: Authorization, ) -> Result { - let mut request: HttpRequest<()> = - HttpRequest::builder().method("GET").uri(Uri::from_str(uri.as_ref())?).body(())?; + let mut request: HttpRequest<()> = uri.into_client_request()?; let mut auth_value = http::HeaderValue::from_str(&auth.to_string())?; auth_value.set_sensitive(true); diff --git a/ethers-providers/tests/provider.rs b/ethers-providers/tests/provider.rs index 3caec3cf..54129843 100644 --- a/ethers-providers/tests/provider.rs +++ b/ethers-providers/tests/provider.rs @@ -1,5 +1,5 @@ #![cfg(not(target_arch = "wasm32"))] -use ethers_providers::{Http, Middleware, Provider}; +use ethers_providers::{Http, Middleware, Provider, RINKEBY}; use std::{convert::TryFrom, time::Duration}; #[cfg(not(feature = "celo"))] @@ -12,10 +12,7 @@ mod eth_tests { #[tokio::test] async fn non_existing_data_works() { - let provider = Provider::::try_from( - "https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27", - ) - .unwrap(); + let provider = RINKEBY.provider(); assert!(provider.get_transaction(H256::zero()).await.unwrap().is_none()); assert!(provider.get_transaction_receipt(H256::zero()).await.unwrap().is_none()); @@ -25,10 +22,7 @@ mod eth_tests { #[tokio::test] async fn client_version() { - let provider = Provider::::try_from( - "https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27", - ) - .unwrap(); + let provider = RINKEBY.provider(); // e.g., Geth/v1.10.6-omnibus-1af33248/linux-amd64/go1.16.6 assert!(provider @@ -41,11 +35,7 @@ mod eth_tests { // Without TLS this would error with "TLS Support not compiled in" #[tokio::test] async fn ssl_websocket() { - use ethers_providers::Ws; - let ws = Ws::connect("wss://rinkeby.infura.io/ws/v3/c60b0bb42f8a4c6481ecd229eddaca27") - .await - .unwrap(); - let provider = Provider::new(ws); + let provider = RINKEBY.ws().await; let _number = provider.get_block_number().await.unwrap(); } @@ -95,10 +85,7 @@ mod eth_tests { #[tokio::test] async fn eip1559_fee_estimation() { - let provider = Provider::::try_from( - "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27", - ) - .unwrap(); + let provider = ethers_providers::MAINNET.provider(); let (_max_fee_per_gas, _max_priority_fee_per_gas) = provider.estimate_eip1559_fees(None).await.unwrap(); diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml index 31e3d64b..26b14ca7 100644 --- a/ethers-solc/Cargo.toml +++ b/ethers-solc/Cargo.toml @@ -53,7 +53,7 @@ criterion = { version = "0.3", features = ["async_tokio"] } env_logger = "*" tracing-subscriber = {version = "0.3", default-features = false, features = ["env-filter", "fmt"]} rand = "0.8.5" -pretty_assertions = "1.1.0" +pretty_assertions = "1.2.0" tempfile = "3.3.0" tokio = { version = "1.15.0", features = ["full"] } diff --git a/ethers-solc/src/cache.rs b/ethers-solc/src/cache.rs index faf07465..3a365e2f 100644 --- a/ethers-solc/src/cache.rs +++ b/ethers-solc/src/cache.rs @@ -29,7 +29,7 @@ const ETHERS_FORMAT_VERSION: &str = "ethers-rs-sol-cache-2"; /// The file name of the default cache file pub const SOLIDITY_FILES_CACHE_FILENAME: &str = "solidity-files-cache.json"; -/// A hardhat compatible cache representation +/// A multi version cache file #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct SolFilesCache { #[serde(rename = "_format")] @@ -593,62 +593,35 @@ impl<'a, T: ArtifactOutput> ArtifactsCacheInner<'a, T> { } } - /// Returns only dirty sources that: + /// Returns only those sources that /// - are new /// - were changed /// - their imports were changed /// - their artifact is missing - /// This also includes their respective imports fn filter(&mut self, sources: Sources, version: &Version) -> Sources { self.fill_hashes(&sources); - - let mut imports_of_dirty = HashSet::new(); - // separates all source files that fit the criteria (dirty) from those that don't (clean) - let (mut dirty_sources, clean_sources) = sources + sources .into_iter() - .map(|(file, source)| self.filter_source(file, source, version)) - .fold( - (Sources::default(), Vec::new()), - |(mut dirty_sources, mut clean_sources), source| { - if source.dirty { - // mark all files that are imported by a dirty file - imports_of_dirty.extend(self.edges.all_imported_nodes(source.idx)); - dirty_sources.insert(source.file, source.source); - } else { - clean_sources.push(source); - } - - (dirty_sources, clean_sources) - }, - ); - - for clean_source in clean_sources { - let FilteredSource { file, source, idx, .. } = clean_source; - if imports_of_dirty.contains(&idx) { - // file is imported by a dirty file - dirty_sources.insert(file, source); - } else { - self.insert_filtered_source(file, source, version.clone()); - } - } - - // track dirty sources internally - for (file, source) in dirty_sources.iter() { - self.insert_new_cache_entry(file, source, version.clone()); - } - - dirty_sources + .filter_map(|(file, source)| self.requires_solc(file, source, version)) + .collect() } - /// Returns the state of the given source file. - fn filter_source(&self, file: PathBuf, source: Source, version: &Version) -> FilteredSource { - let idx = self.edges.node_id(&file); + /// Returns `Some` if the file _needs_ to be compiled and `None` if the artifact can be reu-used + fn requires_solc( + &mut self, + file: PathBuf, + source: Source, + version: &Version, + ) -> Option<(PathBuf, Source)> { if !self.is_dirty(&file, version) && self.edges.imports(&file).iter().all(|file| !self.is_dirty(file, version)) { - FilteredSource { file, source, idx, dirty: false } + self.insert_filtered_source(file, source, version.clone()); + None } else { - FilteredSource { file, source, idx, dirty: true } + self.insert_new_cache_entry(&file, &source, version.clone()); + + Some((file, source)) } } @@ -665,23 +638,28 @@ impl<'a, T: ArtifactOutput> ArtifactsCacheInner<'a, T> { return true } - if !entry.contains_version(version) { - tracing::trace!( - "missing linked artifacts for source file `{}` for version \"{}\"", - file.display(), - version - ); - return true - } - - if entry.artifacts_for_version(version).any(|artifact_path| { - let missing_artifact = !self.cached_artifacts.has_artifact(artifact_path); - if missing_artifact { - tracing::trace!("missing artifact \"{}\"", artifact_path.display()); + // only check artifact's existence if the file generated artifacts. + // e.g. a solidity file consisting only of import statements (like interfaces that + // re-export) do not create artifacts + if !entry.artifacts.is_empty() { + if !entry.contains_version(version) { + tracing::trace!( + "missing linked artifacts for source file `{}` for version \"{}\"", + file.display(), + version + ); + return true + } + + if entry.artifacts_for_version(version).any(|artifact_path| { + let missing_artifact = !self.cached_artifacts.has_artifact(artifact_path); + if missing_artifact { + tracing::trace!("missing artifact \"{}\"", artifact_path.display()); + } + missing_artifact + }) { + return true } - missing_artifact - }) { - return true } // all things match, can be reused return false @@ -701,14 +679,6 @@ impl<'a, T: ArtifactOutput> ArtifactsCacheInner<'a, T> { } } -/// Helper type to represent the state of a source file -struct FilteredSource { - file: PathBuf, - source: Source, - idx: usize, - dirty: bool, -} - /// Abstraction over configured caching which can be either non-existent or an already loaded cache #[allow(clippy::large_enum_variant)] #[derive(Debug)] diff --git a/ethers-solc/src/compile/mod.rs b/ethers-solc/src/compile/mod.rs index 7f4d48db..6bfbac05 100644 --- a/ethers-solc/src/compile/mod.rs +++ b/ethers-solc/src/compile/mod.rs @@ -372,9 +372,16 @@ impl Solc { pub fn blocking_install(version: &Version) -> std::result::Result<(), svm::SolcVmError> { tracing::trace!("blocking installing solc version \"{}\"", version); crate::report::solc_installation_start(version); - svm::blocking_install(version)?; - crate::report::solc_installation_success(version); - Ok(()) + match svm::blocking_install(version) { + Ok(_) => { + crate::report::solc_installation_success(version); + Ok(()) + } + Err(err) => { + crate::report::solc_installation_error(version, &err.to_string()); + Err(err) + } + } } /// Verify that the checksum for this version of solc is correct. We check against the SHA256 diff --git a/ethers-solc/src/compile/project.rs b/ethers-solc/src/compile/project.rs index 19e36a3c..31174ccb 100644 --- a/ethers-solc/src/compile/project.rs +++ b/ethers-solc/src/compile/project.rs @@ -73,6 +73,33 @@ //! file in the VFS under `dapp-bin/library/math.sol`. If the file is not available there, the //! source unit name will be passed to the Host Filesystem Loader, which will then look in //! `/project/dapp-bin/library/iterable_mapping.sol` +//! +//! +//! ### Caching and Change detection +//! +//! If caching is enabled in the [Project](crate::Project) a cache file will be created upon a +//! successful solc build. The [cache file](crate::SolFilesCache) stores metadata for all the files +//! that were provided to solc. +//! For every file the cache file contains a dedicated [cache +//! entry](crate::CacheEntry), which represents the state of the file. A solidity file can contain +//! several contracts, for every contract a separate [artifact](crate::Artifact) is emitted. +//! Therefor the entry also tracks all artifacts emitted by a file. A solidity file can also be +//! compiled with several solc versions. +//! +//! For example in `A(<=0.8.10) imports C(>0.4.0)` and +//! `B(0.8.11) imports C(>0.4.0)`, both `A` and `B` import `C` but there's no solc version that's +//! compatible with `A` and `B`, in which case two sets are compiled: [`A`, `C`] and [`B`, `C`]. +//! This is reflected in the cache entry which tracks the file's artifacts by version. +//! +//! The cache makes it possible to detect changes during recompilation, so that only the changed, +//! dirty, files need to be passed to solc. A file will be considered as dirty if: +//! - the file is new, not included in the existing cache +//! - the file was modified since the last compiler run, detected by comparing content hashes +//! - any of the imported files is dirty +//! - the file's artifacts don't exist, were deleted. +//! +//! Recompiling a project with cache enabled detects all files that meet these criteria and provides +//! solc with only these dirty files instead of the entire source set. use crate::{ artifact_output::Artifacts, @@ -283,7 +310,13 @@ impl CompilerSources { sources .into_iter() .map(|(solc, (version, sources))| { + tracing::trace!("Filtering {} sources for {}", sources.len(), version); let sources = cache.filter(sources, &version); + tracing::trace!( + "Detected {} dirty sources {:?}", + sources.len(), + sources.keys() + ); (solc, (version, sources)) }) .collect() diff --git a/ethers-solc/src/remappings.rs b/ethers-solc/src/remappings.rs index 29ec1f57..9ff4c73e 100644 --- a/ethers-solc/src/remappings.rs +++ b/ethers-solc/src/remappings.rs @@ -661,7 +661,7 @@ mod tests { assert_eq!(relative.path.original(), Path::new(&remapping.path)); assert!(relative.path.parent.is_none()); - let relative = RelativeRemapping::new(remapping.clone(), "/a/b"); + let relative = RelativeRemapping::new(remapping, "/a/b"); assert_eq!(relative.to_relative_remapping(), Remapping::from_str("oz/=c/d/").unwrap()); } diff --git a/ethers-solc/src/report.rs b/ethers-solc/src/report.rs index b4b38253..d7806ca8 100644 --- a/ethers-solc/src/report.rs +++ b/ethers-solc/src/report.rs @@ -14,7 +14,7 @@ // https://github.com/tokio-rs/tracing/blob/master/tracing-core/src/dispatch.rs -use crate::{CompilerInput, CompilerOutput, Solc}; +use crate::{remappings::Remapping, CompilerInput, CompilerOutput, Solc}; use semver::Version; use std::{ any::{Any, TypeId}, @@ -99,11 +99,14 @@ pub trait Reporter: 'static { /// Invoked before a new [`Solc`] bin is installed fn on_solc_installation_start(&self, _version: &Version) {} - /// Invoked before a new [`Solc`] bin was successfully installed + /// Invoked after a new [`Solc`] bin was successfully installed fn on_solc_installation_success(&self, _version: &Version) {} - /// Invoked if the import couldn't be resolved - fn on_unresolved_import(&self, _import: &Path) {} + /// Invoked after a [`Solc`] installation failed + fn on_solc_installation_error(&self, _version: &Version, _error: &str) {} + + /// Invoked if the import couldn't be resolved with these remappings + fn on_unresolved_import(&self, _import: &Path, _remappings: &[Remapping]) {} /// If `self` is the same type as the provided `TypeId`, returns an untyped /// [`NonNull`] pointer to that type. Otherwise, returns `None`. @@ -166,8 +169,13 @@ pub(crate) fn solc_installation_success(version: &Version) { get_default(|r| r.reporter.on_solc_installation_success(version)); } -pub(crate) fn unresolved_import(import: &Path) { - get_default(|r| r.reporter.on_unresolved_import(import)); +#[allow(unused)] +pub(crate) fn solc_installation_error(version: &Version, error: &str) { + get_default(|r| r.reporter.on_solc_installation_error(version, error)); +} + +pub(crate) fn unresolved_import(import: &Path, remappings: &[Remapping]) { + get_default(|r| r.reporter.on_unresolved_import(import, remappings)); } fn get_global() -> Option<&'static Report> { @@ -308,8 +316,16 @@ impl Reporter for BasicStdoutReporter { println!("Successfully installed solc {}", version); } - fn on_unresolved_import(&self, import: &Path) { - println!("Unable to resolve imported file: \"{}\"", import.display()); + fn on_solc_installation_error(&self, version: &Version, error: &str) { + eprintln!("Failed to install solc {}: {}", version, error); + } + + fn on_unresolved_import(&self, import: &Path, remappings: &[Remapping]) { + println!( + "Unable to resolve import: \"{}\" with remappings:\n {}", + import.display(), + remappings.iter().map(|r| r.to_string()).collect::>().join("\n ") + ); } } diff --git a/ethers-solc/src/resolver/mod.rs b/ethers-solc/src/resolver/mod.rs index a62fc258..f0a70bd7 100644 --- a/ethers-solc/src/resolver/mod.rs +++ b/ethers-solc/src/resolver/mod.rs @@ -274,7 +274,7 @@ impl Graph { add_node(&mut unresolved, &mut index, &mut resolved_imports, import)?; } Err(err) => { - crate::report::unresolved_import(import.data()); + crate::report::unresolved_import(import.data(), &paths.remappings); tracing::trace!("failed to resolve import component \"{:?}\"", err) } }; diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs index 15a61b98..9048522d 100644 --- a/ethers-solc/tests/project.rs +++ b/ethers-solc/tests/project.rs @@ -685,3 +685,51 @@ fn can_recompile_with_changes() { assert!(compiled.find("A").is_some()); assert!(compiled.find("B").is_some()); } + +#[test] +fn can_recompile_unchanged_with_empty_files() { + let tmp = TempProject::dapptools().unwrap(); + + tmp.add_source( + "A", + r#" + pragma solidity ^0.8.10; + import "./B.sol"; + contract A {} + "#, + ) + .unwrap(); + + tmp.add_source( + "B", + r#" + pragma solidity ^0.8.10; + import "./C.sol"; + "#, + ) + .unwrap(); + + let c = r#" + pragma solidity ^0.8.10; + contract C {} + "#; + tmp.add_source("C", c).unwrap(); + + let compiled = tmp.compile().unwrap(); + assert!(!compiled.has_compiler_errors()); + assert!(compiled.find("A").is_some()); + assert!(compiled.find("C").is_some()); + + let compiled = tmp.compile().unwrap(); + assert!(compiled.find("A").is_some()); + assert!(compiled.find("C").is_some()); + assert!(compiled.is_unchanged()); + + // modify C.sol + tmp.add_source("C", format!("{}\n", c)).unwrap(); + let compiled = tmp.compile().unwrap(); + assert!(!compiled.has_compiler_errors()); + assert!(!compiled.is_unchanged()); + assert!(compiled.find("A").is_some()); + assert!(compiled.find("C").is_some()); +} diff --git a/examples/contract_human_readable.rs b/examples/contract_human_readable.rs index 674c2e66..a4315a7c 100644 --- a/examples/contract_human_readable.rs +++ b/examples/contract_human_readable.rs @@ -30,7 +30,7 @@ async fn main() -> Result<()> { let project = Project::builder().paths(paths).ephemeral().no_artifacts().build().unwrap(); // compile the project and get the artifacts let output = project.compile().unwrap(); - let contract = output.find("SimpleStorage").expect("could not find contract").into_owned(); + let contract = output.find("SimpleStorage").expect("could not find contract").clone(); let (abi, bytecode, _) = contract.into_parts_or_default(); // 2. instantiate our wallet & ganache