Merge pull request #1 from gakonst/master

Update
This commit is contained in:
ControlCplusControlV 2022-03-14 20:06:04 -06:00 committed by GitHub
commit 7c3ee09b90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 294 additions and 152 deletions

View File

@ -130,6 +130,13 @@
## ethers-providers ## 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 ### 0.6.0
- re-export error types for `Http` and `Ws` providers in - 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. - Add support for `evm_snapshot` and `evm_revert` dev RPC methods.
[640](https://github.com/gakonst/ethers-rs/pull/640) [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 ### 0.5.3
- Expose `ens` module [#435](https://github.com/gakonst/ethers-rs/pull/435) - Expose `ens` module [#435](https://github.com/gakonst/ethers-rs/pull/435)

7
Cargo.lock generated
View File

@ -1322,6 +1322,7 @@ dependencies = [
"futures-util", "futures-util",
"hex", "hex",
"http", "http",
"once_cell",
"parking_lot", "parking_lot",
"pin-project", "pin-project",
"reqwest", "reqwest",
@ -2666,9 +2667,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]] [[package]]
name = "pretty_assertions" name = "pretty_assertions"
version = "1.1.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76d5b548b725018ab5496482b45cb8bef21e9fed1858a6d674e3a8a0f0bb5d50" checksum = "57c038cb5319b9c704bf9c227c261d275bfec0ad438118a2787ce47944fb228b"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"ctor", "ctor",
@ -3575,7 +3576,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]] [[package]]
name = "svm-rs" name = "svm-rs"
version = "0.2.9" version = "0.2.9"
source = "git+https://github.com/roynalnaruto/svm-rs#8e33f55fa2a2afb937749e31b2ffa42600bfe216" source = "git+https://github.com/roynalnaruto/svm-rs#ae79a29f5bde08f1991f981456253fa5b6859047"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cfg-if 1.0.0", "cfg-if 1.0.0",

View File

@ -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); let file_path = dir.join(file_name);
eyre::ensure!(file_path.is_file(), "Not a file: {}", file_path.display()); eyre::ensure!(file_path.is_file(), "Not a file: {}", file_path.display());
let contents = fs::read(file_path).expect("Unable to read file"); let contents = fs::read(&file_path).expect("Unable to read file");
eyre::ensure!(contents == expected_contents, "file contents do not match"); 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(()) Ok(())
} }

View File

@ -105,6 +105,7 @@ impl VerifyContract {
) -> Self { ) -> Self {
self.constructor_arguments = constructor_arguments.map(|s| { self.constructor_arguments = constructor_arguments.map(|s| {
s.into() s.into()
.trim()
// TODO is this correct? // TODO is this correct?
.trim_start_matches("0x") .trim_start_matches("0x")
.to_string() .to_string()

View File

@ -1,5 +1,6 @@
use ethers_core::types::{ 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_providers::{maybe, FromErr, Middleware, PendingTransaction};
use ethers_signers::Signer; use ethers_signers::Signer;
@ -158,6 +159,14 @@ where
this.signer = signer; this.signer = signer;
this 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))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@ -264,6 +273,32 @@ where
) -> Result<Signature, Self::Error> { ) -> Result<Signature, Self::Error> {
self.signer.sign_message(data.into()).await.map_err(SignerMiddlewareError::SignerError) self.signer.sign_message(data.into()).await.map_err(SignerMiddlewareError::SignerError)
} }
async fn estimate_gas(&self, tx: &TypedTransaction) -> Result<U256, Self::Error> {
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<BlockId>,
) -> Result<AccessListWithGasUsed, Self::Error> {
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<BlockId>,
) -> Result<Bytes, Self::Error> {
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")))] #[cfg(all(test, not(feature = "celo"), not(target_arch = "wasm32")))]

View File

@ -4,7 +4,7 @@ use ethers_middleware::{
gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice}, gas_escalator::{Frequency, GasEscalatorMiddleware, GeometricGasPrice},
signer::SignerMiddleware, signer::SignerMiddleware,
}; };
use ethers_providers::{Middleware, Provider, Ws}; use ethers_providers::Middleware;
use ethers_signers::{LocalWallet, Signer}; use ethers_signers::{LocalWallet, Signer};
use std::time::Duration; use std::time::Duration;
@ -12,10 +12,8 @@ use std::time::Duration;
#[ignore] #[ignore]
async fn gas_escalator_live() { async fn gas_escalator_live() {
// connect to ropsten for getting bad block times // connect to ropsten for getting bad block times
let ws = Ws::connect("wss://ropsten.infura.io/ws/v3/fd8b88b56aa84f6da87b60f5441d6778") let provider = ethers_providers::ROPSTEN.ws().await;
.await let provider = provider.interval(Duration::from_millis(2000u64));
.unwrap();
let provider = Provider::new(ws).interval(Duration::from_millis(2000u64));
let wallet = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169" let wallet = "fdb33e2105f08abe41a8ee3b758726a31abdd57b7a443f470f23efce853af169"
.parse::<LocalWallet>() .parse::<LocalWallet>()
.unwrap(); .unwrap();

View File

@ -4,14 +4,11 @@
async fn nonce_manager() { async fn nonce_manager() {
use ethers_core::types::*; use ethers_core::types::*;
use ethers_middleware::{nonce_manager::NonceManagerMiddleware, signer::SignerMiddleware}; use ethers_middleware::{nonce_manager::NonceManagerMiddleware, signer::SignerMiddleware};
use ethers_providers::{Http, Middleware, Provider}; use ethers_providers::Middleware;
use ethers_signers::{LocalWallet, Signer}; use ethers_signers::{LocalWallet, Signer};
use std::{convert::TryFrom, time::Duration}; use std::time::Duration;
let provider = let provider = ethers_providers::RINKEBY.provider().interval(Duration::from_millis(2000u64));
Provider::<Http>::try_from("https://rinkeby.infura.io/v3/fd8b88b56aa84f6da87b60f5441d6778")
.unwrap()
.interval(Duration::from_millis(2000u64));
let chain_id = provider.get_chainid().await.unwrap().as_u64(); let chain_id = provider.get_chainid().await.unwrap().as_u64();
let wallet = std::env::var("RINKEBY_PRIVATE_KEY") let wallet = std::env::var("RINKEBY_PRIVATE_KEY")

View File

@ -1,5 +1,5 @@
#![allow(unused)] #![allow(unused)]
use ethers_providers::{Http, JsonRpcClient, Middleware, Provider}; use ethers_providers::{Http, JsonRpcClient, Middleware, Provider, RINKEBY};
use ethers_core::{ use ethers_core::{
types::{BlockNumber, TransactionRequest}, types::{BlockNumber, TransactionRequest},
@ -8,7 +8,7 @@ use ethers_core::{
use ethers_middleware::signer::SignerMiddleware; use ethers_middleware::signer::SignerMiddleware;
use ethers_signers::{coins_bip39::English, LocalWallet, MnemonicBuilder, Signer}; use ethers_signers::{coins_bip39::English, LocalWallet, MnemonicBuilder, Signer};
use once_cell::sync::Lazy; 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<TestWallets> = Lazy::new(|| { static WALLETS: Lazy<TestWallets> = Lazy::new(|| {
TestWallets { TestWallets {
@ -54,10 +54,7 @@ async fn send_eth() {
#[tokio::test] #[tokio::test]
#[cfg(not(feature = "celo"))] #[cfg(not(feature = "celo"))]
async fn pending_txs_with_confirmations_testnet() { async fn pending_txs_with_confirmations_testnet() {
let provider = let provider = RINKEBY.provider().interval(Duration::from_millis(3000));
Provider::<Http>::try_from("https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27")
.unwrap()
.interval(Duration::from_millis(3000));
let chain_id = provider.get_chainid().await.unwrap(); let chain_id = provider.get_chainid().await.unwrap();
let wallet = WALLETS.next().with_chain_id(chain_id.as_u64()); let wallet = WALLETS.next().with_chain_id(chain_id.as_u64());
let address = wallet.address(); let address = wallet.address();
@ -72,11 +69,7 @@ use ethers_core::types::{Address, Eip1559TransactionRequest};
#[tokio::test] #[tokio::test]
#[cfg(not(feature = "celo"))] #[cfg(not(feature = "celo"))]
async fn websocket_pending_txs_with_confirmations_testnet() { async fn websocket_pending_txs_with_confirmations_testnet() {
let provider = let provider = RINKEBY.ws().await.interval(Duration::from_millis(3000));
Provider::connect("wss://rinkeby.infura.io/ws/v3/c60b0bb42f8a4c6481ecd229eddaca27")
.await
.unwrap()
.interval(Duration::from_millis(3000));
let chain_id = provider.get_chainid().await.unwrap(); let chain_id = provider.get_chainid().await.unwrap();
let wallet = WALLETS.next().with_chain_id(chain_id.as_u64()); let wallet = WALLETS.next().with_chain_id(chain_id.as_u64());
let address = wallet.address(); let address = wallet.address();
@ -97,9 +90,7 @@ async fn generic_pending_txs_test<M: Middleware>(provider: M, who: Address) {
#[tokio::test] #[tokio::test]
#[cfg(not(feature = "celo"))] #[cfg(not(feature = "celo"))]
async fn typed_txs() { async fn typed_txs() {
let provider = let provider = RINKEBY.provider();
Provider::<Http>::try_from("https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27")
.unwrap();
let chain_id = provider.get_chainid().await.unwrap(); let chain_id = provider.get_chainid().await.unwrap();
let wallet = WALLETS.next().with_chain_id(chain_id.as_u64()); let wallet = WALLETS.next().with_chain_id(chain_id.as_u64());

View File

@ -39,6 +39,7 @@ tracing = { version = "0.1.32", default-features = false }
tracing-futures = { version = "0.2.5", default-features = false, features = ["std-future"] } tracing-futures = { version = "0.2.5", default-features = false, features = ["std-future"] }
bytes = { version = "1.1.0", default-features = false, optional = true } bytes = { version = "1.1.0", default-features = false, optional = true }
once_cell = "1.10.0"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# tokio # tokio

View File

@ -657,3 +657,63 @@ pub trait CeloMiddleware: Middleware {
self.provider().get_validators_bls_public_keys(block_id).await.map_err(FromErr::from) 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<TestProvider> =
Lazy::new(|| TestProvider::new(INFURA_KEYS, "rinkeby"));
pub static MAINNET: Lazy<TestProvider> =
Lazy::new(|| TestProvider::new(INFURA_KEYS, "mainnet"));
pub static GOERLI: Lazy<TestProvider> = Lazy::new(|| TestProvider::new(INFURA_KEYS, "goerli"));
pub static ROPSTEN: Lazy<TestProvider> =
Lazy::new(|| TestProvider::new(INFURA_KEYS, "ropsten"));
#[derive(Debug)]
pub struct TestProvider {
network: String,
keys: Mutex<Cycle<Iter<'static, &'static str>>>,
}
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<Http> {
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<crate::Ws> {
let url = format!(
"wss://{}.infura.io/ws/v3/{}",
self.network,
self.keys.lock().unwrap().next().unwrap()
);
Provider::connect(url.as_str()).await.unwrap()
}
}
}

View File

@ -1488,12 +1488,10 @@ mod tests {
}; };
use futures_util::StreamExt; use futures_util::StreamExt;
const INFURA: &str = "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27";
#[tokio::test] #[tokio::test]
// Test vector from: https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#id2 // Test vector from: https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#id2
async fn mainnet_resolve_name() { async fn mainnet_resolve_name() {
let provider = Provider::<HttpProvider>::try_from(INFURA).unwrap(); let provider = crate::test_provider::MAINNET.provider();
let addr = provider.resolve_name("registrar.firefly.eth").await.unwrap(); let addr = provider.resolve_name("registrar.firefly.eth").await.unwrap();
assert_eq!(addr, "6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap()); assert_eq!(addr, "6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap());
@ -1508,7 +1506,7 @@ mod tests {
#[tokio::test] #[tokio::test]
// Test vector from: https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#id2 // Test vector from: https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#id2
async fn mainnet_lookup_address() { async fn mainnet_lookup_address() {
let provider = Provider::<HttpProvider>::try_from(INFURA).unwrap(); let provider = crate::MAINNET.provider();
let name = provider let name = provider
.lookup_address("6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap()) .lookup_address("6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap())
@ -1525,7 +1523,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn mainnet_resolve_avatar() { async fn mainnet_resolve_avatar() {
let provider = Provider::<HttpProvider>::try_from(INFURA).unwrap(); let provider = crate::MAINNET.provider();
for (ens_name, res) in &[ for (ens_name, res) in &[
// HTTPS // HTTPS

View File

@ -22,7 +22,7 @@ macro_rules! if_not_wasm {
#[cfg(all(target_family = "unix", feature = "ipc"))] #[cfg(all(target_family = "unix", feature = "ipc"))]
mod ipc; mod ipc;
#[cfg(all(target_family = "unix", feature = "ipc"))] #[cfg(all(target_family = "unix", feature = "ipc"))]
pub use ipc::Ipc; pub use ipc::{Ipc, IpcError};
mod http; mod http;
pub use self::http::{ClientError as HttpClientError, Provider as Http}; pub use self::http::{ClientError as HttpClientError, Provider as Http};
@ -34,7 +34,7 @@ pub use ws::{ClientError as WsClientError, Ws};
mod quorum; mod quorum;
pub(crate) use quorum::JsonRpcClientWrapper; pub(crate) use quorum::JsonRpcClientWrapper;
pub use quorum::{Quorum, QuorumProvider, WeightedProvider}; pub use quorum::{Quorum, QuorumError, QuorumProvider, WeightedProvider};
mod mock; mod mock;
pub use mock::{MockError, MockProvider}; pub use mock::{MockError, MockProvider};

View File

@ -62,8 +62,7 @@ if_not_wasm! {
use super::Authorization; use super::Authorization;
use tracing::{debug, error, warn}; use tracing::{debug, error, warn};
use http::Request as HttpRequest; use http::Request as HttpRequest;
use http::Uri; use tungstenite::client::IntoClientRequest;
use std::str::FromStr;
} }
type Pending = oneshot::Sender<Result<serde_json::Value, JsonRpcError>>; type Pending = oneshot::Sender<Result<serde_json::Value, JsonRpcError>>;
@ -137,9 +136,7 @@ impl Ws {
/// Initializes a new WebSocket Client /// Initializes a new WebSocket Client
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub async fn connect( pub async fn connect(url: impl IntoClientRequest + Unpin) -> Result<Self, ClientError> {
url: impl tungstenite::client::IntoClientRequest + Unpin,
) -> Result<Self, ClientError> {
let (ws, _) = connect_async(url).await?; let (ws, _) = connect_async(url).await?;
Ok(Self::new(ws)) Ok(Self::new(ws))
} }
@ -147,11 +144,10 @@ impl Ws {
/// Initializes a new WebSocket Client with authentication /// Initializes a new WebSocket Client with authentication
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub async fn connect_with_auth( pub async fn connect_with_auth(
uri: impl AsRef<str> + Unpin, uri: impl IntoClientRequest + Unpin,
auth: Authorization, auth: Authorization,
) -> Result<Self, ClientError> { ) -> Result<Self, ClientError> {
let mut request: HttpRequest<()> = let mut request: HttpRequest<()> = uri.into_client_request()?;
HttpRequest::builder().method("GET").uri(Uri::from_str(uri.as_ref())?).body(())?;
let mut auth_value = http::HeaderValue::from_str(&auth.to_string())?; let mut auth_value = http::HeaderValue::from_str(&auth.to_string())?;
auth_value.set_sensitive(true); auth_value.set_sensitive(true);

View File

@ -1,5 +1,5 @@
#![cfg(not(target_arch = "wasm32"))] #![cfg(not(target_arch = "wasm32"))]
use ethers_providers::{Http, Middleware, Provider}; use ethers_providers::{Http, Middleware, Provider, RINKEBY};
use std::{convert::TryFrom, time::Duration}; use std::{convert::TryFrom, time::Duration};
#[cfg(not(feature = "celo"))] #[cfg(not(feature = "celo"))]
@ -12,10 +12,7 @@ mod eth_tests {
#[tokio::test] #[tokio::test]
async fn non_existing_data_works() { async fn non_existing_data_works() {
let provider = Provider::<Http>::try_from( let provider = RINKEBY.provider();
"https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27",
)
.unwrap();
assert!(provider.get_transaction(H256::zero()).await.unwrap().is_none()); assert!(provider.get_transaction(H256::zero()).await.unwrap().is_none());
assert!(provider.get_transaction_receipt(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] #[tokio::test]
async fn client_version() { async fn client_version() {
let provider = Provider::<Http>::try_from( let provider = RINKEBY.provider();
"https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27",
)
.unwrap();
// e.g., Geth/v1.10.6-omnibus-1af33248/linux-amd64/go1.16.6 // e.g., Geth/v1.10.6-omnibus-1af33248/linux-amd64/go1.16.6
assert!(provider assert!(provider
@ -41,11 +35,7 @@ mod eth_tests {
// Without TLS this would error with "TLS Support not compiled in" // Without TLS this would error with "TLS Support not compiled in"
#[tokio::test] #[tokio::test]
async fn ssl_websocket() { async fn ssl_websocket() {
use ethers_providers::Ws; let provider = RINKEBY.ws().await;
let ws = Ws::connect("wss://rinkeby.infura.io/ws/v3/c60b0bb42f8a4c6481ecd229eddaca27")
.await
.unwrap();
let provider = Provider::new(ws);
let _number = provider.get_block_number().await.unwrap(); let _number = provider.get_block_number().await.unwrap();
} }
@ -95,10 +85,7 @@ mod eth_tests {
#[tokio::test] #[tokio::test]
async fn eip1559_fee_estimation() { async fn eip1559_fee_estimation() {
let provider = Provider::<Http>::try_from( let provider = ethers_providers::MAINNET.provider();
"https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27",
)
.unwrap();
let (_max_fee_per_gas, _max_priority_fee_per_gas) = let (_max_fee_per_gas, _max_priority_fee_per_gas) =
provider.estimate_eip1559_fees(None).await.unwrap(); provider.estimate_eip1559_fees(None).await.unwrap();

View File

@ -53,7 +53,7 @@ criterion = { version = "0.3", features = ["async_tokio"] }
env_logger = "*" env_logger = "*"
tracing-subscriber = {version = "0.3", default-features = false, features = ["env-filter", "fmt"]} tracing-subscriber = {version = "0.3", default-features = false, features = ["env-filter", "fmt"]}
rand = "0.8.5" rand = "0.8.5"
pretty_assertions = "1.1.0" pretty_assertions = "1.2.0"
tempfile = "3.3.0" tempfile = "3.3.0"
tokio = { version = "1.15.0", features = ["full"] } tokio = { version = "1.15.0", features = ["full"] }

View File

@ -29,7 +29,7 @@ const ETHERS_FORMAT_VERSION: &str = "ethers-rs-sol-cache-2";
/// The file name of the default cache file /// The file name of the default cache file
pub const SOLIDITY_FILES_CACHE_FILENAME: &str = "solidity-files-cache.json"; 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)] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct SolFilesCache { pub struct SolFilesCache {
#[serde(rename = "_format")] #[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 /// - are new
/// - were changed /// - were changed
/// - their imports were changed /// - their imports were changed
/// - their artifact is missing /// - their artifact is missing
/// This also includes their respective imports
fn filter(&mut self, sources: Sources, version: &Version) -> Sources { fn filter(&mut self, sources: Sources, version: &Version) -> Sources {
self.fill_hashes(&sources); self.fill_hashes(&sources);
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
.into_iter() .into_iter()
.map(|(file, source)| self.filter_source(file, source, version)) .filter_map(|(file, source)| self.requires_solc(file, source, version))
.fold( .collect()
(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
} }
/// Returns the state of the given source file. /// Returns `Some` if the file _needs_ to be compiled and `None` if the artifact can be reu-used
fn filter_source(&self, file: PathBuf, source: Source, version: &Version) -> FilteredSource { fn requires_solc(
let idx = self.edges.node_id(&file); &mut self,
file: PathBuf,
source: Source,
version: &Version,
) -> Option<(PathBuf, Source)> {
if !self.is_dirty(&file, version) && if !self.is_dirty(&file, version) &&
self.edges.imports(&file).iter().all(|file| !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 { } 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 return true
} }
if !entry.contains_version(version) { // only check artifact's existence if the file generated artifacts.
tracing::trace!( // e.g. a solidity file consisting only of import statements (like interfaces that
"missing linked artifacts for source file `{}` for version \"{}\"", // re-export) do not create artifacts
file.display(), if !entry.artifacts.is_empty() {
version if !entry.contains_version(version) {
); tracing::trace!(
return true "missing linked artifacts for source file `{}` for version \"{}\"",
} file.display(),
version
if entry.artifacts_for_version(version).any(|artifact_path| { );
let missing_artifact = !self.cached_artifacts.has_artifact(artifact_path); return true
if missing_artifact { }
tracing::trace!("missing artifact \"{}\"", artifact_path.display());
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 // all things match, can be reused
return false 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 /// Abstraction over configured caching which can be either non-existent or an already loaded cache
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[derive(Debug)] #[derive(Debug)]

View File

@ -372,9 +372,16 @@ impl Solc {
pub fn blocking_install(version: &Version) -> std::result::Result<(), svm::SolcVmError> { pub fn blocking_install(version: &Version) -> std::result::Result<(), svm::SolcVmError> {
tracing::trace!("blocking installing solc version \"{}\"", version); tracing::trace!("blocking installing solc version \"{}\"", version);
crate::report::solc_installation_start(version); crate::report::solc_installation_start(version);
svm::blocking_install(version)?; match svm::blocking_install(version) {
crate::report::solc_installation_success(version); Ok(_) => {
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 /// Verify that the checksum for this version of solc is correct. We check against the SHA256

View File

@ -73,6 +73,33 @@
//! file in the VFS under `dapp-bin/library/math.sol`. If the file is not available there, the //! 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 //! source unit name will be passed to the Host Filesystem Loader, which will then look in
//! `/project/dapp-bin/library/iterable_mapping.sol` //! `/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::{ use crate::{
artifact_output::Artifacts, artifact_output::Artifacts,
@ -283,7 +310,13 @@ impl CompilerSources {
sources sources
.into_iter() .into_iter()
.map(|(solc, (version, sources))| { .map(|(solc, (version, sources))| {
tracing::trace!("Filtering {} sources for {}", sources.len(), version);
let sources = cache.filter(sources, &version); let sources = cache.filter(sources, &version);
tracing::trace!(
"Detected {} dirty sources {:?}",
sources.len(),
sources.keys()
);
(solc, (version, sources)) (solc, (version, sources))
}) })
.collect() .collect()

View File

@ -661,7 +661,7 @@ mod tests {
assert_eq!(relative.path.original(), Path::new(&remapping.path)); assert_eq!(relative.path.original(), Path::new(&remapping.path));
assert!(relative.path.parent.is_none()); 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()); assert_eq!(relative.to_relative_remapping(), Remapping::from_str("oz/=c/d/").unwrap());
} }

View File

@ -14,7 +14,7 @@
// https://github.com/tokio-rs/tracing/blob/master/tracing-core/src/dispatch.rs // 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 semver::Version;
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
@ -99,11 +99,14 @@ pub trait Reporter: 'static {
/// Invoked before a new [`Solc`] bin is installed /// Invoked before a new [`Solc`] bin is installed
fn on_solc_installation_start(&self, _version: &Version) {} 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) {} fn on_solc_installation_success(&self, _version: &Version) {}
/// Invoked if the import couldn't be resolved /// Invoked after a [`Solc`] installation failed
fn on_unresolved_import(&self, _import: &Path) {} 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 /// If `self` is the same type as the provided `TypeId`, returns an untyped
/// [`NonNull`] pointer to that type. Otherwise, returns `None`. /// [`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)); get_default(|r| r.reporter.on_solc_installation_success(version));
} }
pub(crate) fn unresolved_import(import: &Path) { #[allow(unused)]
get_default(|r| r.reporter.on_unresolved_import(import)); 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> { fn get_global() -> Option<&'static Report> {
@ -308,8 +316,16 @@ impl Reporter for BasicStdoutReporter {
println!("Successfully installed solc {}", version); println!("Successfully installed solc {}", version);
} }
fn on_unresolved_import(&self, import: &Path) { fn on_solc_installation_error(&self, version: &Version, error: &str) {
println!("Unable to resolve imported file: \"{}\"", import.display()); 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::<Vec<_>>().join("\n ")
);
} }
} }

View File

@ -274,7 +274,7 @@ impl Graph {
add_node(&mut unresolved, &mut index, &mut resolved_imports, import)?; add_node(&mut unresolved, &mut index, &mut resolved_imports, import)?;
} }
Err(err) => { Err(err) => {
crate::report::unresolved_import(import.data()); crate::report::unresolved_import(import.data(), &paths.remappings);
tracing::trace!("failed to resolve import component \"{:?}\"", err) tracing::trace!("failed to resolve import component \"{:?}\"", err)
} }
}; };

View File

@ -685,3 +685,51 @@ fn can_recompile_with_changes() {
assert!(compiled.find("A").is_some()); assert!(compiled.find("A").is_some());
assert!(compiled.find("B").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());
}

View File

@ -30,7 +30,7 @@ async fn main() -> Result<()> {
let project = Project::builder().paths(paths).ephemeral().no_artifacts().build().unwrap(); let project = Project::builder().paths(paths).ephemeral().no_artifacts().build().unwrap();
// compile the project and get the artifacts // compile the project and get the artifacts
let output = project.compile().unwrap(); 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(); let (abi, bytecode, _) = contract.into_parts_or_default();
// 2. instantiate our wallet & ganache // 2. instantiate our wallet & ganache