specify the datatype when creating the call
This commit is contained in:
parent
c46442dd12
commit
25f2c5e45d
|
@ -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"]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue