feat: add basic contract support

This commit is contained in:
Georgios Konstantopoulos 2020-05-25 18:35:38 +03:00
parent 1dd3c3dc89
commit 33b36bbc52
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
15 changed files with 366 additions and 47 deletions

11
Cargo.lock generated
View File

@ -66,6 +66,16 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" 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]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.2.1" version = "1.2.1"
@ -235,6 +245,7 @@ name = "ethers"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bincode",
"ethabi", "ethabi",
"ethereum-types", "ethereum-types",
"failure", "failure",

View File

@ -21,6 +21,7 @@ tiny-keccak = { version = "2.0.2", default-features = false }
solc = { git = "https://github.com/paritytech/rust_solc "} solc = { git = "https://github.com/paritytech/rust_solc "}
rlp = "0.4.5" rlp = "0.4.5"
ethabi = "12.0.0" ethabi = "12.0.0"
bincode = "1.2.1"
[dev-dependencies] [dev-dependencies]
tokio = { version = "0.2.21", features = ["macros"] } tokio = { version = "0.2.21", features = ["macros"] }

View File

@ -1,6 +1,8 @@
use ethers::{ use ethers::{
abi::ParamType,
contract::Contract,
types::{Address, Filter}, types::{Address, Filter},
Contract, HttpProvider, MainnetWallet, HttpProvider, MainnetWallet,
}; };
use std::convert::TryFrom; use std::convert::TryFrom;
@ -10,16 +12,59 @@ async fn main() -> Result<(), failure::Error> {
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 = "15c42bf2987d5a8a73804a8ea72fb4149f88adf73e98fc3f8a8ce9f24fcb7774" let client = "d22cf25d564c3c3f99677f8710b2f045045f16eccd31140c92d6feb18c1169e9"
.parse::<MainnetWallet>()? .parse::<MainnetWallet>()?
.connect(&provider); .connect(&provider);
// Contract should take both provider or a signer // Contract should take both provider or a signer
let contract = Contract::new( // get the contract's address
"f817796F60D268A36a57b8D2dF1B97B14C0D0E1d".parse::<Address>()?, let addr = "683BEE23D79A1D8664dF70714edA966e1484Fd3d".parse::<Address>()?;
abi,
);
// 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::<Result<Vec<_>, ethabi::Error>>()?;
dbg!(logs);
Ok(()) Ok(())
} }

View File

