feat: 1559/2930 txs (part 2) (#355)
* feat: add eip2930 tx type * feat: add typed transaction enum * chore: rlp_opt take reference to option * feat: add eip-1559 tx type * feat: add eip-1559 to the typed tx enum * fix: references to rlp_opt / add access list setter * chore: ignore etherchain and ethgasstation Their APIs had a breaking change * fix(1559/2930): serialize properly 1. The AccessList struct should have used RlpEncodableWrapper, otherwise we get extra bytes 2. The 1559/2930 types do not use eip-155-style replay protection * feat: add helpers for operating on the txs enum * chore: cargo fmt / lints
This commit is contained in:
parent
09ff480f19
commit
ccff9fc98f
|
@ -2116,9 +2116,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rlp"
|
name = "rlp"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e54369147e3e7796c9b885c7304db87ca3d09a0a98f72843d532868675bbfba8"
|
checksum = "999508abb0ae792aabed2460c45b89106d97fe4adac593bdaef433c2605847b5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"rustc-hex",
|
"rustc-hex",
|
||||||
|
|
|
@ -7,9 +7,13 @@ pub use ethabi::ethereum_types::H256 as TxHash;
|
||||||
|
|
||||||
pub use ethabi::ethereum_types::{Address, Bloom, H160, H256, U128, U256, U64};
|
pub use ethabi::ethereum_types::{Address, Bloom, H160, H256, U128, U256, U64};
|
||||||
|
|
||||||
mod transaction;
|
pub mod transaction;
|
||||||
pub use transaction::request::TransactionRequest;
|
pub use transaction::{
|
||||||
pub use transaction::response::{Transaction, TransactionReceipt};
|
eip1559::Eip1559TransactionRequest,
|
||||||
|
eip2930::Eip2930TransactionRequest,
|
||||||
|
request::TransactionRequest,
|
||||||
|
response::{Transaction, TransactionReceipt},
|
||||||
|
};
|
||||||
|
|
||||||
mod address_or_bytes;
|
mod address_or_bytes;
|
||||||
pub use address_or_bytes::AddressOrBytes;
|
pub use address_or_bytes::AddressOrBytes;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
const NUM_EIP2930_FIELDS: usize = 8;
|
||||||
|
|
||||||
/// Access list
|
/// 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>);
|
pub struct AccessList(pub Vec<AccessListItem>);
|
||||||
|
|
||||||
impl From<Vec<AccessListItem>> for AccessList {
|
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
|
/// Access list item
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, RlpEncodable)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, RlpEncodable)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AccessListItem {
|
pub struct AccessListItem {
|
||||||
/// Accessed address
|
/// Accessed address
|
||||||
|
@ -22,3 +39,103 @@ pub struct AccessListItem {
|
||||||
/// Accessed storage keys
|
/// Accessed storage keys
|
||||||
pub storage_keys: Vec<H256>,
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
pub mod request;
|
pub mod request;
|
||||||
pub mod response;
|
pub mod response;
|
||||||
|
|
||||||
|
pub mod eip1559;
|
||||||
|
pub mod eip2718;
|
||||||
pub mod eip2930;
|
pub mod eip2930;
|
||||||
|
|
||||||
pub(crate) const BASE_NUM_TX_FIELDS: usize = 9;
|
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")]
|
#[cfg(feature = "celo")]
|
||||||
pub(crate) const NUM_TX_FIELDS: usize = BASE_NUM_TX_FIELDS + 3;
|
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>) {
|
pub(super) fn rlp_opt<T: rlp::Encodable>(rlp: &mut rlp::RlpStream, opt: &Option<T>) {
|
||||||
if let Some(ref inner) = opt {
|
if let Some(inner) = opt {
|
||||||
rlp.append(inner);
|
rlp.append(inner);
|
||||||
} else {
|
} else {
|
||||||
rlp.append(&"");
|
rlp.append(&"");
|
||||||
|
|
|
@ -153,16 +153,16 @@ impl TransactionRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) 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.nonce);
|
||||||
rlp_opt(rlp, self.gas_price);
|
rlp_opt(rlp, &self.gas_price);
|
||||||
rlp_opt(rlp, self.gas);
|
rlp_opt(rlp, &self.gas);
|
||||||
|
|
||||||
#[cfg(feature = "celo")]
|
#[cfg(feature = "celo")]
|
||||||
self.inject_celo_metadata(rlp);
|
self.inject_celo_metadata(rlp);
|
||||||
|
|
||||||
rlp_opt(rlp, self.to.as_ref());
|
rlp_opt(rlp, &self.to.as_ref());
|
||||||
rlp_opt(rlp, self.value);
|
rlp_opt(rlp, &self.value);
|
||||||
rlp_opt(rlp, self.data.as_ref().map(|d| d.as_ref()));
|
rlp_opt(rlp, &self.data.as_ref().map(|d| d.as_ref()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,9 +171,9 @@ impl TransactionRequest {
|
||||||
impl TransactionRequest {
|
impl TransactionRequest {
|
||||||
// modifies the RLP stream with the Celo-specific information
|
// modifies the RLP stream with the Celo-specific information
|
||||||
fn inject_celo_metadata(&self, rlp: &mut RlpStream) {
|
fn inject_celo_metadata(&self, rlp: &mut RlpStream) {
|
||||||
rlp_opt(rlp, self.fee_currency);
|
rlp_opt(rlp, &self.fee_currency);
|
||||||
rlp_opt(rlp, self.gateway_fee_recipient);
|
rlp_opt(rlp, &self.gateway_fee_recipient);
|
||||||
rlp_opt(rlp, self.gateway_fee);
|
rlp_opt(rlp, &self.gateway_fee);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the `fee_currency` field in the transaction to the provided value
|
/// Sets the `fee_currency` field in the transaction to the provided value
|
||||||
|
|
|
@ -87,7 +87,8 @@ pub struct Transaction {
|
||||||
pub gateway_fee: Option<U256>,
|
pub gateway_fee: Option<U256>,
|
||||||
|
|
||||||
// EIP2718
|
// 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")]
|
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
|
||||||
pub transaction_type: Option<U64>,
|
pub transaction_type: Option<U64>,
|
||||||
|
|
||||||
|
@ -130,9 +131,9 @@ impl Transaction {
|
||||||
// of this code duplication?
|
// of this code duplication?
|
||||||
#[cfg(feature = "celo")]
|
#[cfg(feature = "celo")]
|
||||||
fn inject_celo_metadata(&self, rlp: &mut RlpStream) {
|
fn inject_celo_metadata(&self, rlp: &mut RlpStream) {
|
||||||
rlp_opt(rlp, self.fee_currency);
|
rlp_opt(rlp, &self.fee_currency);
|
||||||
rlp_opt(rlp, self.gateway_fee_recipient);
|
rlp_opt(rlp, &self.gateway_fee_recipient);
|
||||||
rlp_opt(rlp, self.gateway_fee);
|
rlp_opt(rlp, &self.gateway_fee);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hash(&self) -> H256 {
|
pub fn hash(&self) -> H256 {
|
||||||
|
@ -149,7 +150,7 @@ impl Transaction {
|
||||||
#[cfg(feature = "celo")]
|
#[cfg(feature = "celo")]
|
||||||
self.inject_celo_metadata(&mut rlp);
|
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.value);
|
||||||
rlp.append(&self.input.as_ref());
|
rlp.append(&self.input.as_ref());
|
||||||
rlp.append(&self.v);
|
rlp.append(&self.v);
|
||||||
|
|
|
@ -15,7 +15,7 @@ async fn using_gas_oracle() {
|
||||||
let provider = Provider::<Http>::try_from(ganache.endpoint()).unwrap();
|
let provider = Provider::<Http>::try_from(ganache.endpoint()).unwrap();
|
||||||
|
|
||||||
// assign a gas oracle to use
|
// 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 expected_gas_price = gas_oracle.fetch().await.unwrap();
|
||||||
|
|
||||||
let provider = GasOracleMiddleware::new(provider, gas_oracle);
|
let provider = GasOracleMiddleware::new(provider, gas_oracle);
|
||||||
|
@ -32,6 +32,7 @@ async fn using_gas_oracle() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
async fn eth_gas_station() {
|
async fn eth_gas_station() {
|
||||||
// initialize and fetch gas estimates from EthGasStation
|
// initialize and fetch gas estimates from EthGasStation
|
||||||
let eth_gas_station_oracle = EthGasStation::new(None);
|
let eth_gas_station_oracle = EthGasStation::new(None);
|
||||||
|
@ -58,6 +59,7 @@ async fn etherscan() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
async fn etherchain() {
|
async fn etherchain() {
|
||||||
// initialize and fetch gas estimates from Etherchain
|
// initialize and fetch gas estimates from Etherchain
|
||||||
let etherchain_oracle = Etherchain::new().category(GasCategory::Fast);
|
let etherchain_oracle = Etherchain::new().category(GasCategory::Fast);
|
||||||
|
|
Loading…
Reference in New Issue