diff --git a/Cargo.lock b/Cargo.lock index 7bab1cfa..d4dd2855 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,6 +177,18 @@ dependencies = [ "thiserror 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ethers-contract" +version = "0.1.0" +dependencies = [ + "ethers-abi 0.1.0", + "ethers-providers 0.1.0", + "ethers-signers 0.1.0", + "ethers-types 0.1.0", + "rustc-hex 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ethers-providers" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 8ead2fac..2b06e327 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [ # "./crates/ethers", "./crates/ethers-abi", - # "./crates/ethers-contract", + "./crates/ethers-contract", # "./crates/ethers-derive", "./crates/ethers-providers", "./crates/ethers-signers", diff --git a/crates/ethers-contract/Cargo.toml b/crates/ethers-contract/Cargo.toml index 18fc0aeb..e721f481 100644 --- a/crates/ethers-contract/Cargo.toml +++ b/crates/ethers-contract/Cargo.toml @@ -4,6 +4,11 @@ version = "0.1.0" authors = ["Georgios Konstantopoulos "] edition = "2018" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] +ethers-abi = { path = "../ethers-abi" } +ethers-providers = { path = "../ethers-providers" } +ethers-signers = { path = "../ethers-signers" } +ethers-types = { path = "../ethers-types" } + +serde = { version = "1.0.110", default-features = false } +rustc-hex = { version = "2.1.0", default-features = false } diff --git a/crates/ethers-contract/src/contract.rs b/crates/ethers-contract/src/contract.rs deleted file mode 100644 index 9af09b68..00000000 --- a/crates/ethers-contract/src/contract.rs +++ /dev/null @@ -1,235 +0,0 @@ -use crate::{ - abi::{self, Abi, EventExt, Function, FunctionExt}, - contract::{Detokenize, Tokenize}, - providers::{JsonRpcClient, Provider}, - signers::{Client, Signer}, - types::{Address, BlockNumber, Filter, Selector, TransactionRequest, ValueOrArray, H256, U256}, -}; -use rustc_hex::ToHex; -use serde::Deserialize; -use std::{collections::HashMap, fmt::Debug, hash::Hash}; - -/// Represents a contract instance at an address. Provides methods for -/// contract interaction. -#[derive(Debug, Clone)] -pub struct Contract<'a, S, P> { - client: &'a Client<'a, S, P>, - abi: Abi, - address: Address, - - /// A mapping from method signature to a name-index pair for accessing - /// functions in the contract ABI. This is used to avoid allocation when - /// searching for matching functions by signature. - // Adapted from: https://github.com/gnosis/ethcontract-rs/blob/master/src/contract.rs - methods: HashMap, -} - -impl<'a, S, P> 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 { - let methods = create_mapping(&abi.functions, |function| function.selector()); - - Self { - client, - abi, - address, - methods, - } - } - - /// 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, abi::Error> - where - 'a: 'b, - { - // get the event's full name - let event = self.abi.event(name)?; - Ok(Event { - provider: &self.client.provider, - filter: Filter::new().event(&event.abi_signature()), - event: &event, - }) - } - - /// 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, abi::Error> { - // get the function - let function = self.abi.function(name)?; - self.method_func(function, args) - } - - /// 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( - &self, - signature: Selector, - args: T, - ) -> Result, abi::Error> { - let function = self - .methods - .get(&signature) - .map(|(name, index)| &self.abi.functions[name][*index]) - .ok_or_else(|| abi::Error::InvalidName(signature.to_hex::()))?; - self.method_func(function, args) - } - - fn method_func( - &self, - function: &Function, - args: T, - ) -> Result, abi::Error> { - // create the calldata - let data = function.encode_input(&args.into_tokens())?; - - // create the tx object - let tx = TransactionRequest { - to: Some(self.address), - data: Some(data.into()), - ..Default::default() - }; - - Ok(Sender { - tx, - client: self.client, - block: None, - }) - } - - pub fn address(&self) -> &Address { - &self.address - } - - pub fn abi(&self) -> &Abi { - &self.abi - } -} - -pub struct Sender<'a, S, P> { - tx: TransactionRequest, - client: &'a Client<'a, S, P>, - block: Option, -} - -impl<'a, S, P> Sender<'a, S, P> { - /// 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()); - self - } - - /// Sets the `gas` field in the transaction to the provided value - pub fn gas>(mut self, gas: T) -> Self { - self.tx.gas = Some(gas.into()); - self - } - - /// Sets the `gas_price` field in the transaction to the provided value - pub fn gas_price>(mut self, gas_price: T) -> Self { - self.tx.gas_price = Some(gas_price.into()); - self - } - - /// Sets the `value` field in the transaction to the provided value - pub fn value>(mut self, value: T) -> Self { - self.tx.value = Some(value.into()); - self - } -} - -impl<'a, S: Signer, P: JsonRpcClient> Sender<'a, S, P> { - pub async fn call Deserialize<'b>>(self) -> Result { - self.client.call(self.tx).await - } - - pub async fn send(self) -> Result { - self.client.send_transaction(self.tx, self.block).await - } -} - -pub struct Event<'a, 'b, P> { - filter: Filter, - provider: &'a Provider

, - event: &'b abi::Event, -} - -// copy of the builder pattern from Filter -impl<'a, 'b, P> Event<'a, 'b, P> { - pub fn from_block>(mut self, block: T) -> Self { - self.filter.from_block = Some(block.into()); - self - } - - pub fn to_block>(mut self, block: T) -> Self { - self.filter.to_block = Some(block.into()); - self - } - - pub fn topic>>(mut self, topic: T) -> Self { - self.filter.topics.push(topic.into()); - self - } - - pub fn topics(mut self, topics: &[ValueOrArray]) -> Self { - self.filter.topics.extend_from_slice(topics); - self - } -} - -impl<'a, 'b, P: JsonRpcClient> Event<'a, 'b, P> { - pub async fn query(self) -> Result, P::Error> { - // get the logs - let logs = self.provider.get_logs(&self.filter).await?; - - let events = logs - .into_iter() - .map(|log| { - // ethabi parses the unindexed and indexed logs together to a - // vector of tokens - let tokens = self - .event - .parse_log(abi::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() - }) - .collect::>(); - - Ok(events) - } -} - -// Helpers - -/// Utility function for creating a mapping between a unique signature and a -/// name-index pair for accessing contract ABI items. -fn create_mapping( - elements: &HashMap>, - signature: F, -) -> HashMap -where - S: Hash + Eq, - F: Fn(&T) -> S, -{ - let signature = &signature; - elements - .iter() - .flat_map(|(name, sub_elements)| { - sub_elements - .iter() - .enumerate() - .map(move |(index, element)| (signature(element), (name.to_owned(), index))) - }) - .collect() -} diff --git a/crates/ethers-contract/src/lib.rs b/crates/ethers-contract/src/lib.rs index 31e1bb20..9735d1b0 100644 --- a/crates/ethers-contract/src/lib.rs +++ b/crates/ethers-contract/src/lib.rs @@ -1,7 +1,235 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); +use ethers_abi::{ + Abi, Detokenize, Error, Event as AbiEvent, EventExt, Function, FunctionExt, RawLog, Tokenize, +}; +use ethers_providers::{JsonRpcClient, Provider}; +use ethers_signers::{Client, Signer}; +use ethers_types::{ + Address, BlockNumber, Filter, Selector, TransactionRequest, ValueOrArray, H256, U256, +}; + +use rustc_hex::ToHex; +use serde::Deserialize; +use std::{collections::HashMap, fmt::Debug, hash::Hash}; + +/// Represents a contract instance at an address. Provides methods for +/// contract interaction. +#[derive(Debug, Clone)] +pub struct Contract<'a, S, P> { + client: &'a Client<'a, S, P>, + abi: Abi, + address: Address, + + /// A mapping from method signature to a name-index pair for accessing + /// functions in the contract ABI. This is used to avoid allocation when + /// searching for matching functions by signature. + // Adapted from: https://github.com/gnosis/ethcontract-rs/blob/master/src/contract.rs + methods: HashMap, +} + +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 { + let methods = create_mapping(&abi.functions, |function| function.selector()); + + Self { + client, + abi, + address, + methods, + } + } + + /// 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> + where + 'a: 'b, + { + // get the event's full name + let event = self.abi.event(name)?; + Ok(Event { + provider: &self.client.provider(), + filter: Filter::new().event(&event.abi_signature()), + event: &event, + }) + } + + /// 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> { + // get the function + let function = self.abi.function(name)?; + self.method_func(function, args) + } + + /// 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( + &self, + signature: Selector, + args: T, + ) -> Result, Error> { + let function = self + .methods + .get(&signature) + .map(|(name, index)| &self.abi.functions[name][*index]) + .ok_or_else(|| Error::InvalidName(signature.to_hex::()))?; + self.method_func(function, args) + } + + fn method_func( + &self, + function: &Function, + args: T, + ) -> Result, Error> { + // create the calldata + let data = function.encode_input(&args.into_tokens())?; + + // create the tx object + let tx = TransactionRequest { + to: Some(self.address), + data: Some(data.into()), + ..Default::default() + }; + + Ok(Sender { + tx, + client: self.client, + block: None, + }) + } + + pub fn address(&self) -> &Address { + &self.address + } + + pub fn abi(&self) -> &Abi { + &self.abi } } + +pub struct Sender<'a, S, P> { + tx: TransactionRequest, + client: &'a Client<'a, S, P>, + block: Option, +} + +impl<'a, S, P> Sender<'a, S, P> { + /// 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()); + self + } + + /// Sets the `gas` field in the transaction to the provided value + pub fn gas>(mut self, gas: T) -> Self { + self.tx.gas = Some(gas.into()); + self + } + + /// Sets the `gas_price` field in the transaction to the provided value + pub fn gas_price>(mut self, gas_price: T) -> Self { + self.tx.gas_price = Some(gas_price.into()); + self + } + + /// Sets the `value` field in the transaction to the provided value + pub fn value>(mut self, value: T) -> Self { + self.tx.value = Some(value.into()); + self + } +} + +impl<'a, S: Signer, P: JsonRpcClient> Sender<'a, S, P> { + pub async fn call Deserialize<'b>>(self) -> Result { + self.client.call(self.tx).await + } + + pub async fn send(self) -> Result { + self.client.send_transaction(self.tx, self.block).await + } +} + +pub struct Event<'a, 'b, P> { + filter: Filter, + provider: &'a Provider

, + event: &'b AbiEvent, +} + +// copy of the builder pattern from Filter +impl<'a, 'b, P> Event<'a, 'b, P> { + pub fn from_block>(mut self, block: T) -> Self { + self.filter.from_block = Some(block.into()); + self + } + + pub fn to_block>(mut self, block: T) -> Self { + self.filter.to_block = Some(block.into()); + self + } + + pub fn topic>>(mut self, topic: T) -> Self { + self.filter.topics.push(topic.into()); + self + } + + pub fn topics(mut self, topics: &[ValueOrArray]) -> Self { + self.filter.topics.extend_from_slice(topics); + self + } +} + +impl<'a, 'b, P: JsonRpcClient> Event<'a, 'b, P> { + pub async fn query(self) -> Result, P::Error> { + // get the logs + let logs = self.provider.get_logs(&self.filter).await?; + + let events = logs + .into_iter() + .map(|log| { + // ethabi parses the unindexed and indexed logs together to a + // vector of tokens + let tokens = self + .event + .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() + }) + .collect::>(); + + Ok(events) + } +} + +/// Utility function for creating a mapping between a unique signature and a +/// name-index pair for accessing contract ABI items. +fn create_mapping( + elements: &HashMap>, + signature: F, +) -> HashMap +where + S: Hash + Eq, + F: Fn(&T) -> S, +{ + let signature = &signature; + elements + .iter() + .flat_map(|(name, sub_elements)| { + sub_elements + .iter() + .enumerate() + .map(move |(index, element)| (signature(element), (name.to_owned(), index))) + }) + .collect() +} diff --git a/crates/ethers-contract/src/mod.rs b/crates/ethers-contract/src/mod.rs deleted file mode 100644 index 98ea0efa..00000000 --- a/crates/ethers-contract/src/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod contract; -pub use contract::Contract; - -mod tokens; -pub use tokens::{Detokenize, InvalidOutputType, Tokenize}; diff --git a/crates/ethers-signers/src/client.rs b/crates/ethers-signers/src/client.rs index 2b05e6b6..d6438ee9 100644 --- a/crates/ethers-signers/src/client.rs +++ b/crates/ethers-signers/src/client.rs @@ -83,6 +83,10 @@ impl<'a, S: Signer, P: JsonRpcClient> Client<'a, S, P> { .map(|s| s.address()) .unwrap_or_default() } + + pub fn provider(&self) -> &Provider

{ + self.provider + } } // Abuse Deref to use the Provider's methods without re-writing everything. diff --git a/crates/ethers-signers/src/lib.rs b/crates/ethers-signers/src/lib.rs index 1408beae..7588f80f 100644 --- a/crates/ethers-signers/src/lib.rs +++ b/crates/ethers-signers/src/lib.rs @@ -18,7 +18,7 @@ mod wallet; pub use wallet::Wallet; mod client; -pub(crate) use client::Client; +pub use client::Client; use ethers_types::{Address, Signature, Transaction, TransactionRequest}; use std::error::Error; diff --git a/crates/ethers-types/src/log.rs b/crates/ethers-types/src/log.rs index 5a09fa72..f812dbab 100644 --- a/crates/ethers-types/src/log.rs +++ b/crates/ethers-types/src/log.rs @@ -65,11 +65,11 @@ pub struct Log { pub struct Filter { /// From Block #[serde(rename = "fromBlock", skip_serializing_if = "Option::is_none")] - pub(crate) from_block: Option, + pub from_block: Option, /// To Block #[serde(rename = "toBlock", skip_serializing_if = "Option::is_none")] - pub(crate) to_block: Option, + pub to_block: Option, /// Address #[serde(skip_serializing_if = "Option::is_none")] @@ -80,7 +80,7 @@ pub struct Filter { /// Topics #[serde(skip_serializing_if = "Vec::is_empty")] // TODO: Split in an event name + 3 topics - pub(crate) topics: Vec>, + pub topics: Vec>, /// Limit #[serde(skip_serializing_if = "Option::is_none")]