From 96add978695a2668f1259db7253cbc016dd1e9de Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Sun, 24 May 2020 21:34:56 +0300 Subject: [PATCH] use builder pattern for txs --- examples/local_signer.rs | 26 +++--------- examples/transfer_eth.rs | 13 ++---- src/providers/mod.rs | 25 +++++++++++ src/signers/client.rs | 26 +++++++++++- src/types/keys.rs | 9 +++- src/types/transaction.rs | 92 +++++++++++++++++++++++++++++++++++----- 6 files changed, 149 insertions(+), 42 deletions(-) diff --git a/examples/local_signer.rs b/examples/local_signer.rs index 2bed794c..c7577f59 100644 --- a/examples/local_signer.rs +++ b/examples/local_signer.rs @@ -1,6 +1,5 @@ use ethers::{types::TransactionRequest, HttpProvider, MainnetWallet}; use std::convert::TryFrom; -use std::str::FromStr; #[tokio::main] async fn main() -> Result<(), failure::Error> { @@ -8,28 +7,17 @@ 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 = MainnetWallet::from_str( - "15c42bf2987d5a8a73804a8ea72fb4149f88adf73e98fc3f8a8ce9f24fcb7774", - )? - .connect(&provider); - - // get the account's nonce (we abuse the Deref to access the provider's functions) - let nonce = client.get_transaction_count(client.address(), None).await?; - dbg!(nonce); + let client = "15c42bf2987d5a8a73804a8ea72fb4149f88adf73e98fc3f8a8ce9f24fcb7774" + .parse::()? + .connect(&provider); // craft the transaction - let tx = TransactionRequest { - from: None, - to: Some("986eE0C8B91A58e490Ee59718Cca41056Cf55f24".parse().unwrap()), - gas: Some(21000.into()), - gas_price: Some(100_000.into()), - value: Some(10000.into()), - data: Some(vec![].into()), - nonce: Some(nonce), - }; + let tx = TransactionRequest::new() + .send_to_str("986eE0C8B91A58e490Ee59718Cca41056Cf55f24")? + .value(10000); // send it! - let tx = client.sign_and_send_transaction(tx).await?; + let tx = client.sign_and_send_transaction(tx, None).await?; // get the mined tx let tx = client.get_transaction(tx.hash).await?; diff --git a/examples/transfer_eth.rs b/examples/transfer_eth.rs index b46e3eba..dc83eab6 100644 --- a/examples/transfer_eth.rs +++ b/examples/transfer_eth.rs @@ -12,15 +12,10 @@ async fn main() -> Result<(), failure::Error> { let from = accounts[0]; // craft the tx - let tx = TransactionRequest { - from: Some(from), - to: Some("9A7e5d4bcA656182e66e33340d776D1542143006".parse()?), - value: Some(1000u64.into()), - gas: None, - gas_price: None, - data: None, - nonce: None, - }; + let tx = TransactionRequest::new() + .send_to_str("9A7e5d4bcA656182e66e33340d776D1542143006")? + .value(1000) + .from(from); // specify the `from` field so that the client knows which account to use let balance_before = provider.get_balance(from, None).await?; diff --git a/src/providers/mod.rs b/src/providers/mod.rs index f33ee288..7a166083 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -46,6 +46,29 @@ impl Provider

{ } } + // Cost related + + /// Gets the current gas price as estimated by the node + pub async fn get_gas_price(&self) -> Result { + self.0.request("eth_gasPrice", None::<()>).await + } + + /// Tries to estimate the gas for the transaction + pub async fn estimate_gas( + &self, + tx: &TransactionRequest, + block: Option, + ) -> Result { + let tx = utils::serialize(tx); + + let args = match block { + Some(block) => vec![tx, utils::serialize(&block)], + None => vec![tx], + }; + + self.0.request("eth_estimateGas", Some(args)).await + } + /// Gets the accounts on the node pub async fn get_accounts(&self) -> Result, P::Error> { self.0.request("eth_accounts", None::<()>).await @@ -65,6 +88,8 @@ impl Provider

{ self.0.request("eth_getTransactionByHash", Some(hash)).await } + // State mutations + /// 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 591c0205..1f98d932 100644 --- a/src/signers/client.rs +++ b/src/signers/client.rs @@ -1,7 +1,7 @@ use crate::{ providers::{JsonRpcClient, Provider}, signers::Signer, - types::{Address, Transaction, TransactionRequest}, + types::{Address, BlockNumber, Transaction, TransactionRequest}, }; use std::ops::Deref; @@ -17,8 +17,30 @@ impl<'a, S: Signer, P: JsonRpcClient> Client<'a, S, P> { /// API pub async fn sign_and_send_transaction( &self, - tx: TransactionRequest, + mut tx: TransactionRequest, + block: Option, ) -> Result { + // TODO: Convert to join'ed futures + // get the gas price + if tx.gas_price.is_none() { + tx.gas_price = Some(self.provider.get_gas_price().await?); + } + + // estimate the gas + if tx.gas.is_none() { + tx.from = Some(self.address()); + tx.gas = Some(self.provider.estimate_gas(&tx, block).await?); + } + + // set our nonce + if tx.nonce.is_none() { + tx.nonce = Some( + self.provider + .get_transaction_count(self.address(), block) + .await?, + ); + } + // sign the transaction let signed_tx = self.signer.sign_transaction(tx).unwrap(); // TODO diff --git a/src/types/keys.rs b/src/types/keys.rs index 7e135bb7..1b11e117 100644 --- a/src/types/keys.rs +++ b/src/types/keys.rs @@ -59,13 +59,18 @@ impl PrivateKey { self.sign_with_eip155(&sig_message, None) } - /// RLP encodes and then signs the stransaction. If no chain_id is provided, then EIP-155 is - /// not used. + /// RLP encodes and then signs the stransaction. + /// + /// If no chain_id is provided, then EIP-155 is not used. + /// + /// This will return an error if called if any of the `nonce`, `gas_price` or `gas` + /// fields are not populated. pub fn sign_transaction( &self, tx: TransactionRequest, chain_id: Option, ) -> Result { + // Calling ` let nonce = tx.nonce.ok_or(TxError::NonceMissing)?; let gas_price = tx.gas_price.ok_or(TxError::NonceMissing)?; let gas = tx.gas.ok_or(TxError::NonceMissing)?; diff --git a/src/types/transaction.rs b/src/types/transaction.rs index a5739399..6df68e0b 100644 --- a/src/types/transaction.rs +++ b/src/types/transaction.rs @@ -5,9 +5,10 @@ use crate::{ }; use rlp::RlpStream; use serde::{Deserialize, Serialize}; +use std::str::FromStr; /// Parameters for sending a transaction -#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)] +#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)] pub struct TransactionRequest { /// Sender address or ENS name #[serde(skip_serializing_if = "Option::is_none")] @@ -41,27 +42,89 @@ pub struct TransactionRequest { } impl TransactionRequest { - fn rlp_base(&self, rlp: &mut RlpStream) { - rlp_opt(rlp, self.nonce); - rlp_opt(rlp, self.gas_price); - rlp_opt(rlp, self.gas); - rlp_opt(rlp, self.to); - rlp_opt(rlp, self.value); - rlp_opt(rlp, self.data.as_ref().map(|d| &d.0[..])); + /// Creates an empty transaction request with all fields left empty + pub fn new() -> Self { + Self::default() } - pub fn hash(&self, chain_id: Option) -> H256 { + /// Convenience function for sending a new payment transaction to the receiver. The + /// `gas`, `gas_price` and `nonce` fields are left empty, to be populated + pub fn pay, V: Into>(to: T, value: V) -> Self { + TransactionRequest { + from: None, + to: Some(to.into()), + gas: None, + gas_price: None, + value: Some(value.into()), + data: None, + nonce: None, + } + } + + // Builder pattern helpers + + /// Sets the `from` field in the transaction to the provided value + pub fn from>(mut self, from: T) -> Self { + self.from = Some(from.into()); + self + } + + /// Sets the `to` field in the transaction to the provided value + pub fn send_to_str(mut self, to: &str) -> Result { + self.to = Some(Address::from_str(to)?); + Ok(self) + } + + /// Sets the `to` field in the transaction to the provided value + pub fn to>(mut self, to: T) -> Self { + self.to = Some(to.into()); + self + } + + /// Sets the `gas` field in the transaction to the provided value + pub fn gas>(mut self, gas: T) -> Self { + self.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.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.value = Some(value.into()); + self + } + + /// Sets the `data` field in the transaction to the provided value + pub fn data>(mut self, data: T) -> Self { + self.data = Some(data.into()); + self + } + + /// Sets the `nonce` field in the transaction to the provided value + pub fn nonce>(mut self, nonce: T) -> Self { + self.nonce = Some(nonce.into()); + self + } + + /// Hashes the transaction's data with the provided chain id + pub fn hash>(&self, chain_id: Option) -> H256 { let mut rlp = RlpStream::new(); rlp.begin_list(9); self.rlp_base(&mut rlp); - rlp.append(&chain_id.unwrap_or_else(U64::zero)); + rlp.append(&chain_id.map(|c| c.into()).unwrap_or_else(U64::zero)); rlp.append(&0u8); rlp.append(&0u8); keccak256(rlp.out().as_ref()).into() } + /// Produces the RLP encoding of the transaction with the provided signature pub fn rlp_signed(&self, signature: &Signature) -> Bytes { let mut rlp = RlpStream::new(); rlp.begin_list(9); @@ -73,6 +136,15 @@ impl TransactionRequest { rlp.out().into() } + + fn rlp_base(&self, rlp: &mut RlpStream) { + rlp_opt(rlp, self.nonce); + rlp_opt(rlp, self.gas_price); + rlp_opt(rlp, self.gas); + rlp_opt(rlp, self.to); + rlp_opt(rlp, self.value); + rlp_opt(rlp, self.data.as_ref().map(|d| &d.0[..])); + } } fn rlp_opt(rlp: &mut RlpStream, opt: Option) {