diff --git a/crates/ethers-contract/Cargo.toml b/crates/ethers-contract/Cargo.toml index e721f481..6933f80f 100644 --- a/crates/ethers-contract/Cargo.toml +++ b/crates/ethers-contract/Cargo.toml @@ -5,6 +5,8 @@ authors = ["Georgios Konstantopoulos "] 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"] diff --git a/crates/ethers-contract/src/lib.rs b/crates/ethers-contract/src/lib.rs index 9735d1b0..f020b60b 100644 --- a/crates/ethers-contract/src/lib.rs +++ b/crates/ethers-contract/src/lib.rs @@ -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, } +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, Error> + pub fn event<'b, D: Detokenize>(&'a self, name: &str) -> Result, 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(&self, name: &str, args: T) -> Result, Error> { + pub fn method( + &self, + name: &str, + args: Option, + ) -> Result, 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( + pub fn method_hash( &self, signature: Selector, - args: T, - ) -> Result, Error> { + args: Option, + ) -> Result, 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( + fn method_func( &self, function: &Function, - args: T, - ) -> Result, Error> { + args: Option, + ) -> Result, 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, + datatype: PhantomData, } -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>(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 Deserialize<'b>>(self) -> Result { - self.client.call(self.tx).await +#[derive(ThisError, Debug)] +// TODO: Can we get rid of this static? +pub enum ContractError +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> { + 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 { @@ -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

, event: &'b AbiEvent, + datatype: PhantomData, } -// 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>(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(self) -> Result, 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, ContractError

> { // 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::>(); // convert the tokens to the requested datatype - T::from_tokens(tokens).unwrap() + Ok::<_, ContractError

>(D::from_tokens(tokens)?) }) - .collect::>(); + .collect::, _>>()?; Ok(events) } diff --git a/crates/ethers-providers/Cargo.toml b/crates/ethers-providers/Cargo.toml index 0a610b01..8e4ff8a3 100644 --- a/crates/ethers-providers/Cargo.toml +++ b/crates/ethers-providers/Cargo.toml @@ -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"] } diff --git a/crates/ethers-providers/src/provider.rs b/crates/ethers-providers/src/provider.rs index eba2bfc0..8b608085 100644 --- a/crates/ethers-providers/src/provider.rs +++ b/crates/ethers-providers/src/provider.rs @@ -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 Provider

{ // State mutations /// Broadcasts the transaction request via the `eth_sendTransaction` API - pub async fn call Deserialize<'a>>( + pub async fn call( &self, tx: TransactionRequest, - ) -> Result { - self.0.request("eth_call", Some(tx)).await + block: Option, + ) -> Result { + 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 diff --git a/crates/ethers/examples/contract.rs b/crates/ethers/examples/contract.rs index 578a8b62..c6f4ce69 100644 --- a/crates/ethers/examples/contract.rs +++ b/crates/ethers/examples/contract.rs @@ -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>(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>(&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::()? .connect(&provider); // Contract should take both provider or a signer // get the contract's address - let addr = "683BEE23D79A1D8664dF70714edA966e1484Fd3d".parse::

()?; + let addr = "ebBe15d9C365fC8a04a82E06644d6B39aF20cC31".parse::
()?; // 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 = 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(()) }