Repository: gakonst/ethers-rs
Pull request #355 (2021-07-31 14:13:33)

feat: 1559/2930 txs (part 2)
* Introduces the eip1559/2930 tx types 
* Introduces a `TypedTransaction` enum w/ `From` implementations for each type, which will replace `TransactionRequest` in the rest of the codebase
* Fixes a bug pointed out by @guanqun on the `AccessList` RLP encoding derivation method
* Adds tests to ensure that RLP encoding and the sighash matches Geth's

guanqun: Thank you for the feature.

My only comment is the naming. These EIP numbers are just magic numbers, we should avoid using magic numbers.

How do you think? @gakonst @mattsse 
gakonst: @guanqun replied above, let's stick with the EIP numbers, they're numbers that everyone in Ethereum knows about (EIP-1559 is very specific about what it gives, vs "Dynamic Fee" can mean literally anything)
guanqun: Thanks for the info.

I agree that EIP-1559 is popular, but from my own view, other two EIP-2930 and EIP-2718 are less popular. I doubt that people would immediately know the meaning behind them.

That said, you have the final call.
guanqun: FYI. A few small changes are needed to make it work on goerli network. The biggest one missing here is the leading header byte. I hack around and come up with a patch here, if it's useful to others. https://github.com/guanqun/ethers-rs/commit/68de2a26680d7e14a6eca858ebdac50430a64103

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 9d31619f..7e98c6b2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2116,9 +2116,9 @@ dependencies = [
 
 [[package]]
 name = "rlp"
-version = "0.5.0"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e54369147e3e7796c9b885c7304db87ca3d09a0a98f72843d532868675bbfba8"
+checksum = "999508abb0ae792aabed2460c45b89106d97fe4adac593bdaef433c2605847b5"
 dependencies = [
  "bytes",
  "rustc-hex",
diff --git a/ethers-core/src/types/mod.rs b/ethers-core/src/types/mod.rs
index 597b7cb6..2cf9d5fc 100644
--- a/ethers-core/src/types/mod.rs
+++ b/ethers-core/src/types/mod.rs
@@ -7,9 +7,13 @@ 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::request::TransactionRequest;
-pub use transaction::response::{Transaction, TransactionReceipt};
+pub mod transaction;
+pub use transaction::{
+    eip1559::Eip1559TransactionRequest,
+    eip2930::Eip2930TransactionRequest,
+    request::TransactionRequest,
+    response::{Transaction, TransactionReceipt},
+};
 
 mod address_or_bytes;
 pub use address_or_bytes::AddressOrBytes;
diff --git a/ethers-core/src/types/transaction/eip1559.rs b/ethers-core/src/types/transaction/eip1559.rs
new file mode 100644
index 00000000..29d39da2
--- /dev/null
+++ b/ethers-core/src/types/transaction/eip1559.rs
@@ -0,0 +1,172 @@
+use super::{eip2930::AccessList, rlp_opt};
+use crate::{
+    types::{Address, Bytes, NameOrAddress, Signature, H256, U256, U64},
+    utils::keccak256,
+};
+use rlp::RlpStream;
+
+/// EIP-1559 transactions have 9 fields
+const NUM_TX_FIELDS: usize = 9;
+
+use serde::{Deserialize, Serialize};
+/// Parameters for sending a transaction
+#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)]
+pub struct Eip1559TransactionRequest {
+    /// Sender address or ENS name
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub from: Option<Address>,
+
+    /// Recipient address (None for contract creation)
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub to: Option<NameOrAddress>,
+
+    /// Supplied gas (None for sensible default)
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub gas: Option<U256>,
+
+    /// Transfered value (None for no transfer)
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub value: Option<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
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub data: Option<Bytes>,
+
+    /// Transaction nonce (None for next available nonce)
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub nonce: Option<U256>,
+
+    #[serde(
+        rename = "accessList",
+        default,
+        skip_serializing_if = "Option::is_none"
+    )]
+    pub access_list: Option<AccessList>,
+
+    #[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<U256>,
+
+    #[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<U256>,
+}
+
+impl Eip1559TransactionRequest {
+    /// Creates an empty transaction request with all fields left empty
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    // 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 to<T: Into<NameOrAddress>>(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 `max_priority_fee_per_gas` field in the transaction to the provided value
+    pub fn max_priority_fee_per_gas<T: Into<U256>>(mut self, max_priority_fee_per_gas: T) -> Self {
+        self.max_priority_fee_per_gas = Some(max_priority_fee_per_gas.into());
+        self
+    }
+
+    /// Sets the `max_fee_per_gas` field in the transaction to the provided value
+    pub fn max_fee_per_gas<T: Into<U256>>(mut self, max_fee_per_gas: T) -> Self {
+        self.max_fee_per_gas = Some(max_fee_per_gas.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 `access_list` field in the transaction to the provided value
+    pub fn access_list<T: Into<AccessList>>(mut self, access_list: T) -> Self {
+        self.access_list = Some(access_list.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 sighash<T: Into<U64>>(&self, chain_id: T) -> H256 {
+        keccak256(self.rlp(chain_id).as_ref()).into()
+    }
+
+    /// Gets the unsigned transaction's RLP encoding
+    pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes {
+        let mut rlp = RlpStream::new();
+        rlp.begin_list(NUM_TX_FIELDS);
+        self.rlp_base(chain_id, &mut rlp);
+        rlp.out().freeze().into()
+    }
+
+    /// Produces the RLP encoding of the transaction with the provided signature
+    pub fn rlp_signed<T: Into<U64>>(&self, chain_id: T, signature: &Signature) -> Bytes {
+        let mut rlp = RlpStream::new();
+        rlp.begin_unbounded_list();
+        self.rlp_base(chain_id, &mut rlp);
+
+        // append the signature
+        rlp.append(&signature.v);
+        rlp.append(&signature.r);
+        rlp.append(&signature.s);
+        rlp.finalize_unbounded_list();
+        rlp.out().freeze().into()
+    }
+
+    pub(crate) fn rlp_base<T: Into<U64>>(&self, chain_id: T, rlp: &mut RlpStream) {
+        rlp.append(&chain_id.into());
+        rlp_opt(rlp, &self.nonce);
+        rlp_opt(rlp, &self.max_priority_fee_per_gas);
+        rlp_opt(rlp, &self.max_fee_per_gas);
+        rlp_opt(rlp, &self.gas);
+        rlp_opt(rlp, &self.to.as_ref());
+        rlp_opt(rlp, &self.value);
+        rlp_opt(rlp, &self.data.as_ref().map(|d| d.as_ref()));
+        rlp_opt(rlp, &self.access_list);
+    }
+}
diff --git a/ethers-core/src/types/transaction/eip2718.rs b/ethers-core/src/types/transaction/eip2718.rs
new file mode 100644
index 00000000..fd23e178
--- /dev/null
+++ b/ethers-core/src/types/transaction/eip2718.rs
@@ -0,0 +1,237 @@
+use super::{eip1559::Eip1559TransactionRequest, eip2930::Eip2930TransactionRequest};
+use crate::{
+    types::{Address, Bytes, NameOrAddress, Signature, TransactionRequest, H256, U256, U64},
+    utils::keccak256,
+};
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
+#[serde(tag = "type")]
+pub enum TypedTransaction {
+    // 0x00
+    #[serde(rename = "0x00")]
+    Legacy(TransactionRequest),
+    // 0x01
+    #[serde(rename = "0x01")]
+    Eip2930(Eip2930TransactionRequest),
+    // 0x02
+    #[serde(rename = "0x02")]
+    Eip1559(Eip1559TransactionRequest),
+}
+
+impl TypedTransaction {
+    pub fn from(&self) -> Option<&Address> {
+        use TypedTransaction::*;
+        match self {
+            Legacy(inner) => inner.from.as_ref(),
+            Eip2930(inner) => inner.tx.from.as_ref(),
+            Eip1559(inner) => inner.from.as_ref(),
+        }
+    }
+
+    pub fn set_from(&mut self, from: Address) {
+        use TypedTransaction::*;
+        match self {
+            Legacy(inner) => inner.from = Some(from),
+            Eip2930(inner) => inner.tx.from = Some(from),
+            Eip1559(inner) => inner.from = Some(from),
+        };
+    }
+
+    pub fn to(&self) -> Option<&NameOrAddress> {
+        use TypedTransaction::*;
+        match self {
+            Legacy(inner) => inner.to.as_ref(),
+            Eip2930(inner) => inner.tx.to.as_ref(),
+            Eip1559(inner) => inner.to.as_ref(),
+        }
+    }
+
+    pub fn set_to<T: Into<NameOrAddress>>(&mut self, to: T) {
+        let to = to.into();
+        use TypedTransaction::*;
+        match self {
+            Legacy(inner) => inner.to = Some(to),
+            Eip2930(inner) => inner.tx.to = Some(to),
+            Eip1559(inner) => inner.to = Some(to),
+        };
+    }
+
+    pub fn nonce(&self) -> Option<&U256> {
+        use TypedTransaction::*;
+        match self {
+            Legacy(inner) => inner.nonce.as_ref(),
+            Eip2930(inner) => inner.tx.nonce.as_ref(),
+            Eip1559(inner) => inner.nonce.as_ref(),
+        }
+    }
+
+    pub fn set_nonce<T: Into<U256>>(&mut self, nonce: T) {
+        let nonce = nonce.into();
+        use TypedTransaction::*;
+        match self {
+            Legacy(inner) => inner.nonce = Some(nonce),
+            Eip2930(inner) => inner.tx.nonce = Some(nonce),
+            Eip1559(inner) => inner.nonce = Some(nonce),
+        };
+    }
+
+    pub fn value(&self) -> Option<&U256> {
+        use TypedTransaction::*;
+        match self {
+            Legacy(inner) => inner.value.as_ref(),
+            Eip2930(inner) => inner.tx.value.as_ref(),
+            Eip1559(inner) => inner.value.as_ref(),
+        }
+    }
+
+    pub fn set_value<T: Into<U256>>(&mut self, value: T) {
+        let value = value.into();
+        use TypedTransaction::*;
+        match self {
+            Legacy(inner) => inner.value = Some(value),
+            Eip2930(inner) => inner.tx.value = Some(value),
+            Eip1559(inner) => inner.value = Some(value),
+        };
+    }
+
+    pub fn gas(&self) -> Option<&U256> {
+        use TypedTransaction::*;
+        match self {
+            Legacy(inner) => inner.gas.as_ref(),
+            Eip2930(inner) => inner.tx.gas.as_ref(),
+            Eip1559(inner) => inner.gas.as_ref(),
+        }
+    }
+
+    pub fn set_gas<T: Into<U256>>(&mut self, gas: T) {
+        let gas = gas.into();
+        use TypedTransaction::*;
+        match self {
+            Legacy(inner) => inner.gas = Some(gas),
+            Eip2930(inner) => inner.tx.gas = Some(gas),
+            Eip1559(inner) => inner.gas = Some(gas),
+        };
+    }
+
+    pub fn set_gas_price<T: Into<U256>>(&mut self, gas_price: T) {
+        let gas_price = gas_price.into();
+        use TypedTransaction::*;
+        match self {
+            Legacy(inner) => inner.gas_price = Some(gas_price),
+            Eip2930(inner) => inner.tx.gas_price = Some(gas_price),
+            Eip1559(inner) => {
+                inner.max_fee_per_gas = Some(gas_price);
+                inner.max_priority_fee_per_gas = Some(gas_price);
+            }
+        };
+    }
+
+    pub fn data(&self) -> Option<&Bytes> {
+        use TypedTransaction::*;
+        match self {
+            Legacy(inner) => inner.data.as_ref(),
+            Eip2930(inner) => inner.tx.data.as_ref(),
+            Eip1559(inner) => inner.data.as_ref(),
+        }
+    }
+
+    pub fn set_data(&mut self, data: Bytes) {
+        use TypedTransaction::*;
+        match self {
+            Legacy(inner) => inner.data = Some(data),
+            Eip2930(inner) => inner.tx.data = Some(data),
+            Eip1559(inner) => inner.data = Some(data),
+        };
+    }
+
+    pub fn rlp_signed<T: Into<U64>>(&self, chain_id: T, signature: &Signature) -> Bytes {
+        use TypedTransaction::*;
+        let mut encoded = vec![];
+        match self {
+            Legacy(inner) => {
+                encoded.extend_from_slice(&[0x0]);
+                encoded.extend_from_slice(inner.rlp_signed(signature).as_ref());
+            }
+            Eip2930(inner) => {
+                encoded.extend_from_slice(&[0x1]);
+                encoded.extend_from_slice(inner.rlp_signed(chain_id, signature).as_ref());
+            }
+            Eip1559(inner) => {
+                encoded.extend_from_slice(&[0x2]);
+                encoded.extend_from_slice(inner.rlp_signed(chain_id, signature).as_ref());
+            }
+        };
+
+        rlp::encode(&encoded).freeze().into()
+    }
+
+    pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes {
+        let chain_id = chain_id.into();
+        let mut encoded = vec![];
+        use TypedTransaction::*;
+        match self {
+            Legacy(inner) => {
+                encoded.extend_from_slice(&[0x0]);
+                encoded.extend_from_slice(inner.rlp(chain_id).as_ref());
+            }
+            Eip2930(inner) => {
+                encoded.extend_from_slice(&[0x1]);
+                encoded.extend_from_slice(inner.rlp(chain_id).as_ref());
+            }
+            Eip1559(inner) => {
+                encoded.extend_from_slice(&[0x2]);
+                encoded.extend_from_slice(inner.rlp(chain_id).as_ref());
+            }
+        };
+
+        encoded.into()
+    }
+
+    /// Hashes the transaction's data with the provided chain id
+    /// Does not double-RLP encode
+    pub fn sighash<T: Into<U64>>(&self, chain_id: T) -> H256 {
+        let encoded = self.rlp(chain_id);
+        keccak256(encoded).into()
+    }
+}
+
+impl From<TransactionRequest> for TypedTransaction {
+    fn from(src: TransactionRequest) -> TypedTransaction {
+        TypedTransaction::Legacy(src)
+    }
+}
+
+impl From<Eip2930TransactionRequest> for TypedTransaction {
+    fn from(src: Eip2930TransactionRequest) -> TypedTransaction {
+        TypedTransaction::Eip2930(src)
+    }
+}
+
+impl From<Eip1559TransactionRequest> for TypedTransaction {
+    fn from(src: Eip1559TransactionRequest) -> TypedTransaction {
+        TypedTransaction::Eip1559(src)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::types::{Address, U256};
+
+    #[test]
+    fn serde_legacy_tx() {
+        let tx = TransactionRequest::new()
+            .to(Address::zero())
+            .value(U256::from(100));
+        let tx: TypedTransaction = tx.into();
+        let serialized = serde_json::to_string(&tx).unwrap();
+
+        // deserializes to either the envelope type or the inner type
+        let de: TypedTransaction = serde_json::from_str(&serialized).unwrap();
+        assert_eq!(tx, de);
+
+        let de: TransactionRequest = serde_json::from_str(&serialized).unwrap();
+        assert_eq!(tx, TypedTransaction::Legacy(de));
+    }
+}
diff --git a/ethers-core/src/types/transaction/eip2930.rs b/ethers-core/src/types/transaction/eip2930.rs
index ae0c7419..b8577d69 100644
--- a/ethers-core/src/types/transaction/eip2930.rs
+++ b/ethers-core/src/types/transaction/eip2930.rs
@@ -1,10 +1,16 @@
-use crate::types::{Address, H256};
+use super::request::TransactionRequest;
+use crate::types::{Address, Bytes, Signature, H256, U64};
 
-use rlp_derive::RlpEncodable;
+use rlp::RlpStream;
+use rlp_derive::{RlpEncodable, RlpEncodableWrapper};
 use serde::{Deserialize, Serialize};
 
+const NUM_EIP2930_FIELDS: usize = 8;
+
 /// Access list
-#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, RlpEncodable)]
+// NB: Need to use `RlpEncodableWrapper` else we get an extra [] in the output
+// https://github.com/gakonst/ethers-rs/pull/353#discussion_r680683869
+#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, RlpEncodableWrapper)]
 pub struct AccessList(pub Vec<AccessListItem>);
 
 impl From<Vec<AccessListItem>> for AccessList {
@@ -13,8 +19,19 @@ impl From<Vec<AccessListItem>> for AccessList {
     }
 }
 
+impl TransactionRequest {
+    /// Sets the `access_list` field in the transaction (converts the [`TransactionRequest`] to
+    /// an [`Eip2930TransactionRequest`])
+    pub fn with_access_list<T: Into<AccessList>>(
+        self,
+        access_list: T,
+    ) -> Eip2930TransactionRequest {
+        Eip2930TransactionRequest::new(self, access_list.into())
+    }
+}
+
 /// Access list item
-#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, RlpEncodable)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, RlpEncodable)]
 #[serde(rename_all = "camelCase")]
 pub struct AccessListItem {
     /// Accessed address
@@ -22,3 +39,103 @@ pub struct AccessListItem {
     /// Accessed storage keys
     pub storage_keys: Vec<H256>,
 }
+
+/// An EIP-2930 transaction is a legacy transaction including an [`AccessList`].
+#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
+pub struct Eip2930TransactionRequest {
+    #[serde(flatten)]
+    pub tx: TransactionRequest,
+    pub access_list: AccessList,
+}
+
+impl Eip2930TransactionRequest {
+    pub fn new(tx: TransactionRequest, access_list: AccessList) -> Self {
+        Self { tx, access_list }
+    }
+
+    pub fn rlp<T: Into<U64>>(&self, chain_id: T) -> Bytes {
+        let mut rlp = RlpStream::new();
+        rlp.begin_list(NUM_EIP2930_FIELDS);
+        rlp.append(&chain_id.into());
+        self.tx.rlp_base(&mut rlp);
+        // append the access list in addition to the base rlp encoding
+        rlp.append(&self.access_list);
+
+        rlp.out().freeze().into()
+    }
+
+    /// Produces the RLP encoding of the transaction with the provided signature
+    pub fn rlp_signed<T: Into<U64>>(&self, chain_id: T, signature: &Signature) -> Bytes {
+        let mut rlp = RlpStream::new();
+        rlp.begin_list(NUM_EIP2930_FIELDS + 3);
+
+        rlp.append(&chain_id.into());
+        self.tx.rlp_base(&mut rlp);
+        // append the access list in addition to the base rlp encoding
+        rlp.append(&self.access_list);
+
+        // append the signature
+        rlp.append(&signature.v);
+        rlp.append(&signature.r);
+        rlp.append(&signature.s);
+        rlp.out().freeze().into()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::types::{transaction::eip2718::TypedTransaction, U256};
+
+    #[test]
+    // https://github.com/ethereum/go-ethereum/blob/c503f98f6d5e80e079c1d8a3601d188af2a899da/core/types/transaction_test.go#L59-L67
+    fn rlp() {
+        let tx: TypedTransaction = TransactionRequest::new()
+            .nonce(3)
+            .gas_price(1)
+            .gas(25000)
+            .to("b94f5374fce5edbc8e2a8697c15331677e6ebf0b"
+                .parse::<Address>()
+                .unwrap())
+            .value(10)
+            .data(vec![0x55, 0x44])
+            .with_access_list(vec![])
+            .into();
+
+        let hash = tx.sighash(1);
+        let sig: Signature = "c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b266032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d3752101".parse().unwrap();
+        let enc = tx.rlp_signed(1, &sig);
+
+        assert_eq!(
+            hash,
+            "49b486f0ec0a60dfbbca2d30cb07c9e8ffb2a2ff41f29a1ab6737475f6ff69f3"
+                .parse()
+                .unwrap()
+        );
+
+        let expected = "b86601f8630103018261a894b94f5374fce5edbc8e2a8697c15331677e6ebf0b0a825544c001a0c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b2660a032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d37521";
+        assert_eq!(hex::encode(enc.to_vec()), expected);
+    }
+
+    #[test]
+    fn serde_eip2930_tx() {
+        let access_list = vec![AccessListItem {
+            address: Address::zero(),
+            storage_keys: vec![H256::zero()],
+        }];
+        let tx = TransactionRequest::new()
+            .to(Address::zero())
+            .value(U256::from(100))
+            .with_access_list(access_list);
+        let tx: TypedTransaction = tx.into();
+        let serialized = serde_json::to_string(&tx).unwrap();
+        dbg!(&serialized);
+
+        // deserializes to either the envelope type or the inner type
+        let de: TypedTransaction = serde_json::from_str(&serialized).unwrap();
+        assert_eq!(tx, de);
+
+        let de: Eip2930TransactionRequest = serde_json::from_str(&serialized).unwrap();
+        assert_eq!(tx, TypedTransaction::Eip2930(de));
+    }
+}
diff --git a/ethers-core/src/types/transaction/mod.rs b/ethers-core/src/types/transaction/mod.rs
index 22f5e8c8..5e95d089 100644
--- a/ethers-core/src/types/transaction/mod.rs
+++ b/ethers-core/src/types/transaction/mod.rs
@@ -1,6 +1,8 @@
 pub mod request;
 pub mod response;
 
+pub mod eip1559;
+pub mod eip2718;
 pub mod eip2930;
 
 pub(crate) const BASE_NUM_TX_FIELDS: usize = 9;
@@ -12,8 +14,8 @@ pub(crate) const NUM_TX_FIELDS: usize = BASE_NUM_TX_FIELDS;
 #[cfg(feature = "celo")]
 pub(crate) const NUM_TX_FIELDS: usize = BASE_NUM_TX_FIELDS + 3;
 
-pub(super) fn rlp_opt<T: rlp::Encodable>(rlp: &mut rlp::RlpStream, opt: Option<T>) {
-    if let Some(ref inner) = opt {
+pub(super) fn rlp_opt<T: rlp::Encodable>(rlp: &mut rlp::RlpStream, opt: &Option<T>) {
+    if let Some(inner) = opt {
         rlp.append(inner);
     } else {
         rlp.append(&"");
diff --git a/ethers-core/src/types/transaction/request.rs b/ethers-core/src/types/transaction/request.rs
index 7dd1e1e2..e71f5fc9 100644
--- a/ethers-core/src/types/transaction/request.rs
+++ b/ethers-core/src/types/transaction/request.rs
@@ -153,16 +153,16 @@ impl TransactionRequest {
     }
 
     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);
+        rlp_opt(rlp, &self.nonce);
+        rlp_opt(rlp, &self.gas_price);
+        rlp_opt(rlp, &self.gas);
 
         #[cfg(feature = "celo")]
         self.inject_celo_metadata(rlp);
 
-        rlp_opt(rlp, self.to.as_ref());
-        rlp_opt(rlp, self.value);
-        rlp_opt(rlp, self.data.as_ref().map(|d| d.as_ref()));
+        rlp_opt(rlp, &self.to.as_ref());
+        rlp_opt(rlp, &self.value);
+        rlp_opt(rlp, &self.data.as_ref().map(|d| d.as_ref()));
     }
 }
 
@@ -171,9 +171,9 @@ impl TransactionRequest {
 impl TransactionRequest {
     // modifies the RLP stream with the Celo-specific information
     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);
+        rlp_opt(rlp, &self.fee_currency);
+        rlp_opt(rlp, &self.gateway_fee_recipient);
+        rlp_opt(rlp, &self.gateway_fee);
     }
 
     /// Sets the `fee_currency` field in the transaction to the provided value
diff --git a/ethers-core/src/types/transaction/response.rs b/ethers-core/src/types/transaction/response.rs
index 85222d48..9c427acd 100644
--- a/ethers-core/src/types/transaction/response.rs
+++ b/ethers-core/src/types/transaction/response.rs
@@ -87,7 +87,8 @@ pub struct Transaction {
     pub gateway_fee: Option<U256>,
 
     // EIP2718
-    /// Transaction type, Some(1) for AccessList transaction, None for Legacy
+    /// Transaction type, Some(2) for EIP-1559 transaction,
+    /// Some(1) for AccessList transaction, None for Legacy
     #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
     pub transaction_type: Option<U64>,
 
@@ -130,9 +131,9 @@ impl Transaction {
     // 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);
+        rlp_opt(rlp, &self.fee_currency);
+        rlp_opt(rlp, &self.gateway_fee_recipient);
+        rlp_opt(rlp, &self.gateway_fee);
     }
 
     pub fn hash(&self) -> H256 {
@@ -149,7 +150,7 @@ impl Transaction {
         #[cfg(feature = "celo")]
         self.inject_celo_metadata(&mut rlp);
 
-        rlp_opt(&mut rlp, self.to);
+        rlp_opt(&mut rlp, &self.to);
         rlp.append(&self.value);
         rlp.append(&self.input.as_ref());
         rlp.append(&self.v);


Test patch:
diff --git a/ethers-middleware/tests/gas_oracle.rs b/ethers-middleware/tests/gas_oracle.rs
index 8366abfd..ae79f461 100644
--- a/ethers-middleware/tests/gas_oracle.rs
+++ b/ethers-middleware/tests/gas_oracle.rs
@@ -15,7 +15,7 @@ async fn using_gas_oracle() {
     let provider = Provider::<Http>::try_from(ganache.endpoint()).unwrap();
 
     // assign a gas oracle to use
-    let gas_oracle = Etherchain::new().category(GasCategory::Fastest);
+    let gas_oracle = GasNow::new().category(GasCategory::Fastest);
     let expected_gas_price = gas_oracle.fetch().await.unwrap();
 
     let provider = GasOracleMiddleware::new(provider, gas_oracle);
@@ -32,6 +32,7 @@ async fn using_gas_oracle() {
 }
 
 #[tokio::test]
+#[ignore]
 async fn eth_gas_station() {
     // initialize and fetch gas estimates from EthGasStation
     let eth_gas_station_oracle = EthGasStation::new(None);
@@ -58,6 +59,7 @@ async fn etherscan() {
 }
 
 #[tokio::test]
+#[ignore]
 async fn etherchain() {
     // initialize and fetch gas estimates from Etherchain
     let etherchain_oracle = Etherchain::new().category(GasCategory::Fast);