use builder pattern for txs

This commit is contained in:
Georgios Konstantopoulos 2020-05-24 21:34:56 +03:00
parent f8ca81ab22
commit 96add97869
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
6 changed files with 149 additions and 42 deletions

View File

@ -1,6 +1,5 @@
use ethers::{types::TransactionRequest, HttpProvider, MainnetWallet}; use ethers::{types::TransactionRequest, HttpProvider, MainnetWallet};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::str::FromStr;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), failure::Error> { async fn main() -> Result<(), failure::Error> {
@ -8,28 +7,17 @@ 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 = MainnetWallet::from_str( let client = "15c42bf2987d5a8a73804a8ea72fb4149f88adf73e98fc3f8a8ce9f24fcb7774"
"15c42bf2987d5a8a73804a8ea72fb4149f88adf73e98fc3f8a8ce9f24fcb7774", .parse::<MainnetWallet>()?
)? .connect(&provider);
.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);
// craft the transaction // craft the transaction
let tx = TransactionRequest { let tx = TransactionRequest::new()
from: None, .send_to_str("986eE0C8B91A58e490Ee59718Cca41056Cf55f24")?
to: Some("986eE0C8B91A58e490Ee59718Cca41056Cf55f24".parse().unwrap()), .value(10000);
gas: Some(21000.into()),
gas_price: Some(100_000.into()),
value: Some(10000.into()),
data: Some(vec![].into()),
nonce: Some(nonce),
};
// send it! // 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 // get the mined tx
let tx = client.get_transaction(tx.hash).await?; let tx = client.get_transaction(tx.hash).await?;

View File

@ -12,15 +12,10 @@ async fn main() -> Result<(), failure::Error> {
let from = accounts[0]; let from = accounts[0];
// craft the tx // craft the tx
let tx = TransactionRequest { let tx = TransactionRequest::new()
from: Some(from), .send_to_str("9A7e5d4bcA656182e66e33340d776D1542143006")?
to: Some("9A7e5d4bcA656182e66e33340d776D1542143006".parse()?), .value(1000)
value: Some(1000u64.into()), .from(from); // specify the `from` field so that the client knows which account to use
gas: None,
gas_price: None,
data: None,
nonce: None,
};
let balance_before = provider.get_balance(from, None).await?; let balance_before = provider.get_balance(from, None).await?;

View File

@ -46,6 +46,29 @@ impl<P: JsonRpcClient> Provider<P> {
} }
} }
// Cost related
/// Gets the current gas price as estimated by the node
pub async fn get_gas_price(&self) -> Result<U256, P::Error> {
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<BlockNumber>,
) -> Result<U256, P::Error> {
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 /// Gets the accounts on the node
pub async fn get_accounts(&self) -> Result<Vec<Address>, P::Error> { pub async fn get_accounts(&self) -> Result<Vec<Address>, P::Error> {
self.0.request("eth_accounts", None::<()>).await self.0.request("eth_accounts", None::<()>).await
@ -65,6 +88,8 @@ impl<P: JsonRpcClient> Provider<P> {
self.0.request("eth_getTransactionByHash", Some(hash)).await self.0.request("eth_getTransactionByHash", Some(hash)).await
} }
// State mutations
/// 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,7 @@
use crate::{ use crate::{
providers::{JsonRpcClient, Provider}, providers::{JsonRpcClient, Provider},
signers::Signer, signers::Signer,
types::{Address, Transaction, TransactionRequest}, types::{Address, BlockNumber, Transaction, TransactionRequest},
}; };
use std::ops::Deref; use std::ops::Deref;
@ -17,8 +17,30 @@ impl<'a, S: Signer, P: JsonRpcClient> Client<'a, S, P> {
/// API /// API
pub async fn sign_and_send_transaction( pub async fn sign_and_send_transaction(
&self, &self,
tx: TransactionRequest, mut tx: TransactionRequest,
block: Option<BlockNumber>,
) -> Result<Transaction, P::Error> { ) -> Result<Transaction, P::Error> {
// 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 // sign the transaction
let signed_tx = self.signer.sign_transaction(tx).unwrap(); // TODO let signed_tx = self.signer.sign_transaction(tx).unwrap(); // TODO

View File

@ -59,13 +59,18 @@ impl PrivateKey {
self.sign_with_eip155(&sig_message, None) self.sign_with_eip155(&sig_message, None)
} }
/// RLP encodes and then signs the stransaction. If no chain_id is provided, then EIP-155 is /// RLP encodes and then signs the stransaction.
/// not used. ///
/// 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( pub fn sign_transaction(
&self, &self,
tx: TransactionRequest, tx: TransactionRequest,
chain_id: Option<U64>, chain_id: Option<U64>,
) -> Result<Transaction, TxError> { ) -> Result<Transaction, TxError> {
// Calling `
let nonce = tx.nonce.ok_or(TxError::NonceMissing)?; let nonce = tx.nonce.ok_or(TxError::NonceMissing)?;
let gas_price = tx.gas_price.ok_or(TxError::NonceMissing)?; let gas_price = tx.gas_price.ok_or(TxError::NonceMissing)?;
let gas = tx.gas.ok_or(TxError::NonceMissing)?; let gas = tx.gas.ok_or(TxError::NonceMissing)?;

View File

@ -5,9 +5,10 @@ use crate::{
}; };
use rlp::RlpStream; use rlp::RlpStream;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr;
/// Parameters for sending a transaction /// Parameters for sending a transaction
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)] #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)]
pub struct TransactionRequest { pub struct TransactionRequest {
/// Sender address or ENS name /// Sender address or ENS name
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@ -41,27 +42,89 @@ pub struct TransactionRequest {
} }
impl TransactionRequest { impl TransactionRequest {
fn rlp_base(&self, rlp: &mut RlpStream) { /// Creates an empty transaction request with all fields left empty
rlp_opt(rlp, self.nonce); pub fn new() -> Self {
rlp_opt(rlp, self.gas_price); Self::default()
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[..]));
} }
pub fn hash(&self, chain_id: Option<U64>) -> 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<T: Into<Address>, V: Into<U256>>(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<T: Into<Address>>(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, rustc_hex::FromHexError> {
self.to = Some(Address::from_str(to)?);
Ok(self)
}
/// Sets the `to` field in the transaction to the provided value
pub fn to<T: Into<Address>>(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<T: Into<U256>>(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<T: Into<U256>>(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<T: Into<U256>>(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<T: Into<Bytes>>(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<T: Into<U256>>(mut self, nonce: T) -> Self {
self.nonce = Some(nonce.into());
self
}
/// Hashes the transaction's data with the provided chain id
pub fn hash<T: Into<U64>>(&self, chain_id: Option<T>) -> H256 {
let mut rlp = RlpStream::new(); let mut rlp = RlpStream::new();
rlp.begin_list(9); rlp.begin_list(9);
self.rlp_base(&mut rlp); 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);
rlp.append(&0u8); rlp.append(&0u8);
keccak256(rlp.out().as_ref()).into() 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 { pub fn rlp_signed(&self, signature: &Signature) -> Bytes {
let mut rlp = RlpStream::new(); let mut rlp = RlpStream::new();
rlp.begin_list(9); rlp.begin_list(9);
@ -73,6 +136,15 @@ impl TransactionRequest {
rlp.out().into() 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<T: rlp::Encodable>(rlp: &mut RlpStream, opt: Option<T>) { fn rlp_opt<T: rlp::Encodable>(rlp: &mut RlpStream, opt: Option<T>) {