specify the datatype when creating the call

This commit is contained in:
Georgios Konstantopoulos 2020-05-26 17:43:51 +03:00
parent c46442dd12
commit 25f2c5e45d
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
5 changed files with 132 additions and 43 deletions

View File

@ -5,6 +5,8 @@ authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
edition = "2018"
[dependencies]
ethers-contract-abigen = { path = "ethers-contract-abigen", optional = true }
ethers-abi = { path = "../ethers-abi" }
ethers-providers = { path = "../ethers-providers" }
ethers-signers = { path = "../ethers-signers" }
@ -12,3 +14,8 @@ ethers-types = { path = "../ethers-types" }
serde = { version = "1.0.110", default-features = false }
rustc-hex = { version = "2.1.0", default-features = false }
thiserror = { version = "1.0.19", default-features = false }
[features]
default = []
abigen = ["ethers-contract-abigen"]

View File

@ -8,8 +8,8 @@ use ethers_types::{
};
use rustc_hex::ToHex;
use serde::Deserialize;
use std::{collections::HashMap, fmt::Debug, hash::Hash};
use thiserror::Error as ThisError;
/// Represents a contract instance at an address. Provides methods for
/// contract interaction.
@ -26,6 +26,8 @@ pub struct Contract<'a, S, P> {
methods: HashMap<Selector, (String, usize)>,
}
use std::marker::PhantomData;
impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
/// Creates a new contract from the provided client, abi and address
pub fn new(client: &'a Client<'a, S, P>, abi: Abi, address: Address) -> Self {
@ -42,7 +44,7 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
/// Returns a transaction builder for the provided function 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>(&'a self, name: &str) -> Result<Event<'a, 'b, P>, Error>
pub fn event<'b, D: Detokenize>(&'a self, name: &str) -> Result<Event<'a, 'b, P, D>, Error>
where
'a: 'b,
{
@ -52,13 +54,18 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
provider: &self.client.provider(),
filter: Filter::new().event(&event.abi_signature()),
event: &event,
datatype: PhantomData,
})
}
/// Returns a transaction builder for the provided function 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 method<T: Tokenize>(&self, name: &str, args: T) -> Result<Sender<'a, S, P>, Error> {
pub fn method<T: Tokenize, D: Detokenize>(
&self,
name: &str,
args: Option<T>,
) -> Result<Sender<'a, S, P, D>, Error> {
// get the function
let function = self.abi.function(name)?;
self.method_func(function, args)
@ -66,11 +73,11 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
/// Returns a transaction builder for the selected function signature. This should be
/// preferred if there are overloaded functions in your smart contract
pub fn method_hash<T: Tokenize>(
pub fn method_hash<T: Tokenize, D: Detokenize>(
&self,
signature: Selector,
args: T,
) -> Result<Sender<'a, S, P>, Error> {
args: Option<T>,
) -> Result<Sender<'a, S, P, D>, Error> {
let function = self
.methods
.get(&signature)
@ -79,13 +86,17 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
self.method_func(function, args)
}
fn method_func<T: Tokenize>(
fn method_func<T: Tokenize, D: Detokenize>(
&self,
function: &Function,
args: T,
) -> Result<Sender<'a, S, P>, Error> {
args: Option<T>,
) -> Result<Sender<'a, S, P, D>, Error> {
// create the calldata
let data = function.encode_input(&args.into_tokens())?;
let data = if let Some(args) = args {
function.encode_input(&args.into_tokens())?
} else {
function.selector().to_vec()
};
// create the tx object
let tx = TransactionRequest {
@ -98,6 +109,8 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
tx,
client: self.client,
block: None,
function: function.to_owned(),
datatype: PhantomData,
})
}
@ -110,13 +123,15 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
}
}
pub struct Sender<'a, S, P> {
pub struct Sender<'a, S, P, D> {
tx: TransactionRequest,
function: Function,
client: &'a Client<'a, S, P>,
block: Option<BlockNumber>,
datatype: PhantomData<D>,
}
impl<'a, S, P> Sender<'a, S, P> {
impl<'a, S, P, D: Detokenize> Sender<'a, S, P, 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());
@ -142,9 +157,36 @@ impl<'a, S, P> Sender<'a, S, P> {
}
}
impl<'a, S: Signer, P: JsonRpcClient> Sender<'a, S, P> {
pub async fn call<T: for<'b> Deserialize<'b>>(self) -> Result<T, P::Error> {
self.client.call(self.tx).await
#[derive(ThisError, Debug)]
// TODO: Can we get rid of this static?
pub enum ContractError<P: JsonRpcClient>
where
P::Error: 'static,
{
#[error(transparent)]
DecodingError(#[from] ethers_abi::Error),
#[error(transparent)]
DetokenizationError(#[from] ethers_abi::InvalidOutputType),
#[error(transparent)]
CallError(P::Error),
}
impl<'a, S: Signer, P: JsonRpcClient, D: Detokenize> Sender<'a, S, P, D>
where
P::Error: 'static,
{
pub async fn call(self) -> Result<D, ContractError<P>> {
let bytes = self
.client
.call(self.tx, self.block)
.await
.map_err(ContractError::CallError)?;
let tokens = self.function.decode_output(&bytes.0)?;
let data = D::from_tokens(tokens)?;
Ok(data)
}
pub async fn send(self) -> Result<H256, P::Error> {
@ -152,14 +194,14 @@ impl<'a, S: Signer, P: JsonRpcClient> Sender<'a, S, P> {
}
}
pub struct Event<'a, 'b, P> {
pub struct Event<'a, 'b, P, D> {
filter: Filter,
provider: &'a Provider<P>,
event: &'b AbiEvent,
datatype: PhantomData<D>,
}
// copy of the builder pattern from Filter
impl<'a, 'b, P> Event<'a, 'b, P> {
impl<'a, 'b, P, D: Detokenize> Event<'a, 'b, P, D> {
pub fn from_block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
self.filter.from_block = Some(block.into());
self
@ -181,10 +223,18 @@ impl<'a, 'b, P> Event<'a, 'b, P> {
}
}
impl<'a, 'b, P: JsonRpcClient> Event<'a, 'b, P> {
pub async fn query<T: Detokenize>(self) -> Result<Vec<T>, P::Error> {
// TODO: Can we get rid of the static?
impl<'a, 'b, P: JsonRpcClient, D: Detokenize> Event<'a, 'b, P, D>
where
P::Error: 'static,
{
pub async fn query(self) -> Result<Vec<D>, ContractError<P>> {
// get the logs
let logs = self.provider.get_logs(&self.filter).await?;
let logs = self
.provider
.get_logs(&self.filter)
.await
.map_err(ContractError::CallError)?;
let events = logs
.into_iter()
@ -196,17 +246,16 @@ impl<'a, 'b, P: JsonRpcClient> Event<'a, 'b, P> {
.parse_log(RawLog {
topics: log.topics,
data: log.data.0,
})
.unwrap() // TODO: remove
})?
.params
.into_iter()
.map(|param| param.value)
.collect::<Vec<_>>();
// convert the tokens to the requested datatype
T::from_tokens(tokens).unwrap()
Ok::<_, ContractError<P>>(D::from_tokens(tokens)?)
})
.collect::<Vec<T>>();
.collect::<Result<Vec<_>, _>>()?;
Ok(events)
}

View File

@ -7,6 +7,7 @@ edition = "2018"
[dependencies]
ethers-types = { path = "../ethers-types" }
ethers-utils = { path = "../ethers-utils" }
ethers-abi = { path = "../ethers-abi" }
async-trait = { version = "0.1.31", default-features = false }
reqwest = { version = "0.10.4", default-features = false, features = ["json", "rustls-tls"] }

View File

@ -1,5 +1,5 @@
use ethers_types::{
Address, Block, BlockId, BlockNumber, Filter, Log, Transaction, TransactionReceipt,
Address, Block, BlockId, BlockNumber, Bytes, Filter, Log, Transaction, TransactionReceipt,
TransactionRequest, TxHash, U256,
};
use ethers_utils as utils;
@ -108,11 +108,14 @@ impl<P: JsonRpcClient> Provider<P> {
// State mutations
/// Broadcasts the transaction request via the `eth_sendTransaction` API
pub async fn call<T: for<'a> Deserialize<'a>>(
pub async fn call(
&self,
tx: TransactionRequest,
) -> Result<T, P::Error> {
self.0.request("eth_call", Some(tx)).await
block: Option<BlockNumber>,
) -> Result<Bytes, P::Error> {
let tx = utils::serialize(&tx);
let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest));
self.0.request("eth_call", Some(vec![tx, block])).await
}
/// Broadcasts the transaction request via the `eth_sendTransaction` API

View File

@ -1,9 +1,9 @@
use ethers::{
abi::{Detokenize, InvalidOutputType, Token},
contract::Contract,
providers::HttpProvider,
signers::MainnetWallet,
types::Address,
contract::{Contract, Event, Sender},
providers::{HttpProvider, JsonRpcClient},
signers::{Client, MainnetWallet, Signer},
types::{Address, H256},
};
use anyhow::Result;
@ -12,6 +12,8 @@ use std::convert::TryFrom;
const ABI: &'static str = r#"[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":false,"internalType":"string","name":"oldValue","type":"string"},{"indexed":false,"internalType":"string","name":"newValue","type":"string"}],"name":"ValueChanged","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#;
// abigen!(SimpleContract, ABI);
#[derive(Clone, Debug, Serialize)]
// TODO: This should be `derive`-able on such types -> similar to how Zexe's Deserialize is done
struct ValueChanged {
@ -34,33 +36,60 @@ impl Detokenize for ValueChanged {
}
}
struct SimpleContract<'a, S, P>(Contract<'a, S, P>);
impl<'a, S: Signer, P: JsonRpcClient> SimpleContract<'a, S, P> {
fn new<T: Into<Address>>(address: T, client: &'a Client<'a, S, P>) -> Self {
let contract = Contract::new(client, serde_json::from_str(&ABI).unwrap(), address.into());
Self(contract)
}
fn set_value<T: Into<String>>(&self, val: T) -> Sender<'a, S, P, H256> {
self.0
.method("setValue", Some(val.into()))
.expect("method not found (this should never happen)")
}
fn value_changed<'b>(&'a self) -> Event<'a, 'b, P, ValueChanged>
where
'a: 'b,
{
self.0.event("ValueChanged").expect("event does not exist")
}
fn get_value(&self) -> Sender<'a, S, P, String> {
self.0
.method("getValue", None::<()>)
.expect("method not found (this should never happen)")
}
}
#[tokio::main]
async fn main() -> Result<()> {
// connect to the network
let provider = HttpProvider::try_from("http://localhost:8545")?;
// create a wallet and connect it to the provider
let client = "d22cf25d564c3c3f99677f8710b2f045045f16eccd31140c92d6feb18c1169e9"
let client = "ea878d94d9b1ffc78b45fc7bfc72ec3d1ce6e51e80c8e376c3f7c9a861f7c214"
.parse::<MainnetWallet>()?
.connect(&provider);
// Contract should take both provider or a signer
// get the contract's address
let addr = "683BEE23D79A1D8664dF70714edA966e1484Fd3d".parse::<Address>()?;
let addr = "ebBe15d9C365fC8a04a82E06644d6B39aF20cC31".parse::<Address>()?;
// instantiate it
let contract = Contract::new(&client, serde_json::from_str(ABI)?, addr);
let contract = SimpleContract::new(addr, &client);
// call the method
let _tx_hash = contract.method("setValue", "hi".to_owned())?.send().await?;
let _tx_hash = contract.set_value("hi").send().await?;
let logs: Vec<ValueChanged> = contract
.event("ValueChanged")?
.from_block(0u64)
.query()
.await?;
let logs = contract.value_changed().from_block(0u64).query().await?;
let value = contract.get_value().call().await?;
println!("Value: {}. Logs: {}", value, serde_json::to_string(&logs)?);
println!("{}", serde_json::to_string(&logs)?);
Ok(())
}