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]]
|
||||
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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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};
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(&"");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue