From 08a25fb848f9ee4aa76956aa559d2a6fec75d5f2 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Sun, 24 May 2020 20:17:46 +0300 Subject: [PATCH] deduplicate tx types --- examples/local_signer.rs | 18 +++++----- examples/transfer_eth.rs | 4 +-- src/signers/client.rs | 6 ++-- src/signers/mod.rs | 10 ++++-- src/signers/wallet.rs | 6 ++-- src/types/keys.rs | 52 +++++++++++++++++++--------- src/types/mod.rs | 6 ++-- src/types/transaction.rs | 74 ++++++++++++---------------------------- 8 files changed, 86 insertions(+), 90 deletions(-) diff --git a/examples/local_signer.rs b/examples/local_signer.rs index 21f1f5ab..2bed794c 100644 --- a/examples/local_signer.rs +++ b/examples/local_signer.rs @@ -1,4 +1,4 @@ -use ethers::{types::UnsignedTransaction, HttpProvider, MainnetWallet}; +use ethers::{types::TransactionRequest, HttpProvider, MainnetWallet}; use std::convert::TryFrom; use std::str::FromStr; @@ -9,21 +9,23 @@ async fn main() -> Result<(), failure::Error> { // create a wallet and connect it to the provider let client = MainnetWallet::from_str( - "d8ebe1e50cfea1f9961908d9df28e64bb163fee9ee48320361b2eb0a54974269", + "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); // craft the transaction - let tx = UnsignedTransaction { + let tx = TransactionRequest { + from: None, to: Some("986eE0C8B91A58e490Ee59718Cca41056Cf55f24".parse().unwrap()), - gas: 21000.into(), - gas_price: 100_000.into(), - value: 10000.into(), - input: vec![].into(), - nonce, + gas: Some(21000.into()), + gas_price: Some(100_000.into()), + value: Some(10000.into()), + data: Some(vec![].into()), + nonce: Some(nonce), }; // send it! diff --git a/examples/transfer_eth.rs b/examples/transfer_eth.rs index 45cab594..75f19a01 100644 --- a/examples/transfer_eth.rs +++ b/examples/transfer_eth.rs @@ -8,11 +8,11 @@ use std::convert::TryFrom; async fn main() -> Result<(), failure::Error> { // connect to the network let provider = HttpProvider::try_from("http://localhost:8545")?; - let from = "4916064D2E9C1b2ccC466EEc3d30B2b08F1C130D".parse()?; + let from = "784C1bA9846aB4CE78E9CFa27884E29dd31d593A".parse()?; // craft the tx let tx = TransactionRequest { - from, + from: Some(from), to: Some("9A7e5d4bcA656182e66e33340d776D1542143006".parse()?), value: Some(1000u64.into()), gas: None, diff --git a/src/signers/client.rs b/src/signers/client.rs index ed1dbe21..591c0205 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, UnsignedTransaction}, + types::{Address, Transaction, TransactionRequest}, }; use std::ops::Deref; @@ -17,10 +17,10 @@ impl<'a, S: Signer, P: JsonRpcClient> Client<'a, S, P> { /// API pub async fn sign_and_send_transaction( &self, - tx: UnsignedTransaction, + tx: TransactionRequest, ) -> Result { // sign the transaction - let signed_tx = self.signer.sign_transaction(tx.clone()); + let signed_tx = self.signer.sign_transaction(tx).unwrap(); // TODO // broadcast it self.provider.send_raw_transaction(&signed_tx).await?; diff --git a/src/signers/mod.rs b/src/signers/mod.rs index 5b2a67d8..a53bd50d 100644 --- a/src/signers/mod.rs +++ b/src/signers/mod.rs @@ -1,4 +1,8 @@ //! Sign and broadcast transactions +//! +//! Implement the `Signer` trait to add support for new signers, e.g. with Ledger. +//! +//! TODO: We might need a `SignerAsync` trait for HSM use cases? mod networks; pub use networks::instantiated::*; use networks::Network; @@ -9,17 +13,19 @@ pub use wallet::Wallet; mod client; pub(crate) use client::Client; -use crate::types::{Address, Signature, Transaction, UnsignedTransaction}; +use crate::types::{Address, Signature, Transaction, TransactionRequest}; +use std::error::Error; /// Trait for signing transactions and messages /// /// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. pub trait Signer { + type Error: Error; /// Signs the hash of the provided message after prefixing it fn sign_message>(&self, message: S) -> Signature; /// Signs the transaction - fn sign_transaction(&self, message: UnsignedTransaction) -> Transaction; + fn sign_transaction(&self, message: TransactionRequest) -> Result; /// Returns the signer's Ethereum Address fn address(&self) -> Address; diff --git a/src/signers/wallet.rs b/src/signers/wallet.rs index ccd107aa..46022637 100644 --- a/src/signers/wallet.rs +++ b/src/signers/wallet.rs @@ -1,7 +1,7 @@ use crate::{ providers::{JsonRpcClient, Provider}, signers::{Client, Network, Signer}, - types::{Address, PrivateKey, PublicKey, Signature, Transaction, UnsignedTransaction}, + types::{Address, PrivateKey, PublicKey, Signature, Transaction, TransactionRequest, TxError}, }; use rand::Rng; @@ -17,11 +17,13 @@ pub struct Wallet { } impl<'a, N: Network> Signer for Wallet { + type Error = TxError; + fn sign_message>(&self, message: S) -> Signature { self.private_key.sign(message) } - fn sign_transaction(&self, tx: UnsignedTransaction) -> Transaction { + fn sign_transaction(&self, tx: TransactionRequest) -> Result { self.private_key.sign_transaction(tx, N::CHAIN_ID) } diff --git a/src/types/keys.rs b/src/types/keys.rs index 3a3743fb..7e135bb7 100644 --- a/src/types/keys.rs +++ b/src/types/keys.rs @@ -4,10 +4,11 @@ use secp256k1::{ }; use std::ops::Deref; use std::str::FromStr; +use thiserror::Error; use zeroize::DefaultIsZeroes; use crate::{ - types::{Address, Signature, Transaction, UnsignedTransaction, H256, U256, U64}, + types::{Address, Signature, Transaction, TransactionRequest, H256, U256, U64}, utils::{hash_message, keccak256}, }; @@ -24,6 +25,16 @@ impl FromStr for PrivateKey { } } +#[derive(Clone, Debug, Error)] +pub enum TxError { + #[error("no nonce was specified")] + NonceMissing, + #[error("no gas price was specified")] + GasPriceMissing, + #[error("no gas was specified")] + GasMissing, +} + impl PrivateKey { pub fn new(rng: &mut R) -> Self { PrivateKey(SecretKey::new(rng)) @@ -50,7 +61,15 @@ impl PrivateKey { /// RLP encodes and then signs the stransaction. If no chain_id is provided, then EIP-155 is /// not used. - pub fn sign_transaction(&self, tx: UnsignedTransaction, chain_id: Option) -> Transaction { + pub fn sign_transaction( + &self, + tx: TransactionRequest, + chain_id: Option, + ) -> Result { + 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)?; + // Hash the transaction's RLP encoding let hash = tx.hash(chain_id); let message = Message::from_slice(hash.as_bytes()).expect("hash is non-zero 32-bytes; qed"); @@ -60,15 +79,15 @@ impl PrivateKey { let rlp = tx.rlp_signed(&signature); let hash = keccak256(&rlp.0); - Transaction { + Ok(Transaction { hash: hash.into(), - nonce: tx.nonce, + nonce, from: self.into(), to: tx.to, - value: tx.value, - gas_price: tx.gas_price, - gas: tx.gas, - input: tx.input, + value: tx.value.unwrap_or_default(), + gas_price, + gas, + input: tx.data.unwrap_or_default(), v: signature.v.into(), r: U256::from_big_endian(signature.r.as_bytes()), s: U256::from_big_endian(signature.s.as_bytes()), @@ -77,7 +96,7 @@ impl PrivateKey { block_hash: None, block_number: None, transaction_index: None, - } + }) } fn sign_with_eip155(&self, message: &Message, chain_id: Option) -> Signature { @@ -193,13 +212,14 @@ mod tests { fn signs_tx() { // retrieved test vector from: // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction - let tx = UnsignedTransaction { + let tx = TransactionRequest { + from: None, to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse().unwrap()), - value: 1_000_000_000.into(), - gas: 2_000_000.into(), - nonce: 0.into(), - gas_price: 21_000_000_000u128.into(), - input: Bytes::new(), + value: Some(1_000_000_000.into()), + gas: Some(2_000_000.into()), + nonce: Some(0.into()), + gas_price: Some(21_000_000_000u128.into()), + data: None, }; let chain_id = 1; @@ -207,7 +227,7 @@ mod tests { .parse() .unwrap(); - let tx = key.sign_transaction(tx, Some(chain_id.into())); + let tx = key.sign_transaction(tx, Some(chain_id.into())).unwrap(); assert_eq!( tx.hash, diff --git a/src/types/mod.rs b/src/types/mod.rs index 542c1724..9d9b6f2b 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -5,12 +5,10 @@ pub use ethereum_types::H256 as TxHash; pub use ethereum_types::{Address, H256, U256, U64}; mod transaction; -// TODO: Figure out some more intuitive way instead of having 3 similarly named structs -// with the same fields -pub use transaction::{Transaction, TransactionRequest, UnsignedTransaction}; +pub use transaction::{Transaction, TransactionRequest}; mod keys; -pub use keys::{PrivateKey, PublicKey}; +pub use keys::{PrivateKey, PublicKey, TxError}; mod signature; pub use signature::Signature; diff --git a/src/types/transaction.rs b/src/types/transaction.rs index 609effe2..a5739399 100644 --- a/src/types/transaction.rs +++ b/src/types/transaction.rs @@ -1,23 +1,17 @@ //! Transaction types -//! -//! We define 3 transaction types, `TransactionRequest`, `UnsignedTransaction` and `Transaction`. -//! `TransactionRequest` and `UnsignedTransaction` are both unsigned transaction objects. -//! -//! The former gets submitted to the node via an `eth_sendTransaction` call, which populates any missing fields, -//! signs it and broadcasts it. The latter is signed locally by a private key, and the signed -//! transaction is broadcast via `eth_sendRawTransaction`. use crate::{ types::{Address, Bytes, Signature, H256, U256, U64}, utils::keccak256, }; - +use rlp::RlpStream; use serde::{Deserialize, Serialize}; -/// Parameters for sending a transaction to via the eth_sendTransaction API. +/// Parameters for sending a transaction #[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)] pub struct TransactionRequest { /// Sender address or ENS name - pub from: Address, + #[serde(skip_serializing_if = "Option::is_none")] + pub from: Option
, /// Recipient address (None for contract creation) #[serde(skip_serializing_if = "Option::is_none")] @@ -46,40 +40,14 @@ pub struct TransactionRequest { pub nonce: Option, } -/// A raw unsigned transaction where all the information is already specified -#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)] -pub struct UnsignedTransaction { - /// Recipient address (None for contract creation) - pub to: Option
, - - /// Supplied gas - pub gas: U256, - - /// Gas price - #[serde(rename = "gasPrice")] - pub gas_price: U256, - - /// Transfered value - pub value: U256, - - /// The compiled code of a contract OR the first 4 bytes of the hash of the - /// invoked method signature and encoded parameters. For details see Ethereum Contract ABI - pub input: Bytes, - - /// Transaction nonce (None for next available nonce) - pub nonce: U256, -} - -use rlp::RlpStream; - -impl UnsignedTransaction { +impl TransactionRequest { fn rlp_base(&self, rlp: &mut RlpStream) { - rlp.append(&self.nonce); - rlp.append(&self.gas_price); - rlp.append(&self.gas); - rlp_opt(rlp, &self.to); - rlp.append(&self.value); - rlp.append(&self.input.0); + 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[..])); } pub fn hash(&self, chain_id: Option) -> H256 { @@ -107,6 +75,14 @@ impl UnsignedTransaction { } } +fn rlp_opt(rlp: &mut RlpStream, opt: Option) { + if let Some(ref inner) = opt { + rlp.append(inner); + } else { + rlp.append(&""); + } +} + /// Details of a signed transaction #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Transaction { @@ -172,7 +148,7 @@ impl Transaction { rlp.append(&self.nonce); rlp.append(&self.gas_price); rlp.append(&self.gas); - rlp_opt(&mut rlp, &self.to); + rlp_opt(&mut rlp, self.to); rlp.append(&self.value); rlp.append(&self.input.0); rlp.append(&self.v); @@ -183,21 +159,13 @@ impl Transaction { } } -fn rlp_opt(rlp: &mut RlpStream, opt: &Option) { - if let Some(inner) = opt { - rlp.append(inner); - } else { - rlp.append(&""); - } -} - #[cfg(test)] mod tests { use super::*; #[test] fn decode_unsigned_transaction() { - let _res: UnsignedTransaction = serde_json::from_str( + let _res: TransactionRequest = serde_json::from_str( r#"{ "gas":"0xc350", "gasPrice":"0x4a817c800",