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"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ethers-contract-abigen = { path = "ethers-contract-abigen", optional = true }
|
||||||
|
|
||||||
ethers-abi = { path = "../ethers-abi" }
|
ethers-abi = { path = "../ethers-abi" }
|
||||||
ethers-providers = { path = "../ethers-providers" }
|
ethers-providers = { path = "../ethers-providers" }
|
||||||
ethers-signers = { path = "../ethers-signers" }
|
ethers-signers = { path = "../ethers-signers" }
|
||||||
|
@ -12,3 +14,8 @@ ethers-types = { path = "../ethers-types" }
|
||||||
|
|
||||||
serde = { version = "1.0.110", default-features = false }
|
serde = { version = "1.0.110", default-features = false }
|
||||||
rustc-hex = { version = "2.1.0", 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 rustc_hex::ToHex;
|
||||||
use serde::Deserialize;
|
|
||||||
use std::{collections::HashMap, fmt::Debug, hash::Hash};
|
use std::{collections::HashMap, fmt::Debug, hash::Hash};
|
||||||
|
use thiserror::Error as ThisError;
|
||||||
|
|
||||||
/// Represents a contract instance at an address. Provides methods for
|
/// Represents a contract instance at an address. Provides methods for
|
||||||
/// contract interaction.
|
/// contract interaction.
|
||||||
|
@ -26,6 +26,8 @@ pub struct Contract<'a, S, P> {
|
||||||
methods: HashMap<Selector, (String, usize)>,
|
methods: HashMap<Selector, (String, usize)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
|
impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
|
||||||
/// Creates a new contract from the provided client, abi and address
|
/// 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 {
|
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
|
/// Returns a transaction builder for the provided function name. If there are
|
||||||
/// multiple functions with the same name due to overloading, consider using
|
/// multiple functions with the same name due to overloading, consider using
|
||||||
/// the `method_hash` method instead, since this will use the first match.
|
/// 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
|
where
|
||||||
'a: 'b,
|
'a: 'b,
|
||||||
{
|
{
|
||||||
|
@ -52,13 +54,18 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
|
||||||
provider: &self.client.provider(),
|
provider: &self.client.provider(),
|
||||||
filter: Filter::new().event(&event.abi_signature()),
|
filter: Filter::new().event(&event.abi_signature()),
|
||||||
event: &event,
|
event: &event,
|
||||||
|
datatype: PhantomData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a transaction builder for the provided function name. If there are
|
/// Returns a transaction builder for the provided function name. If there are
|
||||||
/// multiple functions with the same name due to overloading, consider using
|
/// multiple functions with the same name due to overloading, consider using
|
||||||
/// the `method_hash` method instead, since this will use the first match.
|
/// 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
|
// get the function
|
||||||
let function = self.abi.function(name)?;
|
let function = self.abi.function(name)?;
|
||||||
self.method_func(function, args)
|
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
|
/// Returns a transaction builder for the selected function signature. This should be
|
||||||
/// preferred if there are overloaded functions in your smart contract
|
/// 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,
|
&self,
|
||||||
signature: Selector,
|
signature: Selector,
|
||||||
args: T,
|
args: Option<T>,
|
||||||
) -> Result<Sender<'a, S, P>, Error> {
|
) -> Result<Sender<'a, S, P, D>, Error> {
|
||||||
let function = self
|
let function = self
|
||||||
.methods
|
.methods
|
||||||
.get(&signature)
|
.get(&signature)
|
||||||
|
@ -79,13 +86,17 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
|
||||||
self.method_func(function, args)
|
self.method_func(function, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn method_func<T: Tokenize>(
|
fn method_func<T: Tokenize, D: Detokenize>(
|
||||||
&self,
|
&self,
|
||||||
function: &Function,
|
function: &Function,
|
||||||
args: T,
|
args: Option<T>,
|
||||||
) -> Result<Sender<'a, S, P>, Error> {
|
) -> Result<Sender<'a, S, P, D>, Error> {
|
||||||
// create the calldata
|
// 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
|
// create the tx object
|
||||||
let tx = TransactionRequest {
|
let tx = TransactionRequest {
|
||||||
|
@ -98,6 +109,8 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
|
||||||
tx,
|
tx,
|
||||||
client: self.client,
|
client: self.client,
|
||||||
block: None,
|
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,
|
tx: TransactionRequest,
|
||||||
|
function: Function,
|
||||||
client: &'a Client<'a, S, P>,
|
client: &'a Client<'a, S, P>,
|
||||||
block: Option<BlockNumber>,
|
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
|
/// Sets the `from` field in the transaction to the provided value
|
||||||
pub fn from<T: Into<Address>>(mut self, from: T) -> Self {
|
pub fn from<T: Into<Address>>(mut self, from: T) -> Self {
|
||||||
self.tx.from = Some(from.into());
|
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> {
|
#[derive(ThisError, Debug)]
|
||||||
pub async fn call<T: for<'b> Deserialize<'b>>(self) -> Result<T, P::Error> {
|
// TODO: Can we get rid of this static?
|
||||||
self.client.call(self.tx).await
|
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> {
|
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,
|
filter: Filter,
|
||||||
provider: &'a Provider<P>,
|
provider: &'a Provider<P>,
|
||||||
event: &'b AbiEvent,
|
event: &'b AbiEvent,
|
||||||
|
datatype: PhantomData<D>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy of the builder pattern from Filter
|
impl<'a, 'b, P, D: Detokenize> Event<'a, 'b, P, D> {
|
||||||
impl<'a, 'b, P> Event<'a, 'b, P> {
|
|
||||||
pub fn from_block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
|
pub fn from_block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
|
||||||
self.filter.from_block = Some(block.into());
|
self.filter.from_block = Some(block.into());
|
||||||
self
|
self
|
||||||
|
@ -181,10 +223,18 @@ impl<'a, 'b, P> Event<'a, 'b, P> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, P: JsonRpcClient> Event<'a, 'b, P> {
|
// TODO: Can we get rid of the static?
|
||||||
pub async fn query<T: Detokenize>(self) -> Result<Vec<T>, P::Error> {
|
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
|
// 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
|
let events = logs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -196,17 +246,16 @@ impl<'a, 'b, P: JsonRpcClient> Event<'a, 'b, P> {
|
||||||
.parse_log(RawLog {
|
.parse_log(RawLog {
|
||||||
topics: log.topics,
|
topics: log.topics,
|
||||||
data: log.data.0,
|
data: log.data.0,
|
||||||
})
|
})?
|
||||||
.unwrap() // TODO: remove
|
|
||||||
.params
|
.params
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|param| param.value)
|
.map(|param| param.value)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// convert the tokens to the requested datatype
|
// 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)
|
Ok(events)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ edition = "2018"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ethers-types = { path = "../ethers-types" }
|
ethers-types = { path = "../ethers-types" }
|
||||||
ethers-utils = { path = "../ethers-utils" }
|
ethers-utils = { path = "../ethers-utils" }
|
||||||
|
ethers-abi = { path = "../ethers-abi" }
|
||||||
|
|
||||||
async-trait = { version = "0.1.31", default-features = false }
|
async-trait = { version = "0.1.31", default-features = false }
|
||||||
reqwest = { version = "0.10.4", default-features = false, features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.10.4", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use ethers_types::{
|
use ethers_types::{
|
||||||
Address, Block, BlockId, BlockNumber, Filter, Log, Transaction, TransactionReceipt,
|
Address, Block, BlockId, BlockNumber, Bytes, Filter, Log, Transaction, TransactionReceipt,
|
||||||
TransactionRequest, TxHash, U256,
|
TransactionRequest, TxHash, U256,
|
||||||
};
|
};
|
||||||
use ethers_utils as utils;
|
use ethers_utils as utils;
|
||||||
|
@ -108,11 +108,14 @@ impl<P: JsonRpcClient> Provider<P> {
|
||||||
// State mutations
|
// State mutations
|
||||||
|
|
||||||
/// Broadcasts the transaction request via the `eth_sendTransaction` API
|
/// Broadcasts the transaction request via the `eth_sendTransaction` API
|
||||||
pub async fn call<T: for<'a> Deserialize<'a>>(
|
pub async fn call(
|
||||||
&self,
|
&self,
|
||||||
tx: TransactionRequest,
|
tx: TransactionRequest,
|
||||||
) -> Result<T, P::Error> {
|
block: Option<BlockNumber>,
|
||||||
self.0.request("eth_call", Some(tx)).await
|
) -> 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
|
/// Broadcasts the transaction request via the `eth_sendTransaction` API
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use ethers::{
|
use ethers::{
|
||||||
abi::{Detokenize, InvalidOutputType, Token},
|
abi::{Detokenize, InvalidOutputType, Token},
|
||||||
contract::Contract,
|
contract::{Contract, Event, Sender},
|
||||||
providers::HttpProvider,
|
providers::{HttpProvider, JsonRpcClient},
|
||||||
signers::MainnetWallet,
|
signers::{Client, MainnetWallet, Signer},
|
||||||
types::Address,
|
types::{Address, H256},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
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"}]"#;
|
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)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
// TODO: This should be `derive`-able on such types -> similar to how Zexe's Deserialize is done
|
// TODO: This should be `derive`-able on such types -> similar to how Zexe's Deserialize is done
|
||||||
struct ValueChanged {
|
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]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
// connect to the network
|
// connect to the network
|
||||||
let provider = HttpProvider::try_from("http://localhost:8545")?;
|
let provider = HttpProvider::try_from("http://localhost:8545")?;
|
||||||
|
|
||||||
// create a wallet and connect it to the provider
|
// create a wallet and connect it to the provider
|
||||||
let client = "d22cf25d564c3c3f99677f8710b2f045045f16eccd31140c92d6feb18c1169e9"
|
let client = "ea878d94d9b1ffc78b45fc7bfc72ec3d1ce6e51e80c8e376c3f7c9a861f7c214"
|
||||||
.parse::<MainnetWallet>()?
|
.parse::<MainnetWallet>()?
|
||||||
.connect(&provider);
|
.connect(&provider);
|
||||||
|
|
||||||
// Contract should take both provider or a signer
|
// Contract should take both provider or a signer
|
||||||
|
|
||||||
// get the contract's address
|
// get the contract's address
|
||||||
let addr = "683BEE23D79A1D8664dF70714edA966e1484Fd3d".parse::<Address>()?;
|
let addr = "ebBe15d9C365fC8a04a82E06644d6B39aF20cC31".parse::<Address>()?;
|
||||||
|
|
||||||
// instantiate it
|
// instantiate it
|
||||||
let contract = Contract::new(&client, serde_json::from_str(ABI)?, addr);
|
let contract = SimpleContract::new(addr, &client);
|
||||||
|
|
||||||
// call the method
|
// 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
|
let logs = contract.value_changed().from_block(0u64).query().await?;
|
||||||
.event("ValueChanged")?
|
|
||||||
.from_block(0u64)
|
let value = contract.get_value().call().await?;
|
||||||
.query()
|
|
||||||
.await?;
|
println!("Value: {}. Logs: {}", value, serde_json::to_string(&logs)?);
|
||||||
|
|
||||||
println!("{}", serde_json::to_string(&logs)?);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue