commit
7c3ee09b90
12
CHANGELOG.md
12
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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<Signature, Self::Error> {
|
||||
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")))]
|
||||
|
|
|
@ -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::<LocalWallet>()
|
||||
.unwrap();
|
||||
|
|
|
@ -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::<Http>::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")
|
||||
|
|
|
@ -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<TestWallets> = 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::<Http>::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<M: Middleware>(provider: M, who: Address) {
|
|||
#[tokio::test]
|
||||
#[cfg(not(feature = "celo"))]
|
||||
async fn typed_txs() {
|
||||
let provider =
|
||||
Provider::<Http>::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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::<HttpProvider>::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::<HttpProvider>::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::<HttpProvider>::try_from(INFURA).unwrap();
|
||||
let provider = crate::MAINNET.provider();
|
||||
|
||||
for (ens_name, res) in &[
|
||||
// HTTPS
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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<Result<serde_json::Value, JsonRpcError>>;
|
||||
|
@ -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<Self, ClientError> {
|
||||
pub async fn connect(url: impl IntoClientRequest + Unpin) -> Result<Self, ClientError> {
|
||||
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<str> + Unpin,
|
||||
uri: impl IntoClientRequest + Unpin,
|
||||
auth: Authorization,
|
||||
) -> Result<Self, ClientError> {
|
||||
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);
|
||||
|
|
|
@ -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::<Http>::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::<Http>::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::<Http>::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();
|
||||
|
|
|
@ -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"] }
|
||||
|
||||
|
|
|
@ -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);
|
||||
.filter_map(|(file, source)| self.requires_solc(file, source, version))
|
||||
.collect()
|
||||
}
|
||||
|
||||
(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.
|
||||
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,6 +638,10 @@ impl<'a, T: ArtifactOutput> ArtifactsCacheInner<'a, T> {
|
|||
return true
|
||||
}
|
||||
|
||||
// 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 \"{}\"",
|
||||
|
@ -683,6 +660,7 @@ impl<'a, T: ArtifactOutput> ArtifactsCacheInner<'a, T> {
|
|||
}) {
|
||||
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)]
|
||||
|
|
|
@ -372,10 +372,17 @@ 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)?;
|
||||
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
|
||||
/// checksum from the build information published by binaries.soliditylang
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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::<Vec<_>>().join("\n ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue