Repository: gakonst/ethers-rs
Pull request #2072 (2023-01-24 14:15:26)

fix(provider): Add Send bound to return type of JsonRpcClient::request
## Motivation

`#[async_trait]` implementations require return types to be `Send`. So except for trivial cases the `JsonRpcClient::request` demanded implementors to be impossibly generic.

## Solution

Add `Send` bound to the method and follow-up on its callers inside the monorepo.

## PR Checklist

-   [ ] Added Tests
-   [ ] Added Documentation
-   [ ] Updated the changelog
-   [ ] Breaking changes



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43f7a742..214b2531 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 ### Unreleased
 
+- Add a `Send` bound to the `IntoFuture` implementation of `ContractCall` [#2083](https://github.com/gakonst/ethers-rs/pull/2083)
 - Bump [`svm-rs`](https://github.com/roynalnaruto/svm-rs) dependency to fix conflicts with Rust Crytpo packages [#2051](https://github.com/gakonst/ethers-rs/pull/2051)
 - Avoid unnecessary allocations in `utils` [#2046](https://github.com/gakonst/ethers-rs/pull/2046)
 - Add abigen support for hardhat generated bytecode json format [#2012](https://github.com/gakonst/ethers-rs/pull/2012)
diff --git a/Cargo.lock b/Cargo.lock
index efea7cbb..57421658 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1409,6 +1409,7 @@ dependencies = [
  "elliptic-curve",
  "ethabi",
  "generic-array 0.14.6",
+ "getrandom 0.2.8",
  "hex",
  "hex-literal",
  "k256",
@@ -2679,18 +2680,18 @@ dependencies = [
 
 [[package]]
 name = "num_enum"
-version = "0.5.7"
+version = "0.5.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9"
+checksum = "8d829733185c1ca374f17e52b762f24f535ec625d2cc1f070e34c8a9068f341b"
 dependencies = [
  "num_enum_derive",
 ]
 
 [[package]]
 name = "num_enum_derive"
-version = "0.5.7"
+version = "0.5.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce"
+checksum = "2be1598bf1c313dcdd12092e3f1920f463462525a21b7b4e11b4168353d0123e"
 dependencies = [
  "proc-macro-crate",
  "proc-macro2",
@@ -3372,11 +3373,11 @@ dependencies = [
 
 [[package]]
 name = "reqwest"
-version = "0.11.13"
+version = "0.11.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c"
+checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9"
 dependencies = [
- "base64 0.13.1",
+ "base64 0.21.0",
  "bytes",
  "encoding_rs",
  "futures-core",
diff --git a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
index 0f6ef3a1..661e9e37 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs
@@ -100,7 +100,7 @@ impl Context {
             };
 
             if is_tuple {
-                fields.push(ty);
+                fields.push(quote!(pub #ty));
             } else {
                 let field_name = util::safe_ident(&field.name().to_snake_case());
                 fields.push(quote! { pub #field_name: #ty });
@@ -331,20 +331,35 @@ impl InternalStructs {
     /// Returns the name of the rust type that will be generated if the given input is a struct
     /// NOTE: this does not account for arrays or fixed arrays
     pub fn get_function_input_struct_type(&self, function: &str, input: &str) -> Option<&str> {
-        let key = (function.to_string(), input.to_string());
-        self.function_params
-            .get(&key)
+        self.get_function_input_struct_solidity_id(function, input)
             .and_then(|id| self.rust_type_names.get(id))
             .map(String::as_str)
     }
 
+    /// Returns solidity type identifier as it's used in the ABI.
+    pub fn get_function_input_struct_solidity_id(
+        &self,
+        function: &str,
+        input: &str,
+    ) -> Option<&str> {
+        let key = (function.to_string(), input.to_string());
+        self.function_params.get(&key).map(String::as_str)
+    }
+
     /// Returns the name of the rust type that will be generated if the given input is a struct
     /// This takes the index of event's parameter instead of the parameter's name like
     /// [`Self::get_function_input_struct_type`] does because we can't rely on the name since events
     /// support nameless parameters NOTE: this does not account for arrays or fixed arrays
     pub fn get_event_input_struct_type(&self, event: &str, idx: usize) -> Option<&str> {
+        self.get_event_input_struct_solidity_id(event, idx)
+            .and_then(|id| self.rust_type_names.get(id))
+            .map(String::as_str)
+    }
+
+    /// Returns the type identifier as it's used in the solidity ABI
+    pub fn get_event_input_struct_solidity_id(&self, event: &str, idx: usize) -> Option<&str> {
         let key = (event.to_string(), idx);
-        self.event_params.get(&key).and_then(|id| self.rust_type_names.get(id)).map(String::as_str)
+        self.event_params.get(&key).map(String::as_str)
     }
 
     /// Returns the name of the rust type that will be generated if the given output is a struct
@@ -353,13 +368,24 @@ impl InternalStructs {
         &self,
         function: &str,
         internal_type: &str,
+    ) -> Option<&str> {
+        self.get_function_output_struct_solidity_id(function, internal_type)
+            .and_then(|id| self.rust_type_names.get(id))
+            .map(String::as_str)
+    }
+
+    /// Returns the name of the rust type that will be generated if the given output is a struct
+    /// NOTE: this does not account for arrays or fixed arrays
+    pub fn get_function_output_struct_solidity_id(
+        &self,
+        function: &str,
+        internal_type: &str,
     ) -> Option<&str> {
         self.outputs
             .get(function)
             .and_then(|outputs| {
                 outputs.iter().find(|s| s.as_str() == struct_type_identifier(internal_type))
             })
-            .and_then(|id| self.rust_type_names.get(id))
             .map(String::as_str)
     }
 
@@ -374,6 +400,8 @@ impl InternalStructs {
     }
 
     /// Returns all the solidity struct types
+    ///
+    /// These are grouped by their case-sensitive type identifiers extracted from the ABI.
     pub fn structs_types(&self) -> &HashMap<String, SolStruct> {
         &self.structs
     }
@@ -562,7 +590,7 @@ fn struct_type_name(name: &str) -> &str {
 }
 
 /// `Pairing.G2Point` -> `Pairing.G2Point`
-fn struct_type_identifier(name: &str) -> &str {
+pub fn struct_type_identifier(name: &str) -> &str {
     name.trim_start_matches("struct ").split('[').next().unwrap()
 }
 
diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs
index c9a8ab63..0c5a4985 100644
--- a/ethers-contract/src/call.rs
+++ b/ethers-contract/src/call.rs
@@ -225,10 +225,10 @@ impl<M, D> IntoFuture for ContractCall<M, D>
 where
     Self: 'static,
     M: Middleware,
-    D: Detokenize,
+    D: Detokenize + Send + Sync,
 {
     type Output = Result<D, ContractError<M>>;
-    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output>>>;
+    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
 
     fn into_future(self) -> Self::IntoFuture {
         Box::pin(async move { self.call().await })
diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml
index e2513bbb..f03ae067 100644
--- a/ethers-core/Cargo.toml
+++ b/ethers-core/Cargo.toml
@@ -42,11 +42,15 @@ cargo_metadata = { version = "0.15.2", optional = true }
 convert_case = { version = "0.6.0", optional = true }
 syn = { version = "1.0.107", optional = true }
 proc-macro2 = { version = "1.0.50", optional = true }
-num_enum = "0.5.7"
+num_enum = "0.5.9"
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 tempfile = { version = "3.3.0", default-features = false }
 
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+# NOTE: this enables wasm compatibility for getrandom indirectly
+getrandom = { version = "0.2", features = ["js"] }
+
 [dev-dependencies]
 serde_json = { version = "1.0.64", default-features = false }
 bincode = { version = "1.3.3", default-features = false }
diff --git a/ethers-core/src/lib.rs b/ethers-core/src/lib.rs
index f9b4d2ac..446a58f8 100644
--- a/ethers-core/src/lib.rs
+++ b/ethers-core/src/lib.rs
@@ -1,7 +1,7 @@
 #![cfg_attr(docsrs, feature(doc_cfg))]
 #![doc = include_str!("../README.md")]
 #![deny(rustdoc::broken_intra_doc_links)]
-#![deny(unused_crate_dependencies)]
+#![cfg_attr(not(target_arch = "wasm32"), deny(unused_crate_dependencies))]
 
 pub mod types;
 
diff --git a/ethers-etherscan/Cargo.toml b/ethers-etherscan/Cargo.toml
index 8de6d5ac..1c5b373d 100644
--- a/ethers-etherscan/Cargo.toml
+++ b/ethers-etherscan/Cargo.toml
@@ -20,7 +20,7 @@ keywords = ["ethereum", "web3", "etherscan", "ethers"]
 [dependencies]
 ethers-core = { version = "^1.0.0", path = "../ethers-core", default-features = false }
 ethers-solc = { version = "^1.0.0", path = "../ethers-solc", default-features = false, optional = true }
-reqwest = { version = "0.11.13", default-features = false, features = ["json"] }
+reqwest = { version = "0.11.14", default-features = false, features = ["json"] }
 serde = { version = "1.0.124", default-features = false, features = ["derive"] }
 serde_json = { version = "1.0.64", default-features = false }
 serde-aux = { version = "4.1.2", default-features = false }
diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml
index 08c79162..741999cf 100644
--- a/ethers-middleware/Cargo.toml
+++ b/ethers-middleware/Cargo.toml
@@ -31,7 +31,7 @@ tracing = { version = "0.1.37", default-features = false }
 tracing-futures = { version = "0.2.5", default-features = false }
 
 # for gas oracles
-reqwest = { version = "0.11.13", default-features = false, features = ["json", "rustls-tls"] }
+reqwest = { version = "0.11.14", default-features = false, features = ["json", "rustls-tls"] }
 url = { version = "2.3.1", default-features = false }
 
 serde_json = { version = "1.0.64", default-features = false }
@@ -50,7 +50,7 @@ ethers-providers = { version = "^1.0.0", path = "../ethers-providers", default-f
 once_cell = "1.17.0"
 ethers-solc = { version = "^1.0.0", path = "../ethers-solc" }
 serial_test = "0.10.0"
-reqwest = { version = "0.11.13", default-features = false, features = ["json", "rustls"] }
+reqwest = { version = "0.11.14", default-features = false, features = ["json", "rustls"] }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
 tokio = { version = "1.18", default-features = false, features = ["rt", "macros", "time"] }
diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml
index a86112ed..cb76aa32 100644
--- a/ethers-providers/Cargo.toml
+++ b/ethers-providers/Cargo.toml
@@ -19,7 +19,7 @@ ethers-core = { version = "^1.0.0", path = "../ethers-core", default-features =
 
 async-trait = { version = "0.1.50", default-features = false }
 hex = { version = "0.4.3", default-features = false, features = ["std"] }
-reqwest = { version = "0.11.13", default-features = false, features = ["json"] }
+reqwest = { version = "0.11.14", default-features = false, features = ["json"] }
 serde = { version = "1.0.124", default-features = false, features = ["derive"] }
 serde_json = { version = "1.0.64", default-features = false, features = ["raw_value"] }
 thiserror = { version = "1.0", default-features = false }
diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs
index 98f59842..d5c34da7 100644
--- a/ethers-providers/src/lib.rs
+++ b/ethers-providers/src/lib.rs
@@ -76,7 +76,7 @@ pub trait JsonRpcClient: Debug + Send + Sync {
     async fn request<T, R>(&self, method: &str, params: T) -> Result<R, Self::Error>
     where
         T: Debug + Serialize + Send + Sync,
-        R: DeserializeOwned;
+        R: DeserializeOwned + Send;
 }
 
 pub trait FromErr<T> {
diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs
index 54fd646e..0ade5eb3 100644
--- a/ethers-providers/src/provider.rs
+++ b/ethers-providers/src/provider.rs
@@ -199,7 +199,7 @@ impl<P: JsonRpcClient> Provider<P> {
     pub async fn request<T, R>(&self, method: &str, params: T) -> Result<R, ProviderError>
     where
         T: Debug + Serialize + Send + Sync,
-        R: Serialize + DeserializeOwned + Debug,
+        R: Serialize + DeserializeOwned + Debug + Send,
     {
         let span =
             tracing::trace_span!("rpc", method = method, params = ?serde_json::to_string(&params)?);
@@ -215,7 +215,7 @@ impl<P: JsonRpcClient> Provider<P> {
         Ok(res)
     }
 
-    async fn get_block_gen<Tx: Default + Serialize + DeserializeOwned + Debug>(
+    async fn get_block_gen<Tx: Default + Serialize + DeserializeOwned + Debug + Send>(
         &self,
         id: BlockId,
         include_txs: bool,
diff --git a/ethers-providers/src/transports/retry.rs b/ethers-providers/src/transports/retry.rs
index 005fe0b5..8b51573c 100644
--- a/ethers-providers/src/transports/retry.rs
+++ b/ethers-providers/src/transports/retry.rs
@@ -238,7 +238,7 @@ where
     async fn request<A, R>(&self, method: &str, params: A) -> Result<R, Self::Error>
     where
         A: Debug + Serialize + Send + Sync,
-        R: DeserializeOwned,
+        R: DeserializeOwned + Send,
     {
         // Helper type that caches the `params` value across several retries
         // This is necessary because the wrapper provider is supposed to skip he `params` if it's of
diff --git a/ethers-providers/src/transports/rw.rs b/ethers-providers/src/transports/rw.rs
index f3c649d2..945c2c52 100644
--- a/ethers-providers/src/transports/rw.rs
+++ b/ethers-providers/src/transports/rw.rs
@@ -105,14 +105,10 @@ where
 
     /// Sends a POST request with the provided method and the params serialized as JSON
     /// over HTTP
-    async fn request<T: Serialize + Send + Sync, R: DeserializeOwned>(
-        &self,
-        method: &str,
-        params: T,
-    ) -> Result<R, Self::Error>
+    async fn request<T, R>(&self, method: &str, params: T) -> Result<R, Self::Error>
     where
         T: std::fmt::Debug + Serialize + Send + Sync,
-        R: DeserializeOwned,
+        R: DeserializeOwned + Send,
     {
         match method {
             "eth_sendTransaction" | "eth_sendRawTransaction" => {
diff --git a/ethers-solc/src/artifact_output/configurable.rs b/ethers-solc/src/artifact_output/configurable.rs
index 3ed99e32..0c87731d 100644
--- a/ethers-solc/src/artifact_output/configurable.rs
+++ b/ethers-solc/src/artifact_output/configurable.rs
@@ -13,15 +13,15 @@ use crate::{
         bytecode::{CompactBytecode, CompactDeployedBytecode},
         contract::{CompactContract, CompactContractBytecode, Contract},
         output_selection::{
-            BytecodeOutputSelection, ContractOutputSelection, EvmOutputSelection,
-            EwasmOutputSelection,
+            BytecodeOutputSelection, ContractOutputSelection, DeployedBytecodeOutputSelection,
+            EvmOutputSelection, EwasmOutputSelection,
         },
         Ast, CompactContractBytecodeCow, DevDoc, Evm, Ewasm, FunctionDebugData, GasEstimates,
         GeneratedSource, LosslessAbi, LosslessMetadata, Metadata, Offsets, Settings, StorageLayout,
         UserDoc,
     },
     sources::VersionedSourceFile,
-    ArtifactOutput, SolcConfig, SolcError, SourceFile,
+    Artifact, ArtifactOutput, SolcConfig, SolcError, SourceFile,
 };
 use serde::{Deserialize, Serialize};
 use std::{borrow::Cow, collections::BTreeMap, fs, path::Path};
@@ -509,6 +509,8 @@ pub struct ExtraOutputFiles {
     pub assembly: bool,
     pub source_map: bool,
     pub generated_sources: bool,
+    pub bytecode: bool,
+    pub deployed_bytecode: bool,
 
     /// PRIVATE: This structure may grow, As such, constructing this structure should
     /// _always_ be done using a public constructor or update syntax:
@@ -537,6 +539,8 @@ impl ExtraOutputFiles {
             assembly: true,
             source_map: true,
             generated_sources: true,
+            bytecode: true,
+            deployed_bytecode: true,
             __non_exhaustive: (),
         }
     }
@@ -565,6 +569,8 @@ impl ExtraOutputFiles {
                         config.assembly = true;
                         config.generated_sources = true;
                         config.source_map = true;
+                        config.bytecode = true;
+                        config.deployed_bytecode = true;
                     }
                     EvmOutputSelection::Assembly => {
                         config.assembly = true;
@@ -572,9 +578,18 @@ impl ExtraOutputFiles {
                     EvmOutputSelection::ByteCode(BytecodeOutputSelection::GeneratedSources) => {
                         config.generated_sources = true;
                     }
+                    EvmOutputSelection::ByteCode(BytecodeOutputSelection::Object) => {
+                        config.bytecode = true;
+                    }
                     EvmOutputSelection::ByteCode(BytecodeOutputSelection::SourceMap) => {
                         config.source_map = true;
                     }
+                    EvmOutputSelection::DeployedByteCode(DeployedBytecodeOutputSelection::All) |
+                    EvmOutputSelection::DeployedByteCode(
+                        DeployedBytecodeOutputSelection::Object,
+                    ) => {
+                        config.deployed_bytecode = true;
+                    }
                     _ => {}
                 },
                 ContractOutputSelection::Ewasm(_) => {
@@ -656,6 +671,21 @@ impl ExtraOutputFiles {
             }
         }
 
+        if self.bytecode {
+            if let Some(ref code) = contract.get_bytecode_bytes() {
+                let code = hex::encode(code.as_ref());
+                let file = file.with_extension("bin");
+                fs::write(&file, code).map_err(|err| SolcError::io(err, file))?
+            }
+        }
+        if self.deployed_bytecode {
+            if let Some(ref code) = contract.get_deployed_bytecode_bytes() {
+                let code = hex::encode(code.as_ref());
+                let file = file.with_extension("deployed-bin");
+                fs::write(&file, code).map_err(|err| SolcError::io(err, file))?
+            }
+        }
+
         Ok(())
     }
 }
diff --git a/ethers-solc/src/artifacts/output_selection.rs b/ethers-solc/src/artifacts/output_selection.rs
index b6017d91..769b4d4b 100644
--- a/ethers-solc/src/artifacts/output_selection.rs
+++ b/ethers-solc/src/artifacts/output_selection.rs
@@ -407,7 +407,7 @@ impl FromStr for BytecodeOutputSelection {
         match s {
             "evm.bytecode" => Ok(BytecodeOutputSelection::All),
             "evm.bytecode.functionDebugData" => Ok(BytecodeOutputSelection::FunctionDebugData),
-            "evm.bytecode.object" => Ok(BytecodeOutputSelection::Object),
+            "code" | "bin" | "evm.bytecode.object" => Ok(BytecodeOutputSelection::Object),
             "evm.bytecode.opcodes" => Ok(BytecodeOutputSelection::Opcodes),
             "evm.bytecode.sourceMap" => Ok(BytecodeOutputSelection::SourceMap),
             "evm.bytecode.linkReferences" => Ok(BytecodeOutputSelection::LinkReferences),
@@ -482,6 +482,10 @@ impl FromStr for DeployedBytecodeOutputSelection {
             "evm.deployedBytecode.functionDebugData" => {
                 Ok(DeployedBytecodeOutputSelection::FunctionDebugData)
             }
+            "deployed-code" |
+            "deployed-bin" |
+            "runtime-code" |
+            "runtime-bin" |
             "evm.deployedBytecode.object" => Ok(DeployedBytecodeOutputSelection::Object),
             "evm.deployedBytecode.opcodes" => Ok(DeployedBytecodeOutputSelection::Opcodes),
             "evm.deployedBytecode.sourceMap" => Ok(DeployedBytecodeOutputSelection::SourceMap),
diff --git a/examples/providers/Cargo.toml b/examples/providers/Cargo.toml
index 337e4e1d..f8c0db21 100644
--- a/examples/providers/Cargo.toml
+++ b/examples/providers/Cargo.toml
@@ -12,7 +12,7 @@ ipc = []
 ethers = { path = "../..", version = "1.0.0", features = ["abigen", "ipc", "rustls", "ws"] }
 
 eyre = "0.6"
-reqwest = { version = "0.11.13", default-features = false }
+reqwest = { version = "0.11.14", default-features = false }
 serde = { version = "1.0.144", features = ["derive"] }
 serde_json = "1.0.64"
 tokio = { version = "1.18", features = ["macros"] }


Test patch:
diff --git a/ethers-contract/tests/it/abigen.rs b/ethers-contract/tests/it/abigen.rs
index 0dacb25b..7658a841 100644
--- a/ethers-contract/tests/it/abigen.rs
+++ b/ethers-contract/tests/it/abigen.rs
@@ -752,6 +752,7 @@ fn can_handle_overloaded_function_with_array() {
 }
 
 #[test]
+#[allow(clippy::disallowed_names)]
 fn convert_uses_correct_abi() {
     abigen!(
         Foo, r#"[function foo()]"#;
diff --git a/ethers-contract/tests/it/contract_call.rs b/ethers-contract/tests/it/contract_call.rs
new file mode 100644
index 00000000..c1708e04
--- /dev/null
+++ b/ethers-contract/tests/it/contract_call.rs
@@ -0,0 +1,21 @@
+use ethers_contract_derive::abigen;
+use ethers_core::abi::Address;
+use ethers_providers::Provider;
+use std::{
+    future::{Future, IntoFuture},
+    sync::Arc,
+};
+
+#[tokio::test]
+async fn contract_call_into_future_is_send() {
+    abigen!(DsProxyFactory, "ethers-middleware/contracts/DsProxyFactory.json");
+    let (provider, _) = Provider::mocked();
+    let client = Arc::new(provider);
+    let contract = DsProxyFactory::new(Address::zero(), client);
+
+    fn is_send<T: Future + Send + 'static>(future: T) -> bool {
+        true
+    }
+
+    assert!(is_send(contract.cache().into_future()));
+}
diff --git a/ethers-contract/tests/it/main.rs b/ethers-contract/tests/it/main.rs
index 4563e571..63414849 100644
--- a/ethers-contract/tests/it/main.rs
+++ b/ethers-contract/tests/it/main.rs
@@ -6,5 +6,6 @@ pub(crate) mod common;
 mod console;
 #[cfg(feature = "abigen")]
 mod contract;
+mod contract_call;
 
 fn main() {}