contract: simplify errors and generics
This commit is contained in:
parent
b5a1b27e3a
commit
6bd3c41bd0
|
@ -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"]
|
||||
|
|
|
@ -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?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())?;
|
||||
|
||||
|
|
|
@ -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)?,
|
||||
))
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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");
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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")]
|
||||
|
|
Loading…
Reference in New Issue