@ -7,7 +7,7 @@ async fn main() -> Result<(), failure::Error> {
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 = "15c42bf2987d5a8a73804a8ea72fb4149f88adf73e98fc3f8a8ce9f24fcb7774" let client = "dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7"
.parse::<MainnetWallet>()? .parse::<MainnetWallet>()?
.connect(&provider); .connect(&provider);
@ -17,10 +17,10 @@ async fn main() -> Result<(), failure::Error> {
.value(10000); .value(10000);
// send it! // send it!
let tx = client.sign_and_send_transaction(tx, None).await?; let hash = client.send_transaction(tx, None).await?;
// get the mined tx // 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?; let receipt = client.get_transaction_receipt(tx.hash).await?;

View File

@ -1,8 +1,10 @@
//! This module implements extensions to the `ethabi` API. //! This module implements extensions to the `ethabi` API.
//! Taken from: https://github.com/gnosis/ethcontract-rs/blob/master/common/src/abiext.rs //! Taken from: https://github.com/gnosis/ethcontract-rs/blob/master/common/src/abiext.rs
use ethabi::{Event, Function, ParamType}; pub use ethabi::Contract as Abi;
use crate::{utils::id, types::Selector}; pub use ethabi::*;
use crate::{types::Selector, utils::id};
/// Extension trait for `ethabi::Function`. /// Extension trait for `ethabi::Function`.
pub trait FunctionExt { pub trait FunctionExt {

169
src/contract/contract.rs Normal file
View File

@ -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<Selector, (String, usize)>,
/// A mapping from event signature to a name-index pair for resolving
/// events in the contract ABI.
events: HashMap<H256, (String, usize)>,
}
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<Sender<'a, S, P>, 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<Sender<'a, S, P>, 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::<String>()))?;
self.method_func(function, args)
}
fn method_func(
&self,
function: &Function,
args: &[ethabi::Token],
) -> Result<Sender<'a, S, P>, 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<BlockNumber>,
}
impl<'a, S, P> Sender<'a, S, P> {
/// 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());
self
}
/// Sets the `gas` field in the transaction to the provided value
pub fn gas<T: Into<U256>>(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<T: Into<U256>>(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<T: Into<U256>>(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<T: for<'b> Deserialize<'b>>(self) -> Result<T, P::Error> {
self.client.call(self.tx).await
}
pub async fn send(self) -> Result<H256, P::Error> {
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<T, S, F>(
elements: &HashMap<String, Vec<T>>,
signature: F,
) -> HashMap<S, (String, usize)>
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()
}

View File

@ -1,17 +1,2 @@
use crate::types::Address; mod contract;
pub use contract::Contract;
mod abi;
pub struct Contract<ABI> {
pub address: Address,
pub abi: ABI,
}
impl<ABI> Contract<ABI> {
pub fn new<A: Into<Address>>(address: A, abi: ABI) -> Self {
Self {
address: address.into(),
abi,
}
}
}

View File

@ -18,10 +18,10 @@
pub mod providers; pub mod providers;
pub use providers::HttpProvider; pub use providers::HttpProvider;
mod contract; pub mod contract;
pub use contract::Contract; pub use contract::Contract;
mod signers; pub(crate) mod signers;
pub use signers::{AnyWallet, MainnetWallet, Signer}; pub use signers::{AnyWallet, MainnetWallet, Signer};
/// Ethereum related datatypes /// Ethereum related datatypes
@ -32,3 +32,6 @@ pub use solc;
/// Various utilities /// Various utilities
pub mod utils; pub mod utils;
/// ABI utilities
pub mod abi;

View File

@ -44,7 +44,7 @@ impl<P: JsonRpcClient> Provider<P> {
/// Connects to a signer and returns a client /// Connects to a signer and returns a client
pub fn connect<S: Signer>(&self, signer: S) -> Client<S, P> { pub fn connect<S: Signer>(&self, signer: S) -> Client<S, P> {
Client { Client {
signer, signer: Some(signer),
provider: self, provider: self,
} }
} }
@ -141,6 +141,14 @@ impl<P: JsonRpcClient> Provider<P> {
// State mutations // State mutations
/// Broadcasts the transaction request via the `eth_sendTransaction` API
pub async fn call<T: for<'a> Deserialize<'a>>(
&self,
tx: TransactionRequest,
) -> Result<T, P::Error> {
self.0.request("eth_call", Some(tx)).await
}
/// Broadcasts the transaction request via the `eth_sendTransaction` API /// Broadcasts the transaction request via the `eth_sendTransaction` API
pub async fn send_transaction(&self, tx: TransactionRequest) -> Result<TxHash, P::Error> { pub async fn send_transaction(&self, tx: TransactionRequest) -> Result<TxHash, P::Error> {
self.0.request("eth_sendTransaction", Some(tx)).await self.0.request("eth_sendTransaction", Some(tx)).await

View File

@ -1,7 +1,8 @@
use crate::{ use crate::{
providers::{JsonRpcClient, Provider}, providers::{JsonRpcClient, Provider},
signers::Signer, signers::Signer,
types::{Address, BlockNumber, Transaction, TransactionRequest}, types::{Address, BlockNumber, Overrides, TransactionRequest, TxHash},
utils,
}; };
use std::ops::Deref; use std::ops::Deref;
@ -9,18 +10,52 @@ use std::ops::Deref;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Client<'a, S, P> { pub struct Client<'a, S, P> {
pub(crate) provider: &'a Provider<P>, pub(crate) provider: &'a Provider<P>,
pub(crate) signer: S, pub(crate) signer: Option<S>,
}
impl<'a, S, P> From<&'a Provider<P>> for Client<'a, S, P> {
fn from(provider: &'a Provider<P>) -> Self {
Client {
provider,
signer: None,
}
}
} }
impl<'a, S: Signer, P: JsonRpcClient> Client<'a, S, P> { impl<'a, S: Signer, P: JsonRpcClient> Client<'a, S, P> {
/// Signs the transaction and then broadcasts its RLP encoding via the `eth_sendRawTransaction` /// Signs the transaction and then broadcasts its RLP encoding via the `eth_sendRawTransaction`
/// API /// API
pub async fn sign_and_send_transaction( pub async fn send_transaction(
&self, &self,
mut tx: TransactionRequest, mut tx: TransactionRequest,
block: Option<BlockNumber>, block: Option<BlockNumber>,
) -> Result<Transaction, P::Error> { ) -> Result<TxHash, P::Error> {
// 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 // TODO: Convert to join'ed futures
async fn fill_transaction(
&self,
tx: &mut TransactionRequest,
block: Option<BlockNumber>,
) -> Result<(), P::Error> {
// get the gas price // get the gas price
if tx.gas_price.is_none() { if tx.gas_price.is_none() {
tx.gas_price = Some(self.provider.get_gas_price().await?); 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 Ok(())
let signed_tx = self.signer.sign_transaction(tx).unwrap(); // TODO }
// broadcast it /// client.call_contract(
self.provider.send_raw_transaction(&signed_tx).await?; /// addr,
/// "transfer(address,uint256)"
/// vec![0x1234, 100]
/// None,
/// None,
/// )
pub async fn call_contract(
&self,
to: impl Into<Address>,
signature: &str,
args: &[ethabi::Token],
overrides: Option<Overrides>,
block: Option<BlockNumber>,
) -> Result<TxHash, P::Error> {
// create the data field from the function signature and the arguments
let data = [&utils::id(signature)[..], &ethabi::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 { pub fn address(&self) -> Address {
self.signer.address() self.signer
.as_ref()
.map(|s| s.address())
.unwrap_or_default()
} }
} }

View File

@ -52,7 +52,7 @@ impl<N: Network> Wallet<N> {
/// Connects to a provider and returns a client /// Connects to a provider and returns a client
pub fn connect<P: JsonRpcClient>(self, provider: &Provider<P>) -> Client<Wallet<N>, P> { pub fn connect<P: JsonRpcClient>(self, provider: &Provider<P>) -> Client<Wallet<N>, P> {
Client { Client {
signer: self, signer: Some(self),
provider, provider,
} }
} }

View File

@ -12,6 +12,12 @@ pub struct Bytes(
pub Vec<u8>, pub Vec<u8>,
); );
impl AsRef<[u8]> for Bytes {
fn as_ref(&self) -> &[u8] {
&self.0[..]
}
}
impl Bytes { impl Bytes {
/// Returns an empty bytes vector /// Returns an empty bytes vector
pub fn new() -> Self { pub fn new() -> Self {

View File

@ -7,7 +7,7 @@ pub use ethereum_types::H256 as TxHash;
pub use ethereum_types::{Address, Bloom, H256, U256, U64}; pub use ethereum_types::{Address, Bloom, H256, U256, U64};
mod transaction; mod transaction;
pub use transaction::{Transaction, TransactionReceipt, TransactionRequest}; pub use transaction::{Overrides, Transaction, TransactionReceipt, TransactionRequest};
mod keys; mod keys;
pub use keys::{PrivateKey, PublicKey, TxError}; pub use keys::{PrivateKey, PublicKey, TxError};
@ -23,5 +23,3 @@ pub use block::{Block, BlockId, BlockNumber};
mod log; mod log;
pub use log::{Filter, Log}; pub use log::{Filter, Log};

View File

@ -7,6 +7,31 @@ use rlp::RlpStream;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; 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<Address>,
/// Supplied gas (None for sensible default)
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) gas: Option<U256>,
/// Gas price (None for sensible default)
#[serde(rename = "gasPrice")]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) gas_price: Option<U256>,
/// Transfered value (None for no transfer)
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) value: Option<U256>,
/// Transaction nonce (None for next available nonce)
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) nonce: Option<U256>,
}
/// Parameters for sending a transaction /// Parameters for sending a transaction
#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)] #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)]
pub struct TransactionRequest { pub struct TransactionRequest {

View File

@ -1,5 +1,5 @@
//! Various utilities for manipulating Ethereum related dat //! Various utilities for manipulating Ethereum related dat
use crate::types::{H256, Selector}; use crate::types::{Selector, H256};
use tiny_keccak::{Hasher, Keccak}; use tiny_keccak::{Hasher, Keccak};
const PREFIX: &str = "\x19Ethereum Signed Message:\n"; const PREFIX: &str = "\x19Ethereum Signed Message:\n";