From 33b36bbc52b38cfa06a6772ab729bfe3cc4f8478 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Mon, 25 May 2020 18:35:38 +0300 Subject: [PATCH] feat: add basic contract support --- Cargo.lock | 11 +++ Cargo.toml | 1 + examples/contract.rs | 57 +++++++++++-- examples/local_signer.rs | 6 +- src/{contract => }/abi.rs | 6 +- src/contract/contract.rs | 169 ++++++++++++++++++++++++++++++++++++++ src/contract/mod.rs | 19 +---- src/lib.rs | 7 +- src/providers/mod.rs | 10 ++- src/signers/client.rs | 88 +++++++++++++++++--- src/signers/wallet.rs | 2 +- src/types/bytes.rs | 6 ++ src/types/mod.rs | 4 +- src/types/transaction.rs | 25 ++++++ src/utils.rs | 2 +- 15 files changed, 366 insertions(+), 47 deletions(-) rename src/{contract => }/abi.rs (97%) create mode 100644 src/contract/contract.rs diff --git a/Cargo.lock b/Cargo.lock index 2361ff47..073f9eef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,16 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" +[[package]] +name = "bincode" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5753e2a71534719bf3f4e57006c3a4f0d2c672a4b676eec84161f763eca87dbf" +dependencies = [ + "byteorder", + "serde", +] + [[package]] name = "bitflags" version = "1.2.1" @@ -235,6 +245,7 @@ name = "ethers" version = "0.1.0" dependencies = [ "async-trait", + "bincode", "ethabi", "ethereum-types", "failure", diff --git a/Cargo.toml b/Cargo.toml index 04f81e5a..7ba73c9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ tiny-keccak = { version = "2.0.2", default-features = false } solc = { git = "https://github.com/paritytech/rust_solc "} rlp = "0.4.5" ethabi = "12.0.0" +bincode = "1.2.1" [dev-dependencies] tokio = { version = "0.2.21", features = ["macros"] } diff --git a/examples/contract.rs b/examples/contract.rs index ef469771..53dd0a72 100644 --- a/examples/contract.rs +++ b/examples/contract.rs @@ -1,6 +1,8 @@ use ethers::{ + abi::ParamType, + contract::Contract, types::{Address, Filter}, - Contract, HttpProvider, MainnetWallet, + HttpProvider, MainnetWallet, }; use std::convert::TryFrom; @@ -10,16 +12,59 @@ async fn main() -> Result<(), failure::Error> { let provider = HttpProvider::try_from("http://localhost:8545")?; // create a wallet and connect it to the provider - let client = "15c42bf2987d5a8a73804a8ea72fb4149f88adf73e98fc3f8a8ce9f24fcb7774" + let client = "d22cf25d564c3c3f99677f8710b2f045045f16eccd31140c92d6feb18c1169e9" .parse::()? .connect(&provider); // Contract should take both provider or a signer - let contract = Contract::new( - "f817796F60D268A36a57b8D2dF1B97B14C0D0E1d".parse::
()?, - abi, - ); + // get the contract's address + let addr = "683BEE23D79A1D8664dF70714edA966e1484Fd3d".parse::
()?; + // get the contract's ABI + let abi = 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"}]"#; + + // instantiate it + let contract = Contract::new(&client, serde_json::from_str(abi)?, addr); + + // get the args + let event = "ValueChanged(address,string,string)"; + + let args = &[ethabi::Token::String("hello!".to_owned())]; + + // call the method + let tx_hash = contract.method("setValue", args)?.send().await?; + + #[derive(Clone, Debug)] + struct ValueChanged { + author: Address, + old_value: String, + new_value: String, + } + + let filter = Filter::new().from_block(0).address(addr).event(event); + let logs = provider + .get_logs(&filter) + .await? + .into_iter() + .map(|log| { + // decode the non-indexed data + let data = ethabi::decode(&[ParamType::String, ParamType::String], log.data.as_ref())?; + + let author = log.topics[1].into(); + + // Unwrap? + let old_value = data[0].clone().to_string().unwrap(); + let new_value = data[1].clone().to_string().unwrap(); + + Ok(ValueChanged { + old_value, + new_value, + author, + }) + }) + .collect::, ethabi::Error>>()?; + + dbg!(logs); Ok(()) } diff --git a/examples/local_signer.rs b/examples/local_signer.rs index 2a2c06a8..6575b60d 100644 --- a/examples/local_signer.rs +++ b/examples/local_signer.rs @@ -7,7 +7,7 @@ async fn main() -> Result<(), failure::Error> { let provider = HttpProvider::try_from("http://localhost:8545")?; // create a wallet and connect it to the provider - let client = "15c42bf2987d5a8a73804a8ea72fb4149f88adf73e98fc3f8a8ce9f24fcb7774" + let client = "dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7" .parse::()? .connect(&provider); @@ -17,10 +17,10 @@ async fn main() -> Result<(), failure::Error> { .value(10000); // send it! - let tx = client.sign_and_send_transaction(tx, None).await?; + let hash = client.send_transaction(tx, None).await?; // get the mined tx - let tx = client.get_transaction(tx.hash).await?; + let tx = client.get_transaction(hash).await?; let receipt = client.get_transaction_receipt(tx.hash).await?; diff --git a/src/contract/abi.rs b/src/abi.rs similarity index 97% rename from src/contract/abi.rs rename to src/abi.rs index db0a3e12..cc74d3bc 100644 --- a/src/contract/abi.rs +++ b/src/abi.rs @@ -1,8 +1,10 @@ //! This module implements extensions to the `ethabi` API. //! Taken from: https://github.com/gnosis/ethcontract-rs/blob/master/common/src/abiext.rs -use ethabi::{Event, Function, ParamType}; -use crate::{utils::id, types::Selector}; +pub use ethabi::Contract as Abi; +pub use ethabi::*; + +use crate::{types::Selector, utils::id}; /// Extension trait for `ethabi::Function`. pub trait FunctionExt { diff --git a/src/contract/contract.rs b/src/contract/contract.rs new file mode 100644 index 00000000..dcc749d3 --- /dev/null +++ b/src/contract/contract.rs @@ -0,0 +1,169 @@ +use crate::{ + abi::{Abi, Function, FunctionExt}, + providers::JsonRpcClient, + signers::{Client, Signer}, + types::{Address, BlockNumber, Selector, TransactionRequest, H256, U256}, +}; + +use rustc_hex::ToHex; +use serde::Deserialize; +use std::{collections::HashMap, 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. + methods: HashMap, + + /// A mapping from event signature to a name-index pair for resolving + /// events in the contract ABI. + events: 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()); + let events = create_mapping(&abi.events, |event| event.signature()); + + Self { + client, + abi, + address, + methods, + events, + } + } + + /// 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: &[ethabi::Token], + ) -> Result, ethabi::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: &[ethabi::Token], + ) -> Result, ethabi::Error> { + let function = self + .methods + .get(&signature) + .map(|(name, index)| &self.abi.functions[name][*index]) + .ok_or_else(|| ethabi::Error::InvalidName(signature.to_hex::()))?; + self.method_func(function, args) + } + + fn method_func( + &self, + function: &Function, + args: &[ethabi::Token], + ) -> Result, ethabi::Error> { + // create the calldata + let data = function.encode_input(args)?; + + // 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 + } + + // call events + // deploy +} + +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 + } +} + +/// 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/src/contract/mod.rs b/src/contract/mod.rs index eb904c63..dd0ebb98 100644 --- a/src/contract/mod.rs +++ b/src/contract/mod.rs @@ -1,17 +1,2 @@ -use crate::types::Address; - -mod abi; - -pub struct Contract { - pub address: Address, - pub abi: ABI, -} - -impl Contract { - pub fn new>(address: A, abi: ABI) -> Self { - Self { - address: address.into(), - abi, - } - } -} +mod contract; +pub use contract::Contract; diff --git a/src/lib.rs b/src/lib.rs index 9e5eeac8..87ef084b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,10 +18,10 @@ pub mod providers; pub use providers::HttpProvider; -mod contract; +pub mod contract; pub use contract::Contract; -mod signers; +pub(crate) mod signers; pub use signers::{AnyWallet, MainnetWallet, Signer}; /// Ethereum related datatypes @@ -32,3 +32,6 @@ pub use solc; /// Various utilities pub mod utils; + +/// ABI utilities +pub mod abi; diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 82de8f01..f1023040 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -44,7 +44,7 @@ impl Provider

{ /// Connects to a signer and returns a client pub fn connect(&self, signer: S) -> Client { Client { - signer, + signer: Some(signer), provider: self, } } @@ -141,6 +141,14 @@ impl Provider

{ // State mutations + /// Broadcasts the transaction request via the `eth_sendTransaction` API + pub async fn call Deserialize<'a>>( + &self, + tx: TransactionRequest, + ) -> Result { + self.0.request("eth_call", Some(tx)).await + } + /// Broadcasts the transaction request via the `eth_sendTransaction` API pub async fn send_transaction(&self, tx: TransactionRequest) -> Result { self.0.request("eth_sendTransaction", Some(tx)).await diff --git a/src/signers/client.rs b/src/signers/client.rs index 1f98d932..86a257a4 100644 --- a/src/signers/client.rs +++ b/src/signers/client.rs @@ -1,7 +1,8 @@ use crate::{ providers::{JsonRpcClient, Provider}, signers::Signer, - types::{Address, BlockNumber, Transaction, TransactionRequest}, + types::{Address, BlockNumber, Overrides, TransactionRequest, TxHash}, + utils, }; use std::ops::Deref; @@ -9,18 +10,52 @@ use std::ops::Deref; #[derive(Clone, Debug)] pub struct Client<'a, S, P> { pub(crate) provider: &'a Provider

, - pub(crate) signer: S, + pub(crate) signer: Option, +} + +impl<'a, S, P> From<&'a Provider

> for Client<'a, S, P> { + fn from(provider: &'a Provider

) -> Self { + Client { + provider, + signer: None, + } + } } impl<'a, S: Signer, P: JsonRpcClient> Client<'a, S, P> { /// Signs the transaction and then broadcasts its RLP encoding via the `eth_sendRawTransaction` /// API - pub async fn sign_and_send_transaction( + pub async fn send_transaction( &self, mut tx: TransactionRequest, block: Option, - ) -> Result { - // TODO: Convert to join'ed futures + ) -> Result { + // if there is no local signer, then the transaction should use the + // node's signer which should already be unlocked + let signer = if let Some(ref signer) = self.signer { + signer + } else { + return self.provider.send_transaction(tx).await; + }; + + // fill any missing fields + self.fill_transaction(&mut tx, block).await?; + + // sign the transaction + let signed_tx = signer.sign_transaction(tx).unwrap(); // TODO + + // broadcast it + self.provider.send_raw_transaction(&signed_tx).await?; + + Ok(signed_tx.hash) + } + + // TODO: Convert to join'ed futures + async fn fill_transaction( + &self, + tx: &mut TransactionRequest, + block: Option, + ) -> Result<(), P::Error> { // get the gas price if tx.gas_price.is_none() { tx.gas_price = Some(self.provider.get_gas_price().await?); @@ -41,17 +76,48 @@ impl<'a, S: Signer, P: JsonRpcClient> Client<'a, S, P> { ); } - // sign the transaction - let signed_tx = self.signer.sign_transaction(tx).unwrap(); // TODO + Ok(()) + } - // broadcast it - self.provider.send_raw_transaction(&signed_tx).await?; + /// client.call_contract( + /// addr, + /// "transfer(address,uint256)" + /// vec![0x1234, 100] + /// None, + /// None, + /// ) + pub async fn call_contract( + &self, + to: impl Into

, + signature: &str, + args: &[ethabi::Token], + overrides: Option, + block: Option, + ) -> Result { + // create the data field from the function signature and the arguments + let data = [&utils::id(signature)[..], ðabi::encode(args)].concat(); - Ok(signed_tx) + let overrides = overrides.unwrap_or_default(); + let tx = TransactionRequest { + to: Some(to.into()), + data: Some(data.into()), + + // forward the overriden data + from: overrides.from, // let it figure it out itself + gas: overrides.gas, + gas_price: overrides.gas_price, + nonce: overrides.nonce, + value: overrides.value, + }; + + self.send_transaction(tx, block).await } pub fn address(&self) -> Address { - self.signer.address() + self.signer + .as_ref() + .map(|s| s.address()) + .unwrap_or_default() } } diff --git a/src/signers/wallet.rs b/src/signers/wallet.rs index 46022637..542f5575 100644 --- a/src/signers/wallet.rs +++ b/src/signers/wallet.rs @@ -52,7 +52,7 @@ impl Wallet { /// Connects to a provider and returns a client pub fn connect(self, provider: &Provider

) -> Client, P> { Client { - signer: self, + signer: Some(self), provider, } } diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 05e1a62f..bdd206d6 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -12,6 +12,12 @@ pub struct Bytes( pub Vec, ); +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + &self.0[..] + } +} + impl Bytes { /// Returns an empty bytes vector pub fn new() -> Self { diff --git a/src/types/mod.rs b/src/types/mod.rs index dc64bd59..e89a8744 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -7,7 +7,7 @@ pub use ethereum_types::H256 as TxHash; pub use ethereum_types::{Address, Bloom, H256, U256, U64}; mod transaction; -pub use transaction::{Transaction, TransactionReceipt, TransactionRequest}; +pub use transaction::{Overrides, Transaction, TransactionReceipt, TransactionRequest}; mod keys; pub use keys::{PrivateKey, PublicKey, TxError}; @@ -23,5 +23,3 @@ pub use block::{Block, BlockId, BlockNumber}; mod log; pub use log::{Filter, Log}; - - diff --git a/src/types/transaction.rs b/src/types/transaction.rs index ca920981..165b5736 100644 --- a/src/types/transaction.rs +++ b/src/types/transaction.rs @@ -7,6 +7,31 @@ use rlp::RlpStream; use serde::{Deserialize, Serialize}; use std::str::FromStr; +/// Override params for interacting with a contract +#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)] +pub struct Overrides { + /// Sender address or ENS name + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) from: Option

, + + /// Supplied gas (None for sensible default) + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) gas: Option, + + /// Gas price (None for sensible default) + #[serde(rename = "gasPrice")] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) gas_price: Option, + + /// Transfered value (None for no transfer) + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) value: Option, + + /// Transaction nonce (None for next available nonce) + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) nonce: Option, +} + /// Parameters for sending a transaction #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)] pub struct TransactionRequest { diff --git a/src/utils.rs b/src/utils.rs index d08a13d4..a571d0f9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,5 @@ //! Various utilities for manipulating Ethereum related dat -use crate::types::{H256, Selector}; +use crate::types::{Selector, H256}; use tiny_keccak::{Hasher, Keccak}; const PREFIX: &str = "\x19Ethereum Signed Message:\n";