contract: simplify errors and generics

This commit is contained in:
Georgios Konstantopoulos 2020-06-02 02:15:33 +03:00
parent b5a1b27e3a
commit 6bd3c41bd0
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
8 changed files with 176 additions and 82 deletions

View File

@ -18,6 +18,9 @@ thiserror = { version = "1.0.19", default-features = false }
once_cell = "1.4.0"
tokio = { version = "0.2.21", default-features = false }
[dev-dependencies]
tokio = { version = "0.2.21", default-features = false, features = ["macros"] }
[features]
default = ["abigen"]
abigen = ["ethers-contract-abigen", "ethers-contract-derive"]

View File

@ -2,22 +2,43 @@ use ethers_core::{
abi::{Detokenize, Error as AbiError, Function, InvalidOutputType},
types::{Address, BlockNumber, TransactionRequest, H256, U256},
};
use ethers_providers::{networks::Network, JsonRpcClient};
use ethers_signers::{Client, Signer};
use ethers_providers::{JsonRpcClient, ProviderError};
use ethers_signers::{Client, ClientError, Signer};
use std::{fmt::Debug, marker::PhantomData};
use thiserror::Error as ThisError;
pub struct ContractCall<'a, P, N, S, D> {
#[derive(ThisError, Debug)]
pub enum ContractError {
#[error(transparent)]
DecodingError(#[from] AbiError),
#[error(transparent)]
DetokenizationError(#[from] InvalidOutputType),
#[error(transparent)]
ClientError(#[from] ClientError),
#[error(transparent)]
ProviderError(#[from] ProviderError),
#[error("constructor is not defined in the ABI")]
ConstructorError,
#[error("Contract was not deployed")]
ContractNotDeployed,
}
pub struct ContractCall<'a, P, S, D> {
pub(crate) tx: TransactionRequest,
pub(crate) function: Function,
pub(crate) client: &'a Client<'a, P, N, S>,
pub(crate) client: &'a Client<P, S>,
pub(crate) block: Option<BlockNumber>,
pub(crate) datatype: PhantomData<D>,
}
impl<'a, P, N, S, D: Detokenize> ContractCall<'a, S, P, N, D> {
impl<'a, P, S, D: Detokenize> ContractCall<'a, P, S, D> {
/// Sets the `from` field in the transaction to the provided value
pub fn from<T: Into<Address>>(mut self, from: T) -> Self {
self.tx.from = Some(from.into());
@ -41,32 +62,18 @@ impl<'a, P, N, S, D: Detokenize> ContractCall<'a, S, P, N, D> {
self.tx.value = Some(value.into());
self
}
/// Sets the `block` field for sending the tx to the chain
pub fn block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
self.block = Some(block.into());
self
}
}
#[derive(ThisError, Debug)]
// TODO: Can we get rid of this static?
pub enum ContractError<P: JsonRpcClient>
where
P::Error: 'static,
{
#[error(transparent)]
DecodingError(#[from] AbiError),
#[error(transparent)]
DetokenizationError(#[from] InvalidOutputType),
#[error(transparent)]
CallError(P::Error),
#[error("constructor is not defined in the ABI")]
ConstructorError,
#[error("Contract was not deployed")]
ContractNotDeployed,
}
impl<'a, P, N, S, D> ContractCall<'a, P, N, S, D>
impl<'a, P, S, D> ContractCall<'a, P, S, D>
where
S: Signer,
P: JsonRpcClient,
P::Error: 'static,
N: Network,
D: Detokenize,
{
/// Queries the blockchain via an `eth_call` for the provided transaction.
@ -78,12 +85,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<D, ContractError<P>> {
let bytes = self
.client
.call(self.tx, self.block)
.await
.map_err(ContractError::CallError)?;
pub async fn call(self) -> Result<D, ContractError> {
let bytes = self.client.call(self.tx, self.block).await?;
let tokens = self.function.decode_output(&bytes.0)?;
@ -93,7 +96,7 @@ where
}
/// Signs and broadcasts the provided transaction
pub async fn send(self) -> Result<H256, P::Error> {
self.client.send_transaction(self.tx, self.block).await
pub async fn send(self) -> Result<H256, ContractError> {
Ok(self.client.send_transaction(self.tx, self.block).await?)
}
}

View File

@ -4,7 +4,7 @@ use ethers_core::{
abi::{Abi, Detokenize, Error, EventExt, Function, FunctionExt, Tokenize},
types::{Address, Filter, NameOrAddress, Selector, TransactionRequest},
};
use ethers_providers::{networks::Network, JsonRpcClient};
use ethers_providers::JsonRpcClient;
use ethers_signers::{Client, Signer};
use rustc_hex::ToHex;
@ -15,8 +15,8 @@ use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData};
// TODO: Should we separate the lifetimes for the two references?
// https://stackoverflow.com/a/29862184
#[derive(Debug, Clone)]
pub struct Contract<'a, P, N, S> {
client: &'a Client<'a, P, N, S>,
pub struct Contract<'a, P, S> {
client: &'a Client<P, S>,
abi: &'a Abi,
address: Address,
@ -27,9 +27,13 @@ pub struct Contract<'a, P, N, S> {
methods: HashMap<Selector, (String, usize)>,
}
impl<'a, P: JsonRpcClient, N: Network, S: Signer> Contract<'a, P, N, S> {
impl<'a, P, S> Contract<'a, P, S>
where
S: Signer,
P: JsonRpcClient,
{
/// Creates a new contract from the provided client, abi and address
pub fn new(client: &'a Client<'a, P, N, S>, abi: &'a Abi, address: Address) -> Self {
pub fn new(address: Address, abi: &'a Abi, client: &'a Client<P, S>) -> Self {
let methods = create_mapping(&abi.functions, |function| function.selector());
Self {
@ -43,7 +47,7 @@ impl<'a, P: JsonRpcClient, N: Network, S: Signer> Contract<'a, P, N, S> {
/// Returns an `Event` builder for the provided event name. If there are
/// multiple functions with the same name due to overloading, consider using
/// the `method_hash` method instead, since this will use the first match.
pub fn event<'b, D: Detokenize>(&'a self, name: &str) -> Result<Event<'a, 'b, P, N, D>, Error>
pub fn event<'b, D: Detokenize>(&'a self, name: &str) -> Result<Event<'a, 'b, P, D>, Error>
where
'a: 'b,
{
@ -64,7 +68,7 @@ impl<'a, P: JsonRpcClient, N: Network, S: Signer> Contract<'a, P, N, S> {
&self,
name: &str,
args: T,
) -> Result<ContractCall<'a, P, N, S, D>, Error> {
) -> Result<ContractCall<'a, P, S, D>, Error> {
// get the function
let function = self.abi.function(name)?;
self.method_func(function, args)
@ -76,7 +80,7 @@ impl<'a, P: JsonRpcClient, N: Network, S: Signer> Contract<'a, P, N, S> {
&self,
signature: Selector,
args: T,
) -> Result<ContractCall<'a, P, N, S, D>, Error> {
) -> Result<ContractCall<'a, P, S, D>, Error> {
let function = self
.methods
.get(&signature)
@ -89,7 +93,7 @@ impl<'a, P: JsonRpcClient, N: Network, S: Signer> Contract<'a, P, N, S> {
&self,
function: &Function,
args: T,
) -> Result<ContractCall<'a, P, N, S, D>, Error> {
) -> Result<ContractCall<'a, P, S, D>, Error> {
// create the calldata
let data = function.encode_input(&args.into_tokens())?;

View File

@ -1,6 +1,6 @@
use crate::ContractError;
use ethers_providers::{networks::Network, JsonRpcClient, Provider};
use ethers_providers::{JsonRpcClient, Provider};
use ethers_core::{
abi::{Detokenize, Event as AbiEvent, RawLog},
@ -9,15 +9,15 @@ use ethers_core::{
use std::{collections::HashMap, marker::PhantomData};
pub struct Event<'a, 'b, P, N, D> {
pub struct Event<'a, 'b, P, D> {
pub filter: Filter,
pub(crate) provider: &'a Provider<P, N>,
pub(crate) provider: &'a Provider<P>,
pub(crate) event: &'b AbiEvent,
pub(crate) datatype: PhantomData<D>,
}
// TODO: Improve these functions
impl<'a, 'b, P, N, D: Detokenize> Event<'a, 'b, P, N, D> {
impl<'a, 'b, P, D: Detokenize> Event<'a, 'b, P, D> {
#[allow(clippy::wrong_self_convention)]
pub fn from_block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
self.filter.from_block = Some(block.into());
@ -41,26 +41,22 @@ impl<'a, 'b, P, N, D: Detokenize> Event<'a, 'b, P, N, D> {
}
}
// TODO: Can we get rid of the static?
impl<'a, 'b, P: JsonRpcClient, N: Network, D: Detokenize + Clone> Event<'a, 'b, P, N, D>
impl<'a, 'b, P, D> Event<'a, 'b, P, D>
where
P::Error: 'static,
P: JsonRpcClient,
D: Detokenize + Clone,
{
/// Queries the blockchain for the selected filter and returns a vector of matching
/// event logs
pub async fn query(self) -> Result<Vec<D>, ContractError<P>> {
pub async fn query(self) -> Result<Vec<D>, ContractError> {
Ok(self.query_with_hashes().await?.values().cloned().collect())
}
/// Queries the blockchain for the selected filter and returns a vector of matching
/// event logs
pub async fn query_with_hashes(self) -> Result<HashMap<H256, D>, ContractError<P>> {
pub async fn query_with_hashes(self) -> Result<HashMap<H256, D>, ContractError> {
// get the logs
let logs = self
.provider
.get_logs(&self.filter)
.await
.map_err(ContractError::CallError)?;
let logs = self.provider.get_logs(&self.filter).await?;
let events = logs
.into_iter()
@ -79,7 +75,7 @@ where
.collect::<Vec<_>>();
// convert the tokens to the requested datatype
Ok::<_, ContractError<P>>((
Ok::<_, ContractError>((
log.transaction_hash.expect("should have tx hash"),
D::from_tokens(tokens)?,
))

View File

@ -4,7 +4,7 @@ use ethers_core::{
abi::{Abi, Tokenize},
types::{Bytes, TransactionRequest},
};
use ethers_providers::{networks::Network, JsonRpcClient};
use ethers_providers::JsonRpcClient;
use ethers_signers::{Client, Signer};
use std::time::Duration;
@ -15,20 +15,18 @@ use tokio::time;
const POLL_INTERVAL: u64 = 7000;
#[derive(Debug, Clone)]
pub struct Deployer<'a, P, N, S> {
client: &'a Client<'a, P, N, S>,
pub struct Deployer<'a, P, S> {
client: &'a Client<P, S>,
abi: &'a Abi,
tx: TransactionRequest,
confs: usize,
poll_interval: Duration,
}
impl<'a, P, N, S> Deployer<'a, P, N, S>
impl<'a, P, S> Deployer<'a, P, S>
where
S: Signer,
P: JsonRpcClient,
P::Error: 'static,
N: Network,
{
pub fn poll_interval<T: Into<Duration>>(mut self, interval: T) -> Self {
self.poll_interval = interval.into();
@ -40,12 +38,8 @@ where
self
}
pub async fn send(self) -> Result<Contract<'a, P, N, S>, ContractError<P>> {
let tx_hash = self
.client
.send_transaction(self.tx, None)
.await
.map_err(ContractError::CallError)?;
pub async fn send(self) -> Result<Contract<'a, P, S>, ContractError> {
let tx_hash = self.client.send_transaction(self.tx, None).await?;
// poll for the receipt
let address;
@ -60,27 +54,25 @@ where
time::delay_for(Duration::from_millis(POLL_INTERVAL)).await;
}
let contract = Contract::new(self.client, self.abi, address);
let contract = Contract::new(address, self.abi, self.client);
Ok(contract)
}
}
#[derive(Debug, Clone)]
pub struct ContractFactory<'a, P, N, S> {
client: &'a Client<'a, P, N, S>,
pub struct ContractFactory<'a, P, S> {
client: &'a Client<P, S>,
abi: &'a Abi,
bytecode: &'a Bytes,
}
impl<'a, P, N, S> ContractFactory<'a, P, N, S>
impl<'a, P, S> ContractFactory<'a, P, S>
where
S: Signer,
P: JsonRpcClient,
P::Error: 'static,
N: Network,
{
/// Instantiate a new contract factory
pub fn new(client: &'a Client<'a, P, N, S>, abi: &'a Abi, bytecode: &'a Bytes) -> Self {
pub fn new(client: &'a Client<P, S>, abi: &'a Abi, bytecode: &'a Bytes) -> Self {
Self {
client,
abi,
@ -93,7 +85,7 @@ where
pub fn deploy<T: Tokenize>(
&self,
constructor_args: T,
) -> Result<Deployer<'a, P, N, S>, ContractError<P>> {
) -> Result<Deployer<'a, P, S>, ContractError> {
// Encode the constructor args & concatenate with the bytecode if necessary
let params = constructor_args.into_tokens();
let data: Bytes = match (self.abi.constructor(), params.is_empty()) {

View File

@ -0,0 +1,78 @@
use ethers_contract::{Contract, ContractFactory};
use ethers_core::{
types::H256,
utils::{GanacheBuilder, Solc},
};
use ethers_providers::{Http, Provider};
use ethers_signers::Wallet;
use std::convert::TryFrom;
#[tokio::test]
async fn deploy_and_call_contract() {
// 1. 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
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::<Wallet>()
.unwrap();
// 4. connect to the network
let provider = Provider::<Http>::try_from(url.as_str()).unwrap();
// 5. instantiate the client with the wallet
let client = wallet.connect(provider);
// 6. 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();
// 8. get the contract's address
let addr = contract.address();
// 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();
assert_eq!(value, "initial value");
// 11. call the `setValue` method (ugly API here)
let _tx_hash = contract
.method::<_, H256>("setValue", "hi".to_owned())
.unwrap()
.send()
.await
.unwrap();
// 12. get the new value
let value: String = contract
.method("getValue", ())
.unwrap()
.call()
.await
.unwrap();
assert_eq!(value, "hi");
}

View File

@ -0,0 +1,22 @@
pragma solidity >=0.4.24;
contract SimpleStorage {
event ValueChanged(address indexed author, string oldValue, string newValue);
string _value;
constructor(string memory value) public {
emit ValueChanged(msg.sender, _value, value);
_value = value;
}
function getValue() view public returns (string memory) {
return _value;
}
function setValue(string memory value) public {
emit ValueChanged(msg.sender, _value, value);
_value = value;
}
}

View File

@ -187,10 +187,6 @@ pub mod core {
#[cfg(feature = "core")]
pub use ethers_core::utils;
// Re-export ethers_providers::networks
#[cfg(feature = "providers")]
pub use ethers_providers::networks;
/// Easy import of frequently used type definitions and traits
pub mod prelude {
#[cfg(feature = "contract")]