contract: allow connecting to many clients/addresses

This commit is contained in:
Georgios Konstantopoulos 2020-06-02 13:36:02 +03:00
parent 6bd3c41bd0
commit e051cffe47
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
8 changed files with 97 additions and 48 deletions

View File

@ -30,6 +30,7 @@ pub enum ContractError {
ContractNotDeployed, ContractNotDeployed,
} }
#[derive(Debug, Clone)]
pub struct ContractCall<'a, P, S, D> { pub struct ContractCall<'a, P, S, D> {
pub(crate) tx: TransactionRequest, pub(crate) tx: TransactionRequest,
pub(crate) function: Function, pub(crate) function: Function,
@ -85,8 +86,8 @@ where
/// and return the return type of the transaction without mutating the state /// and return the return type of the transaction without mutating the state
/// ///
/// Note: this function _does not_ send a transaction from your account /// Note: this function _does not_ send a transaction from your account
pub async fn call(self) -> Result<D, ContractError> { pub async fn call(&self) -> Result<D, ContractError> {
let bytes = self.client.call(self.tx, self.block).await?; let bytes = self.client.call(&self.tx, self.block).await?;
let tokens = self.function.decode_output(&bytes.0)?; let tokens = self.function.decode_output(&bytes.0)?;

View File

@ -113,13 +113,31 @@ where
}) })
} }
pub fn address(&self) -> &Address { pub fn address(&self) -> Address {
&self.address self.address
} }
pub fn abi(&self) -> &Abi { pub fn abi(&self) -> &Abi {
&self.abi &self.abi
} }
/// Returns a new contract instance at `address`.
///
/// Clones `self` internally
pub fn at<T: Into<Address>>(&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<P, S>) -> Self {
let mut this = self.clone();
this.client = client;
this
}
} }
/// Utility function for creating a mapping between a unique signature and a /// Utility function for creating a mapping between a unique signature and a

View File

@ -17,7 +17,7 @@ const POLL_INTERVAL: u64 = 7000;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Deployer<'a, P, S> { pub struct Deployer<'a, P, S> {
client: &'a Client<P, S>, client: &'a Client<P, S>,
abi: &'a Abi, pub abi: &'a Abi,
tx: TransactionRequest, tx: TransactionRequest,
confs: usize, confs: usize,
poll_interval: Duration, poll_interval: Duration,
@ -57,6 +57,14 @@ where
let contract = Contract::new(address, self.abi, self.client); let contract = Contract::new(address, self.abi, self.client);
Ok(contract) Ok(contract)
} }
pub fn abi(&self) -> &Abi {
&self.abi
}
pub fn client(&self) -> &Client<P, S> {
&self.client
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -1,6 +1,6 @@
use ethers_contract::{Contract, ContractFactory}; use ethers_contract::ContractFactory;
use ethers_core::{ use ethers_core::{
types::H256, types::{Address, H256},
utils::{GanacheBuilder, Solc}, utils::{GanacheBuilder, Solc},
}; };
use ethers_providers::{Http, Provider}; use ethers_providers::{Http, Provider};
@ -9,70 +9,90 @@ use std::convert::TryFrom;
#[tokio::test] #[tokio::test]
async fn deploy_and_call_contract() { async fn deploy_and_call_contract() {
// 1. compile the contract // compile the contract
let compiled = Solc::new("./tests/contract.sol").build().unwrap(); let compiled = Solc::new("./tests/contract.sol").build().unwrap();
let contract = compiled let contract = compiled
.get("SimpleStorage") .get("SimpleStorage")
.expect("could not find contract"); .expect("could not find contract");
// 2. launch ganache // launch ganache
let port = 8546u64; let port = 8546u64;
let url = format!("http://localhost:{}", port).to_string(); let url = format!("http://localhost:{}", port).to_string();
let _ganache = GanacheBuilder::new().port(port) let _ganache = GanacheBuilder::new().port(port)
.mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle")
.spawn(); .spawn();
// 3. instantiate our wallet // connect to the network
let wallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
.parse::<Wallet>()
.unwrap();
// 4. connect to the network
let provider = Provider::<Http>::try_from(url.as_str()).unwrap(); let provider = Provider::<Http>::try_from(url.as_str()).unwrap();
// 5. instantiate the client with the wallet // instantiate our wallets
let client = wallet.connect(provider); 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); let factory = ContractFactory::new(&client, &contract.abi, &contract.bytecode);
// 7. deploy it with the constructor arguments // `send` consumes the deployer so it must be cloned for later re-use
let contract = factory // (practically it's not expected that you'll need to deploy multiple instances of
.deploy("initial value".to_string()) // the _same_ deployer, so it's fine to clone here from a dev UX vs perf tradeoff)
.unwrap() let deployer = factory.deploy("initial value".to_string()).unwrap();
.send() let contract = deployer.clone().send().await.unwrap();
.await
.unwrap();
// 8. get the contract's address let get_value = contract.method::<_, String>("getValue", ()).unwrap();
let addr = contract.address(); let last_sender = contract.method::<_, Address>("lastSender", ()).unwrap();
// 9. instantiate the contract // the initial value must be the one set in the constructor
let contract = Contract::new(*addr, contract.abi(), &client); let value = get_value.clone().call().await.unwrap();
// 10. the initial value must be the one set in the constructor
let value: String = contract
.method("getValue", ())
.unwrap()
.call()
.await
.unwrap();
assert_eq!(value, "initial value"); assert_eq!(value, "initial value");
// 11. call the `setValue` method (ugly API here) // make a call with `client2`
let _tx_hash = contract let _tx_hash = contract
.connect(&client2)
.method::<_, H256>("setValue", "hi".to_owned()) .method::<_, H256>("setValue", "hi".to_owned())
.unwrap() .unwrap()
.send() .send()
.await .await
.unwrap(); .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 // we can also call contract methods at other addresses with the `at` call
let value: String = contract // (useful when interacting with multiple ERC20s for example)
.method("getValue", ()) let contract2_addr = deployer.clone().send().await.unwrap().address();
let contract2 = contract.at(contract2_addr);
let init_value: String = contract2
.method::<_, String>("getValue", ())
.unwrap() .unwrap()
.call() .call()
.await .await
.unwrap(); .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");
} }

