contract: allow connecting to many clients/addresses
This commit is contained in:
parent
6bd3c41bd0
commit
e051cffe47
|
@ -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)?;
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue