From 09ff480f19bbdb4abbca7956f044916bdb2383cd Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Mon, 9 Aug 2021 01:49:23 +0300 Subject: [PATCH] feat: eip2930/1559 response type adjustments (part 1) (#353) * refactor: move tx types to subdirectory * feat(receipt): add tx-type and effective gas price for eip1559 * feat: add base_fee_per_gas to Block * feat(tx): add eip1559/2930/2718 ret values * fix(middleware): return signed raw rlp from tx signer / adjust tests * chore: remove unused vars * chore(core): remove funty lock from Cargo.toml * chore: rename json var / cargo fmt * add more context on eip1559 fees --- Cargo.lock | 13 +- ethers-core/Cargo.toml | 5 +- ethers-core/src/types/block.rs | 41 +++ ethers-core/src/types/mod.rs | 3 +- ethers-core/src/types/transaction/eip2930.rs | 24 ++ ethers-core/src/types/transaction/mod.rs | 21 ++ .../request.rs} | 214 +------------ ethers-core/src/types/transaction/response.rs | 290 ++++++++++++++++++ ethers-middleware/src/signer.rs | 122 +++----- ethers-providers/src/lib.rs | 2 +- ethers-providers/src/provider.rs | 4 +- 11 files changed, 438 insertions(+), 301 deletions(-) create mode 100644 ethers-core/src/types/transaction/eip2930.rs create mode 100644 ethers-core/src/types/transaction/mod.rs rename ethers-core/src/types/{transaction.rs => transaction/request.rs} (50%) create mode 100644 ethers-core/src/types/transaction/response.rs diff --git a/Cargo.lock b/Cargo.lock index 5f818eca..9d31619f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,7 +875,6 @@ dependencies = [ "elliptic-curve", "ethabi", "ethers", - "funty", "futures-util", "generic-array 0.14.4", "glob", @@ -885,6 +884,7 @@ dependencies = [ "once_cell", "rand 0.8.4", "rlp", + "rlp-derive", "serde", "serde_json", "thiserror", @@ -2124,6 +2124,17 @@ dependencies = [ "rustc-hex", ] +[[package]] +name = "rlp-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33d7b2abe0c340d8797fe2907d3f20d3b5ea5908683618bfe80df7f621f672a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "rusb" version = "0.8.1" diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml index 26151cc7..66919fec 100644 --- a/ethers-core/Cargo.toml +++ b/ethers-core/Cargo.toml @@ -13,6 +13,7 @@ keywords = ["ethereum", "web3", "celo", "ethers"] rlp = { version = "0.5.0", default-features = false } ethabi = { version = "14.1.0", default-features = false } arrayvec = { version = "0.7.1", default-features = false } +rlp-derive = { version = "0.1.0", default-features = false } # crypto ecdsa = { version = "0.12.3", default-features = false, features = ["std"] } @@ -30,10 +31,6 @@ glob = { version = "0.3.0", default-features = false } bytes = { version = "1.0.1", features = ["serde"] } hex = { version = "0.4.3", default-features = false, features = ["std"] } -# bitvec compilation issue -# https://github.com/bitvecto-rs/bitvec/issues/105#issuecomment-778570981 -funty = "=1.1.0" - # async tokio = { version = "1.5", default-features = false, optional = true} futures-util = { version = "0.3.16", default-features = false, optional = true} diff --git a/ethers-core/src/types/block.rs b/ethers-core/src/types/block.rs index 33f558a3..fcfd0c3a 100644 --- a/ethers-core/src/types/block.rs +++ b/ethers-core/src/types/block.rs @@ -71,6 +71,9 @@ pub struct Block { /// Nonce #[cfg(not(feature = "celo"))] pub nonce: Option, + /// Base fee per unit of gas (if past London) + #[serde(rename = "baseFeePerGas")] + pub base_fee_per_gas: Option, #[cfg(feature = "celo")] #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] @@ -206,6 +209,44 @@ mod tests { let block = r#"{"number":"0x3","hash":"0xda53da08ef6a3cbde84c33e51c04f68c3853b6a3731f10baa2324968eee63972","parentHash":"0x689c70c080ca22bc0e681694fa803c1aba16a69c8b6368fed5311d279eb9de90","mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0000000000000000","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","transactionsRoot":"0x7270c1c4440180f2bd5215809ee3d545df042b67329499e1ab97eb759d31610d","stateRoot":"0x29f32984517a7d25607da485b23cefabfd443751422ca7e603395e1de9bc8a4b","receiptsRoot":"0x056b23fbba480696b65fe5a59b8f2148a1299103c4f57df839233af2cf4ca2d2","miner":"0x0000000000000000000000000000000000000000","difficulty":"0x0","totalDifficulty":"0x0","extraData":"0x","size":"0x3e8","gasLimit":"0x6691b7","gasUsed":"0x5208","timestamp":"0x5ecedbb9","transactions":[{"hash":"0xc3c5f700243de37ae986082fd2af88d2a7c2752a0c0f7b9d6ac47c729d45e067","nonce":"0x2","blockHash":"0xda53da08ef6a3cbde84c33e51c04f68c3853b6a3731f10baa2324968eee63972","blockNumber":"0x3","transactionIndex":"0x0","from":"0xfdcedc3bfca10ecb0890337fbdd1977aba84807a","to":"0xdca8ce283150ab773bcbeb8d38289bdb5661de1e","value":"0x0","gas":"0x15f90","gasPrice":"0x4a817c800","input":"0x","v":"0x25","r":"0x19f2694eb9113656dbea0b925e2e7ceb43df83e601c4116aee9c0dd99130be88","s":"0x73e5764b324a4f7679d890a198ba658ba1c8cd36983ff9797e10b1b89dbb448e"}],"uncles":[]}"#; let _block: Block = serde_json::from_str(block).unwrap(); } + + #[test] + // https://github.com/tomusdrw/rust-web3/commit/3a32ee962c0f2f8d50a5e25be9f2dfec7ae0750d + fn post_london_block() { + let json = serde_json::json!( + { + "baseFeePerGas": "0x7", + "miner": "0x0000000000000000000000000000000000000001", + "number": "0x1b4", + "hash": "0x0e670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1527331", + "parentHash": "0x9646252be9520f6e71339a8df9c55e4d7619deeb018d2a3f2d21fc165dde5eb5", + "mixHash": "0x1010101010101010101010101010101010101010101010101010101010101010", + "nonce": "0x0000000000000000", + "sealFields": [ + "0xe04d296d2460cfb8472af2c5fd05b5a214109c25688d3704aed5484f9a7792f2", + "0x0000000000000042" + ], + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "logsBloom": "0x0e670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273310e670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273310e670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273310e670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273310e670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273310e670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273310e670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273310e670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1527331", + "transactionsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "stateRoot": "0xd5855eb08b3387c0af375e9cdb6acfc05eb8f519e419b874b6ff2ffda7ed1dff", + "difficulty": "0x27f07", + "totalDifficulty": "0x27f07", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000", + "size": "0x27f07", + "gasLimit": "0x9f759", + "minGasPrice": "0x9f759", + "gasUsed": "0x9f759", + "timestamp": "0x54e34e8e", + "transactions": [], + "uncles": [] + } + ); + + let block: Block<()> = serde_json::from_value(json).unwrap(); + assert_eq!(block.base_fee_per_gas, Some(U256::from(7))); + } } #[cfg(test)] diff --git a/ethers-core/src/types/mod.rs b/ethers-core/src/types/mod.rs index 402dd22b..597b7cb6 100644 --- a/ethers-core/src/types/mod.rs +++ b/ethers-core/src/types/mod.rs @@ -8,7 +8,8 @@ pub use ethabi::ethereum_types::H256 as TxHash; pub use ethabi::ethereum_types::{Address, Bloom, H160, H256, U128, U256, U64}; mod transaction; -pub use transaction::{Transaction, TransactionReceipt, TransactionRequest}; +pub use transaction::request::TransactionRequest; +pub use transaction::response::{Transaction, TransactionReceipt}; mod address_or_bytes; pub use address_or_bytes::AddressOrBytes; diff --git a/ethers-core/src/types/transaction/eip2930.rs b/ethers-core/src/types/transaction/eip2930.rs new file mode 100644 index 00000000..ae0c7419 --- /dev/null +++ b/ethers-core/src/types/transaction/eip2930.rs @@ -0,0 +1,24 @@ +use crate::types::{Address, H256}; + +use rlp_derive::RlpEncodable; +use serde::{Deserialize, Serialize}; + +/// Access list +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, RlpEncodable)] +pub struct AccessList(pub Vec); + +impl From> for AccessList { + fn from(src: Vec) -> AccessList { + AccessList(src) + } +} + +/// Access list item +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, RlpEncodable)] +#[serde(rename_all = "camelCase")] +pub struct AccessListItem { + /// Accessed address + pub address: Address, + /// Accessed storage keys + pub storage_keys: Vec, +} diff --git a/ethers-core/src/types/transaction/mod.rs b/ethers-core/src/types/transaction/mod.rs new file mode 100644 index 00000000..22f5e8c8 --- /dev/null +++ b/ethers-core/src/types/transaction/mod.rs @@ -0,0 +1,21 @@ +pub mod request; +pub mod response; + +pub mod eip2930; + +pub(crate) const BASE_NUM_TX_FIELDS: usize = 9; + +// Number of tx fields before signing +#[cfg(not(feature = "celo"))] +pub(crate) const NUM_TX_FIELDS: usize = BASE_NUM_TX_FIELDS; +// Celo has 3 additional fields +#[cfg(feature = "celo")] +pub(crate) const NUM_TX_FIELDS: usize = BASE_NUM_TX_FIELDS + 3; + +pub(super) fn rlp_opt(rlp: &mut rlp::RlpStream, opt: Option) { + if let Some(ref inner) = opt { + rlp.append(inner); + } else { + rlp.append(&""); + } +} diff --git a/ethers-core/src/types/transaction.rs b/ethers-core/src/types/transaction/request.rs similarity index 50% rename from ethers-core/src/types/transaction.rs rename to ethers-core/src/types/transaction/request.rs index d32f56a6..7dd1e1e2 100644 --- a/ethers-core/src/types/transaction.rs +++ b/ethers-core/src/types/transaction/request.rs @@ -1,21 +1,13 @@ //! Transaction types +use super::{rlp_opt, NUM_TX_FIELDS}; use crate::{ - types::{Address, Bloom, Bytes, Log, NameOrAddress, Signature, H256, U256, U64}, + types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64}, utils::keccak256, }; use rlp::RlpStream; use serde::{Deserialize, Serialize}; -const BASE_NUM_TX_FIELDS: usize = 9; - -// Number of tx fields before signing -#[cfg(not(feature = "celo"))] -const NUM_TX_FIELDS: usize = BASE_NUM_TX_FIELDS; -// Celo has 3 additional fields -#[cfg(feature = "celo")] -const NUM_TX_FIELDS: usize = BASE_NUM_TX_FIELDS + 3; - /// Parameters for sending a transaction #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)] pub struct TransactionRequest { @@ -160,7 +152,7 @@ impl TransactionRequest { rlp.out().freeze().into() } - fn rlp_base(&self, rlp: &mut RlpStream) { + pub(crate) fn rlp_base(&self, rlp: &mut RlpStream) { rlp_opt(rlp, self.nonce); rlp_opt(rlp, self.gas_price); rlp_opt(rlp, self.gas); @@ -206,163 +198,6 @@ impl TransactionRequest { } } -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, Default)] -pub struct Transaction { - /// The transaction's hash - pub hash: H256, - - /// The transaction's nonce - pub nonce: U256, - - /// Block hash. None when pending. - #[serde(rename = "blockHash")] - #[serde(skip_serializing_if = "Option::is_none")] - pub block_hash: Option, - - /// Block number. None when pending. - #[serde(rename = "blockNumber")] - #[serde(skip_serializing_if = "Option::is_none")] - pub block_number: Option, - - /// Transaction Index. None when pending. - #[serde(rename = "transactionIndex")] - #[serde(skip_serializing_if = "Option::is_none")] - pub transaction_index: Option, - - /// Sender - pub from: Address, - - /// Recipient (None when contract creation) - #[serde(skip_serializing_if = "Option::is_none")] - pub to: Option
, - - /// Transfered value - pub value: U256, - - /// Gas Price - #[serde(rename = "gasPrice")] - pub gas_price: U256, - - /// Gas amount - pub gas: U256, - - /// Input data - pub input: Bytes, - - /// ECDSA recovery id - pub v: U64, - - /// ECDSA signature r - pub r: U256, - - /// ECDSA signature s - pub s: U256, - - ///////////////// Celo-specific transaction fields ///////////////// - /// The currency fees are paid in (None for native currency) - #[cfg(feature = "celo")] - #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] - #[serde(skip_serializing_if = "Option::is_none", rename = "feeCurrency")] - pub fee_currency: Option
, - - /// Gateway fee recipient (None for no gateway fee paid) - #[cfg(feature = "celo")] - #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] - #[serde( - skip_serializing_if = "Option::is_none", - rename = "gatewayFeeRecipient" - )] - pub gateway_fee_recipient: Option
, - - /// Gateway fee amount (None for no gateway fee paid) - #[cfg(feature = "celo")] - #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] - #[serde(skip_serializing_if = "Option::is_none", rename = "gatewayFee")] - pub gateway_fee: Option, -} - -impl Transaction { - // modifies the RLP stream with the Celo-specific information - // This is duplicated from TransactionRequest. Is there a good way to get rid - // of this code duplication? - #[cfg(feature = "celo")] - fn inject_celo_metadata(&self, rlp: &mut RlpStream) { - rlp_opt(rlp, self.fee_currency); - rlp_opt(rlp, self.gateway_fee_recipient); - rlp_opt(rlp, self.gateway_fee); - } - - pub fn hash(&self) -> H256 { - keccak256(&self.rlp().as_ref()).into() - } - - pub fn rlp(&self) -> Bytes { - let mut rlp = RlpStream::new(); - rlp.begin_list(NUM_TX_FIELDS); - rlp.append(&self.nonce); - rlp.append(&self.gas_price); - rlp.append(&self.gas); - - #[cfg(feature = "celo")] - self.inject_celo_metadata(&mut rlp); - - rlp_opt(&mut rlp, self.to); - rlp.append(&self.value); - rlp.append(&self.input.as_ref()); - rlp.append(&self.v); - rlp.append(&self.r); - rlp.append(&self.s); - - rlp.out().freeze().into() - } -} - -/// "Receipt" of an executed transaction: details of its execution. -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] -pub struct TransactionReceipt { - /// Transaction hash. - #[serde(rename = "transactionHash")] - pub transaction_hash: H256, - /// Index within the block. - #[serde(rename = "transactionIndex")] - pub transaction_index: U64, - /// Hash of the block this transaction was included within. - #[serde(rename = "blockHash")] - pub block_hash: Option, - /// Number of the block this transaction was included within. - #[serde(rename = "blockNumber")] - pub block_number: Option, - /// Cumulative gas used within the block after this was executed. - #[serde(rename = "cumulativeGasUsed")] - pub cumulative_gas_used: U256, - /// Gas used by this transaction alone. - /// - /// Gas used is `None` if the the client is running in light client mode. - #[serde(rename = "gasUsed")] - pub gas_used: Option, - /// Contract address created, or `None` if not a deployment. - #[serde(rename = "contractAddress")] - pub contract_address: Option
, - /// Logs generated within this transaction. - pub logs: Vec, - /// Status: either 1 (success) or 0 (failure). Only present after activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) - pub status: Option, - /// State root. Only present before activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) - pub root: Option, - /// Logs bloom - #[serde(rename = "logsBloom")] - pub logs_bloom: Bloom, -} - #[cfg(test)] #[cfg(not(feature = "celo"))] mod tests { @@ -385,47 +220,4 @@ mod tests { ) .unwrap(); } - - #[test] - fn decode_transaction_response() { - let _res: Transaction = serde_json::from_str( - r#"{ - "blockHash":"0x1d59ff54b1eb26b013ce3cb5fc9dab3705b415a67127a003c3e61eb445bb8df2", - "blockNumber":"0x5daf3b", - "from":"0xa7d9ddbe1f17865597fbd27ec712455208b6b76d", - "gas":"0xc350", - "gasPrice":"0x4a817c800", - "hash":"0x88df016429689c079f3b2f6ad39fa052532c56795b733da78a91ebe6a713944b", - "input":"0x68656c6c6f21", - "nonce":"0x15", - "to":"0xf02c1c8e6114b1dbe8937a39260b5b0a374432bb", - "transactionIndex":"0x41", - "value":"0xf3dbb76162000", - "v":"0x25", - "r":"0x1b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5fea", - "s":"0x4ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c" - }"#, - ) - .unwrap(); - - let _res: Transaction = serde_json::from_str( - r#"{ - "hash":"0xdd79ab0f996150aa3c9f135bbb9272cf0dedb830fafcbbf0c06020503565c44f", - "nonce":"0xe", - "blockHash":"0xef3fe1f532c3d8783a6257619bc123e9453aa8d6614e4cdb4cc8b9e1ed861404", - "blockNumber":"0xf", - "transactionIndex":"0x0", - "from":"0x1b67b03cdccfae10a2d80e52d3d026dbe2960ad0", - "to":"0x986ee0c8b91a58e490ee59718cca41056cf55f24", - "value":"0x2710", - "gas":"0x5208", - "gasPrice":"0x186a0", - "input":"0x", - "v":"0x25", - "r":"0x75188beb2f601bb8cf52ef89f92a6ba2bb7edcf8e3ccde90548cc99cbea30b1e", - "s":"0xc0559a540f16d031f3404d5df2bb258084eee56ed1193d8b534bb6affdb3c2c" - }"#, - ) - .unwrap(); - } } diff --git a/ethers-core/src/types/transaction/response.rs b/ethers-core/src/types/transaction/response.rs new file mode 100644 index 00000000..85222d48 --- /dev/null +++ b/ethers-core/src/types/transaction/response.rs @@ -0,0 +1,290 @@ +//! Transaction types +use super::{eip2930::AccessList, rlp_opt, NUM_TX_FIELDS}; +use crate::{ + types::{Address, Bloom, Bytes, Log, H256, U256, U64}, + utils::keccak256, +}; +use rlp::RlpStream; +use serde::{Deserialize, Serialize}; + +/// Details of a signed transaction +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct Transaction { + /// The transaction's hash + pub hash: H256, + + /// The transaction's nonce + pub nonce: U256, + + /// Block hash. None when pending. + #[serde(rename = "blockHash")] + #[serde(skip_serializing_if = "Option::is_none")] + pub block_hash: Option, + + /// Block number. None when pending. + #[serde(rename = "blockNumber")] + #[serde(skip_serializing_if = "Option::is_none")] + pub block_number: Option, + + /// Transaction Index. None when pending. + #[serde(rename = "transactionIndex")] + #[serde(skip_serializing_if = "Option::is_none")] + pub transaction_index: Option, + + /// Sender + pub from: Address, + + /// Recipient (None when contract creation) + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option
, + + /// Transfered value + pub value: U256, + + /// Gas Price + #[serde(rename = "gasPrice")] + // TODO: Should this be deprecated? + // https://twitter.com/TimBeiko/status/1413536062429794304 + // If yes, how will we handle pre-calculating the transaction's hash keccak256(rlp(tx)), + // given that it contains the gas price? + pub gas_price: U256, + + /// Gas amount + pub gas: U256, + + /// Input data + pub input: Bytes, + + /// ECDSA recovery id + pub v: U64, + + /// ECDSA signature r + pub r: U256, + + /// ECDSA signature s + pub s: U256, + + ///////////////// Celo-specific transaction fields ///////////////// + /// The currency fees are paid in (None for native currency) + #[cfg(feature = "celo")] + #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] + #[serde(skip_serializing_if = "Option::is_none", rename = "feeCurrency")] + pub fee_currency: Option
, + + /// Gateway fee recipient (None for no gateway fee paid) + #[cfg(feature = "celo")] + #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] + #[serde( + skip_serializing_if = "Option::is_none", + rename = "gatewayFeeRecipient" + )] + pub gateway_fee_recipient: Option
, + + /// Gateway fee amount (None for no gateway fee paid) + #[cfg(feature = "celo")] + #[cfg_attr(docsrs, doc(cfg(feature = "celo")))] + #[serde(skip_serializing_if = "Option::is_none", rename = "gatewayFee")] + pub gateway_fee: Option, + + // EIP2718 + /// Transaction type, Some(1) for AccessList transaction, None for Legacy + #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")] + pub transaction_type: Option, + + // EIP2930 + #[serde( + rename = "accessList", + default, + skip_serializing_if = "Option::is_none" + )] + pub access_list: Option, + + #[serde( + rename = "maxPriorityFeePerGas", + default, + skip_serializing_if = "Option::is_none" + )] + /// Represents the maximum tx fee that will go to the miner as part of the user's + /// fee payment. It serves 3 purposes: + /// 1. Compensates miners for the uncle/ommer risk + fixed costs of including transaction in a block; + /// 2. Allows users with high opportunity costs to pay a premium to miners; + /// 3. In times where demand exceeds the available block space (i.e. 100% full, 30mm gas), + /// this component allows first price auctions (i.e. the pre-1559 fee model) to happen on the priority fee. + /// + /// More context [here](https://hackmd.io/@q8X_WM2nTfu6nuvAzqXiTQ/1559-wallets) + pub max_priority_fee_per_gas: Option, + + #[serde( + rename = "maxFeePerGas", + default, + skip_serializing_if = "Option::is_none" + )] + /// Represents the maximum amount that a user is willing to pay for their tx (inclusive of baseFeePerGas and maxPriorityFeePerGas). + /// The difference between maxFeePerGas and baseFeePerGas + maxPriorityFeePerGas is “refunded” to the user. + pub max_fee_per_gas: Option, +} + +impl Transaction { + // modifies the RLP stream with the Celo-specific information + // This is duplicated from TransactionRequest. Is there a good way to get rid + // of this code duplication? + #[cfg(feature = "celo")] + fn inject_celo_metadata(&self, rlp: &mut RlpStream) { + rlp_opt(rlp, self.fee_currency); + rlp_opt(rlp, self.gateway_fee_recipient); + rlp_opt(rlp, self.gateway_fee); + } + + pub fn hash(&self) -> H256 { + keccak256(&self.rlp().as_ref()).into() + } + + pub fn rlp(&self) -> Bytes { + let mut rlp = RlpStream::new(); + rlp.begin_list(NUM_TX_FIELDS); + rlp.append(&self.nonce); + rlp.append(&self.gas_price); + rlp.append(&self.gas); + + #[cfg(feature = "celo")] + self.inject_celo_metadata(&mut rlp); + + rlp_opt(&mut rlp, self.to); + rlp.append(&self.value); + rlp.append(&self.input.as_ref()); + rlp.append(&self.v); + rlp.append(&self.r); + rlp.append(&self.s); + + rlp.out().freeze().into() + } +} + +/// "Receipt" of an executed transaction: details of its execution. +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +pub struct TransactionReceipt { + /// Transaction hash. + #[serde(rename = "transactionHash")] + pub transaction_hash: H256, + /// Index within the block. + #[serde(rename = "transactionIndex")] + pub transaction_index: U64, + /// Hash of the block this transaction was included within. + #[serde(rename = "blockHash")] + pub block_hash: Option, + /// Number of the block this transaction was included within. + #[serde(rename = "blockNumber")] + pub block_number: Option, + /// Cumulative gas used within the block after this was executed. + #[serde(rename = "cumulativeGasUsed")] + pub cumulative_gas_used: U256, + /// Gas used by this transaction alone. + /// + /// Gas used is `None` if the the client is running in light client mode. + #[serde(rename = "gasUsed")] + pub gas_used: Option, + /// Contract address created, or `None` if not a deployment. + #[serde(rename = "contractAddress")] + pub contract_address: Option
, + /// Logs generated within this transaction. + pub logs: Vec, + /// Status: either 1 (success) or 0 (failure). Only present after activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) + pub status: Option, + /// State root. Only present before activation of [EIP-658](https://eips.ethereum.org/EIPS/eip-658) + pub root: Option, + /// Logs bloom + #[serde(rename = "logsBloom")] + pub logs_bloom: Bloom, + /// Transaction type, Some(1) for AccessList transaction, None for Legacy + #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")] + pub transaction_type: Option, + /// The price paid post-execution by the transaction (i.e. base fee + priority fee). + /// Both fields in 1559-style transactions are *maximums* (max fee + max priority fee), the amount + /// that's actually paid by users can only be determined post-execution + #[serde( + rename = "effectiveGasPrice", + default, + skip_serializing_if = "Option::is_none" + )] + pub effective_gas_price: Option, +} + +#[cfg(test)] +#[cfg(not(feature = "celo"))] +mod tests { + use crate::types::transaction::eip2930::AccessListItem; + + use super::*; + + #[test] + fn decode_transaction_response() { + let _res: Transaction = serde_json::from_str( + r#"{ + "blockHash":"0x1d59ff54b1eb26b013ce3cb5fc9dab3705b415a67127a003c3e61eb445bb8df2", + "blockNumber":"0x5daf3b", + "from":"0xa7d9ddbe1f17865597fbd27ec712455208b6b76d", + "gas":"0xc350", + "gasPrice":"0x4a817c800", + "hash":"0x88df016429689c079f3b2f6ad39fa052532c56795b733da78a91ebe6a713944b", + "input":"0x68656c6c6f21", + "nonce":"0x15", + "to":"0xf02c1c8e6114b1dbe8937a39260b5b0a374432bb", + "transactionIndex":"0x41", + "value":"0xf3dbb76162000", + "v":"0x25", + "r":"0x1b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5fea", + "s":"0x4ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c" + }"#, + ) + .unwrap(); + + let _res: Transaction = serde_json::from_str( + r#"{ + "hash":"0xdd79ab0f996150aa3c9f135bbb9272cf0dedb830fafcbbf0c06020503565c44f", + "nonce":"0xe", + "blockHash":"0xef3fe1f532c3d8783a6257619bc123e9453aa8d6614e4cdb4cc8b9e1ed861404", + "blockNumber":"0xf", + "transactionIndex":"0x0", + "from":"0x1b67b03cdccfae10a2d80e52d3d026dbe2960ad0", + "to":"0x986ee0c8b91a58e490ee59718cca41056cf55f24", + "value":"0x2710", + "gas":"0x5208", + "gasPrice":"0x186a0", + "input":"0x", + "v":"0x25", + "r":"0x75188beb2f601bb8cf52ef89f92a6ba2bb7edcf8e3ccde90548cc99cbea30b1e", + "s":"0xc0559a540f16d031f3404d5df2bb258084eee56ed1193d8b534bb6affdb3c2c" + }"#, + ) + .unwrap(); + } + + #[test] + fn decode_london_receipt() { + let receipt: TransactionReceipt = serde_json::from_value(serde_json::json!({"blockHash":"0x55ae43d3511e327dc532855510d110676d340aa1bbba369b4b98896d86559586","blockNumber":"0xa3d322","contractAddress":null,"cumulativeGasUsed":"0x207a5b","effectiveGasPrice":"0x3b9aca07","from":"0x541d6a0e9ca9e7a083e41e2e178eef9f22d7492e","gasUsed":"0x6a40","logs":[],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","status":"0x1","to":"0x8210357f377e901f18e45294e86a2a32215cc3c9","transactionHash":"0x824384376c5972498c6fcafe71fd8cad1689f64e7d5e270d025a898638c0c34d","transactionIndex":"0xd","type":"0x2"})).unwrap(); + assert_eq!(receipt.transaction_type.unwrap().as_u64(), 2); + assert_eq!(receipt.effective_gas_price.unwrap().as_u64(), 0x3b9aca07); + } + + #[test] + fn decode_london_tx() { + let tx: Transaction = serde_json::from_value(serde_json::json!({"accessList":[{"address":"0x8ba1f109551bd432803012645ac136ddd64dba72","storageKeys":["0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000042"]}],"blockHash":"0x55ae43d3511e327dc532855510d110676d340aa1bbba369b4b98896d86559586","blockNumber":"0xa3d322","chainId":"0x3","from":"0x541d6a0e9ca9e7a083e41e2e178eef9f22d7492e","gas":"0x6a40","gasPrice":"0x3b9aca07","hash":"0x824384376c5972498c6fcafe71fd8cad1689f64e7d5e270d025a898638c0c34d","input":"0x","maxFeePerGas":"0x3b9aca0e","maxPriorityFeePerGas":"0x3b9aca00","nonce":"0x2","r":"0xf13b5088108f783f4b6048d4be456971118aabfb88be96bb541d734b6c2b20dc","s":"0x13fb7eb25a7d5df42a176cd4c6a086e19163ed7cd8ffba015f939d24f66bc17a","to":"0x8210357f377e901f18e45294e86a2a32215cc3c9","transactionIndex":"0xd","type":"0x2","v":"0x1","value":"0x7b"})).unwrap(); + assert_eq!(tx.transaction_type.unwrap().as_u64(), 2); + let lst = AccessList(vec![AccessListItem { + address: "0x8ba1f109551bd432803012645ac136ddd64dba72" + .parse() + .unwrap(), + storage_keys: vec![ + "0x0000000000000000000000000000000000000000000000000000000000000000" + .parse() + .unwrap(), + "0x0000000000000000000000000000000000000000000000000000000000000042" + .parse() + .unwrap(), + ], + }]); + assert_eq!(tx.access_list.unwrap(), lst); + assert_eq!(tx.max_fee_per_gas.unwrap().as_u64(), 0x3b9aca0e); + assert_eq!(tx.max_priority_fee_per_gas.unwrap().as_u64(), 0x3b9aca00); + } +} diff --git a/ethers-middleware/src/signer.rs b/ethers-middleware/src/signer.rs index 7bd5d272..a7333884 100644 --- a/ethers-middleware/src/signer.rs +++ b/ethers-middleware/src/signer.rs @@ -1,9 +1,4 @@ -use ethers_core::{ - types::{ - Address, BlockId, Bytes, NameOrAddress, Signature, Transaction, TransactionRequest, U256, - }, - utils::keccak256, -}; +use ethers_core::types::{Address, BlockId, Bytes, NameOrAddress, Signature, TransactionRequest}; use ethers_providers::{FromErr, Middleware, PendingTransaction}; use ethers_signers::Signer; @@ -120,64 +115,19 @@ where } } + /// Signs and returns the RLP encoding of the signed transaction async fn sign_transaction( &self, tx: TransactionRequest, - ) -> Result> { - // The nonce, gas and gasprice fields must already be populated - let nonce = tx.nonce.ok_or(SignerMiddlewareError::NonceMissing)?; - let gas_price = tx.gas_price.ok_or(SignerMiddlewareError::GasPriceMissing)?; - let gas = tx.gas.ok_or(SignerMiddlewareError::GasMissing)?; - - // Can't sign a transaction from a different address - if tx.from.is_some() && tx.from != Some(self.address()) { - return Err(SignerMiddlewareError::WrongSigner); - } - + ) -> Result> { let signature = self .signer .sign_transaction(&tx) .await .map_err(SignerMiddlewareError::SignerError)?; - // Get the actual transaction hash - let rlp = tx.rlp_signed(&signature); - let hash = keccak256(&rlp.as_ref()); - - // This function should not be called with ENS names - let to = tx.to.map(|to| match to { - NameOrAddress::Address(inner) => inner, - NameOrAddress::Name(_) => { - panic!("Expected `to` to be an Ethereum Address, not an ENS name") - } - }); - - Ok(Transaction { - hash: hash.into(), - nonce, - from: self.address(), - to, - 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()), - - // Leave these empty as they're only used for included transactions - block_hash: None, - block_number: None, - transaction_index: None, - - // Celo support - #[cfg(feature = "celo")] - fee_currency: tx.fee_currency, - #[cfg(feature = "celo")] - gateway_fee: tx.gateway_fee, - #[cfg(feature = "celo")] - gateway_fee_recipient: tx.gateway_fee_recipient, - }) + // Return the raw rlp-encoded signed transaction + Ok(tx.rlp_signed(&signature)) } async fn fill_transaction( @@ -282,7 +232,7 @@ where // Submit the raw transaction self.inner - .send_raw_transaction(&signed_tx) + .send_raw_transaction(signed_tx) .await .map_err(SignerMiddlewareError::MiddlewareError) } @@ -317,6 +267,7 @@ where mod tests { use super::*; use ethers::{providers::Provider, signers::LocalWallet}; + use ethers_core::utils::{self, keccak256, Ganache}; use std::convert::TryFrom; #[tokio::test] @@ -349,51 +300,60 @@ mod tests { let tx = client.sign_transaction(tx).await.unwrap(); assert_eq!( - tx.hash, - "de8db924885b0803d2edc335f745b2b8750c8848744905684c20b987443a9593" - .parse() + keccak256(&tx)[..], + hex::decode("de8db924885b0803d2edc335f745b2b8750c8848744905684c20b987443a9593") .unwrap() ); let expected_rlp = Bytes::from(hex::decode("f869808504e3b29200831e848094f0109fc8df283027b6285cc889f5aa624eac1f55843b9aca008025a0c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895a0727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa68").unwrap()); - assert_eq!(tx.rlp(), expected_rlp); + assert_eq!(tx, expected_rlp); } #[tokio::test] async fn handles_tx_from_field() { - use ethers_core::types::Address; - - // new SignerMiddleware - let provider = Provider::try_from("http://localhost:8545").unwrap(); + let ganache = Ganache::new().spawn(); + let acc = ganache.addresses()[0]; + let provider = Provider::try_from(ganache.endpoint()).unwrap(); let key = LocalWallet::new(&mut rand::thread_rng()).with_chain_id(1u32); + provider + .send_transaction( + TransactionRequest::pay(key.address(), utils::parse_ether(1u64).unwrap()).from(acc), + None, + ) + .await + .unwrap(); let client = SignerMiddleware::new(provider, key); - // an address that is not the signer address - let other = "0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4" - .parse::
() - .unwrap(); - - let request = TransactionRequest::new().nonce(0).gas_price(0).gas(0); + let request = TransactionRequest::new(); // signing a TransactionRequest with a from field of None should yield // a signed transaction from the signer address let request_from_none = request.clone(); - let signing_result = client.sign_transaction(request_from_none).await; - - assert_eq!(signing_result.unwrap().from, client.address()); + let hash = *client + .send_transaction(request_from_none, None) + .await + .unwrap(); + let tx = client.get_transaction(hash).await.unwrap().unwrap(); + assert_eq!(tx.from, client.address()); // signing a TransactionRequest with the signer as the from address // should yield a signed transaction from the signer let request_from_signer = request.clone().from(client.address()); - let signing_result = client.sign_transaction(request_from_signer.clone()).await; - - assert_eq!(signing_result.unwrap().from, client.address()); + let hash = *client + .send_transaction(request_from_signer, None) + .await + .unwrap(); + let tx = client.get_transaction(hash).await.unwrap().unwrap(); + assert_eq!(tx.from, client.address()); // signing a TransactionRequest with a from address that is not the - // signer should result in a WrongSigner error - let request_from_other = request.from(other); - let signing_result = client.sign_transaction(request_from_other.clone()).await; - - assert!(signing_result.is_err()); + // signer should result in the default ganache account being used + let request_from_other = request.from(acc); + let hash = *client + .send_transaction(request_from_other, None) + .await + .unwrap(); + let tx = client.get_transaction(hash).await.unwrap().unwrap(); + assert_eq!(tx.from, acc); } } diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs index 4fe63157..09f4817d 100644 --- a/ethers-providers/src/lib.rs +++ b/ethers-providers/src/lib.rs @@ -313,7 +313,7 @@ pub trait Middleware: Sync + Send + Debug { async fn send_raw_transaction<'a>( &'a self, - tx: &Transaction, + tx: Bytes, ) -> Result, Self::Error> { self.inner() .send_raw_transaction(tx) diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs index fe9a92d8..956216fd 100644 --- a/ethers-providers/src/provider.rs +++ b/ethers-providers/src/provider.rs @@ -354,9 +354,9 @@ impl Middleware for Provider

{ /// This will consume gas from the account that signed the transaction. async fn send_raw_transaction<'a>( &'a self, - tx: &Transaction, + tx: Bytes, ) -> Result, ProviderError> { - let rlp = utils::serialize(&tx.rlp()); + let rlp = utils::serialize(&tx); let tx_hash = self.request("eth_sendRawTransaction", [rlp]).await?; Ok(PendingTransaction::new(tx_hash, self).interval(self.get_interval())) }