View File

@ -4,6 +4,7 @@ contract SimpleStorage {
event ValueChanged(address indexed author, string oldValue, string newValue); event ValueChanged(address indexed author, string oldValue, string newValue);
address public lastSender;
string _value; string _value;
constructor(string memory value) public { constructor(string memory value) public {
@ -18,5 +19,6 @@ contract SimpleStorage {
function setValue(string memory value) public { function setValue(string memory value) public {
emit ValueChanged(msg.sender, _value, value); emit ValueChanged(msg.sender, _value, value);
_value = value; _value = value;
lastSender = msg.sender;
} }
} }

View File

@ -17,7 +17,7 @@ pub type HttpProvider = Provider<Http>;
#[async_trait] #[async_trait]
/// Trait which must be implemented by data transports to be used with the Ethereum /// Trait which must be implemented by data transports to be used with the Ethereum
/// JSON-RPC provider. /// JSON-RPC provider.
pub trait JsonRpcClient: Debug { pub trait JsonRpcClient: Debug + Clone {
/// A JSON-RPC Error /// A JSON-RPC Error
type Error: Error + Into<ProviderError>; type Error: Error + Into<ProviderError>;

View File

@ -186,10 +186,10 @@ impl<P: JsonRpcClient> Provider<P> {
/// This is free, since it does not change any state on the blockchain. /// This is free, since it does not change any state on the blockchain.
pub async fn call( pub async fn call(
&self, &self,
tx: TransactionRequest, tx: &TransactionRequest,
block: Option<BlockNumber>, block: Option<BlockNumber>,
) -> Result<Bytes, ProviderError> { ) -> Result<Bytes, ProviderError> {
let tx = utils::serialize(&tx); let tx = utils::serialize(tx);
let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest)); let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest));
Ok(self Ok(self
.0 .0
@ -311,7 +311,7 @@ impl<P: JsonRpcClient> Provider<P> {
// first get the resolver responsible for this name // first get the resolver responsible for this name
// the call will return a Bytes array which we convert to an address // the call will return a Bytes array which we convert to an address
let data = self let data = self
.call(ens::get_resolver(ens_addr, ens_name), None) .call(&ens::get_resolver(ens_addr, ens_name), None)
.await?; .await?;
let resolver_address: Address = decode_bytes(ParamType::Address, data); let resolver_address: Address = decode_bytes(ParamType::Address, data);
@ -321,7 +321,7 @@ impl<P: JsonRpcClient> Provider<P> {
// resolve // resolve
let data = self let data = self
.call(ens::resolve(resolver_address, selector, ens_name), None) .call(&ens::resolve(resolver_address, selector, ens_name), None)
.await?; .await?;
Ok(Some(decode_bytes(param, data))) Ok(Some(decode_bytes(param, data)))

View File

@ -12,7 +12,7 @@ use std::error::Error;
/// ///
/// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. /// Implement this trait to support different signing modes, e.g. Ledger, hosted etc.
// TODO: We might need a `SignerAsync` trait for HSM use cases? // TODO: We might need a `SignerAsync` trait for HSM use cases?
pub trait Signer { pub trait Signer: Clone {
type Error: Error + Into<ClientError>; type Error: Error + Into<ClientError>;
/// Signs the hash of the provided message after prefixing it /// Signs the hash of the provided message after prefixing it
fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Signature; fn sign_message<S: AsRef<[u8]>>(&self, message: S) -> Signature;