From e051cffe47e0146ebf091d2f3884a1486ddb088f Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Tue, 2 Jun 2020 13:36:02 +0300 Subject: [PATCH] contract: allow connecting to many clients/addresses --- ethers-contract/src/call.rs | 5 +- ethers-contract/src/contract.rs | 22 ++++++- ethers-contract/src/factory.rs | 10 +++- ethers-contract/tests/contract.rs | 94 ++++++++++++++++++------------ ethers-contract/tests/contract.sol | 2 + ethers-providers/src/lib.rs | 2 +- ethers-providers/src/provider.rs | 8 +-- ethers-signers/src/lib.rs | 2 +- 8 files changed, 97 insertions(+), 48 deletions(-) diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index ec8eac15..28da6ae1 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -30,6 +30,7 @@ pub enum ContractError { ContractNotDeployed, } +#[derive(Debug, Clone)] pub struct ContractCall<'a, P, S, D> { pub(crate) tx: TransactionRequest, pub(crate) function: Function, @@ -85,8 +86,8 @@ where /// and return the return type of the transaction without mutating the state /// /// Note: this function _does not_ send a transaction from your account - pub async fn call(self) -> Result { - let bytes = self.client.call(self.tx, self.block).await?; + pub async fn call(&self) -> Result { + let bytes = self.client.call(&self.tx, self.block).await?; let tokens = self.function.decode_output(&bytes.0)?; diff --git a/ethers-contract/src/contract.rs b/ethers-contract/src/contract.rs index 42b8ac44..3fbbfd08 100644 --- a/ethers-contract/src/contract.rs +++ b/ethers-contract/src/contract.rs @@ -113,13 +113,31 @@ where }) } - pub fn address(&self) -> &Address { - &self.address + pub fn address(&self) -> Address { + self.address } pub fn abi(&self) -> &Abi { &self.abi } + + /// Returns a new contract instance at `address`. + /// + /// Clones `self` internally + pub fn at>(&self, address: T) -> Self { + let mut this = self.clone(); + this.address = address.into(); + this + } + + /// Returns a new contract instance using the provided client + /// + /// Clones `self` internally + pub fn connect(&self, client: &'a Client) -> Self { + let mut this = self.clone(); + this.client = client; + this + } } /// Utility function for creating a mapping between a unique signature and a diff --git a/ethers-contract/src/factory.rs b/ethers-contract/src/factory.rs index ed2cfa5b..5833f202 100644 --- a/ethers-contract/src/factory.rs +++ b/ethers-contract/src/factory.rs @@ -17,7 +17,7 @@ const POLL_INTERVAL: u64 = 7000; #[derive(Debug, Clone)] pub struct Deployer<'a, P, S> { client: &'a Client, - abi: &'a Abi, + pub abi: &'a Abi, tx: TransactionRequest, confs: usize, poll_interval: Duration, @@ -57,6 +57,14 @@ where let contract = Contract::new(address, self.abi, self.client); Ok(contract) } + + pub fn abi(&self) -> &Abi { + &self.abi + } + + pub fn client(&self) -> &Client { + &self.client + } } #[derive(Debug, Clone)] diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs index 8eefba4e..4792eabf 100644 --- a/ethers-contract/tests/contract.rs +++ b/ethers-contract/tests/contract.rs @@ -1,6 +1,6 @@ -use ethers_contract::{Contract, ContractFactory}; +use ethers_contract::ContractFactory; use ethers_core::{ - types::H256, + types::{Address, H256}, utils::{GanacheBuilder, Solc}, }; use ethers_providers::{Http, Provider}; @@ -9,70 +9,90 @@ use std::convert::TryFrom; #[tokio::test] async fn deploy_and_call_contract() { - // 1. compile the contract + // compile the contract let compiled = Solc::new("./tests/contract.sol").build().unwrap(); let contract = compiled .get("SimpleStorage") .expect("could not find contract"); - // 2. launch ganache + // launch ganache let port = 8546u64; let url = format!("http://localhost:{}", port).to_string(); let _ganache = GanacheBuilder::new().port(port) .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") .spawn(); - // 3. instantiate our wallet - let wallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" - .parse::() - .unwrap(); - - // 4. connect to the network + // connect to the network let provider = Provider::::try_from(url.as_str()).unwrap(); - // 5. instantiate the client with the wallet - let client = wallet.connect(provider); + // instantiate our wallets + let [wallet1, wallet2]: [Wallet; 2] = [ + "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" + .parse() + .unwrap(), + "cc96601bc52293b53c4736a12af9130abf347669b3813f9ec4cafdf6991b087e" + .parse() + .unwrap(), + ]; - // 6. create a factory which will be used to deploy instances of the contract + // Instantiate the clients. We assume that clients consume the provider and the wallet + // (which makes sense), so for multi-client tests, you must clone the provider. + let client = wallet1.connect(provider.clone()); + let client2 = wallet2.connect(provider); + + // create a factory which will be used to deploy instances of the contract let factory = ContractFactory::new(&client, &contract.abi, &contract.bytecode); - // 7. deploy it with the constructor arguments - let contract = factory - .deploy("initial value".to_string()) - .unwrap() - .send() - .await - .unwrap(); + // `send` consumes the deployer so it must be cloned for later re-use + // (practically it's not expected that you'll need to deploy multiple instances of + // the _same_ deployer, so it's fine to clone here from a dev UX vs perf tradeoff) + let deployer = factory.deploy("initial value".to_string()).unwrap(); + let contract = deployer.clone().send().await.unwrap(); - // 8. get the contract's address - let addr = contract.address(); + let get_value = contract.method::<_, String>("getValue", ()).unwrap(); + let last_sender = contract.method::<_, Address>("lastSender", ()).unwrap(); - // 9. instantiate the contract - let contract = Contract::new(*addr, contract.abi(), &client); - - // 10. the initial value must be the one set in the constructor - let value: String = contract - .method("getValue", ()) - .unwrap() - .call() - .await - .unwrap(); + // the initial value must be the one set in the constructor + let value = get_value.clone().call().await.unwrap(); assert_eq!(value, "initial value"); - // 11. call the `setValue` method (ugly API here) + // make a call with `client2` let _tx_hash = contract + .connect(&client2) .method::<_, H256>("setValue", "hi".to_owned()) .unwrap() .send() .await .unwrap(); + assert_eq!(last_sender.clone().call().await.unwrap(), client2.address()); + assert_eq!(get_value.clone().call().await.unwrap(), "hi"); - // 12. get the new value - let value: String = contract - .method("getValue", ()) + // we can also call contract methods at other addresses with the `at` call + // (useful when interacting with multiple ERC20s for example) + let contract2_addr = deployer.clone().send().await.unwrap().address(); + let contract2 = contract.at(contract2_addr); + let init_value: String = contract2 + .method::<_, String>("getValue", ()) .unwrap() .call() .await .unwrap(); - assert_eq!(value, "hi"); + let init_address = contract2 + .method::<_, Address>("lastSender", ()) + .unwrap() + .call() + .await + .unwrap(); + assert_eq!(init_address, Address::zero()); + assert_eq!(init_value, "initial value"); + + // we can still interact with the old contract instance + let _tx_hash = contract + .method::<_, H256>("setValue", "hi2".to_owned()) + .unwrap() + .send() + .await + .unwrap(); + assert_eq!(last_sender.clone().call().await.unwrap(), client.address()); + assert_eq!(get_value.clone().call().await.unwrap(), "hi2"); } diff --git a/ethers-contract/tests/contract.sol b/ethers-contract/tests/contract.sol index 9d04f2f4..306e5fba 100644 --- a/ethers-contract/tests/contract.sol +++ b/ethers-contract/tests/contract.sol @@ -4,6 +4,7 @@ contract SimpleStorage { event ValueChanged(address indexed author, string oldValue, string newValue); + address public lastSender; string _value; constructor(string memory value) public { @@ -18,5 +19,6 @@ contract SimpleStorage { function setValue(string memory value) public { emit ValueChanged(msg.sender, _value, value); _value = value; + lastSender = msg.sender; } } diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs index 04de3a69..fef9164c 100644 --- a/ethers-providers/src/lib.rs +++ b/ethers-providers/src/lib.rs @@ -17,7 +17,7 @@ pub type HttpProvider = Provider; #[async_trait] /// Trait which must be implemented by data transports to be used with the Ethereum /// JSON-RPC provider. -pub trait JsonRpcClient: Debug { +pub trait JsonRpcClient: Debug + Clone { /// A JSON-RPC Error type Error: Error + Into; diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs index 22365484..83c00d32 100644 --- a/ethers-providers/src/provider.rs +++ b/ethers-providers/src/provider.rs @@ -186,10 +186,10 @@ impl Provider

{ /// This is free, since it does not change any state on the blockchain. pub async fn call( &self, - tx: TransactionRequest, + tx: &TransactionRequest, block: Option, ) -> Result { - let tx = utils::serialize(&tx); + let tx = utils::serialize(tx); let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest)); Ok(self .0 @@ -311,7 +311,7 @@ impl Provider

{ // first get the resolver responsible for this name // the call will return a Bytes array which we convert to an address let data = self - .call(ens::get_resolver(ens_addr, ens_name), None) + .call(&ens::get_resolver(ens_addr, ens_name), None) .await?; let resolver_address: Address = decode_bytes(ParamType::Address, data); @@ -321,7 +321,7 @@ impl Provider

{ // resolve let data = self - .call(ens::resolve(resolver_address, selector, ens_name), None) + .call(&ens::resolve(resolver_address, selector, ens_name), None) .await?; Ok(Some(decode_bytes(param, data))) diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index 1e9ac950..124faaa5 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -12,7 +12,7 @@ use std::error::Error; /// /// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. // TODO: We might need a `SignerAsync` trait for HSM use cases? -pub trait Signer { +pub trait Signer: Clone { type Error: Error + Into; /// Signs the hash of the provided message after prefixing it fn sign_message>(&self, message: S) -> Signature;