use crate::{AccessList, AccessListItem, Address, ChainId, Error, Result, Wei};
pub const TRANSFER_GAS: u64 = 21_000;
pub const TOKEN_TRANSFER_GAS: u64 = 65_000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Eip1559Transaction {
pub chain_id: ChainId,
pub nonce: u64,
pub max_priority_fee_per_gas: Wei,
pub max_fee_per_gas: Wei,
pub gas_limit: u64,
pub to: Option<Address>,
pub value: Wei,
pub data: Vec<u8>,
pub access_list: AccessList,
}
impl Eip1559Transaction {
pub const TYPE: u8 = 0x02;
pub fn builder() -> Eip1559TransactionBuilder {
Eip1559TransactionBuilder::new()
}
pub fn validate(&self) -> Result<()> {
if self.max_fee_per_gas < self.max_priority_fee_per_gas {
return Err(Error::ValidationError(
"max_fee_per_gas must be >= max_priority_fee_per_gas".to_string(),
));
}
if self.gas_limit < TRANSFER_GAS {
return Err(Error::InvalidGas(format!(
"gas_limit must be at least {}, got {}",
TRANSFER_GAS, self.gas_limit
)));
}
Ok(())
}
pub fn is_contract_creation(&self) -> bool {
self.to.is_none()
}
pub fn is_transfer(&self) -> bool {
self.to.is_some() && self.data.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct Eip1559TransactionBuilder {
chain_id: Option<ChainId>,
nonce: Option<u64>,
max_priority_fee_per_gas: Option<Wei>,
max_fee_per_gas: Option<Wei>,
gas_limit: Option<u64>,
to: Option<Address>,
value: Option<Wei>,
data: Vec<u8>,
access_list: AccessList,
}
impl Eip1559TransactionBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn chain_id(mut self, chain_id: ChainId) -> Self {
self.chain_id = Some(chain_id);
self
}
pub fn nonce(mut self, nonce: u64) -> Self {
self.nonce = Some(nonce);
self
}
pub fn max_priority_fee_per_gas(mut self, fee: Wei) -> Self {
self.max_priority_fee_per_gas = Some(fee);
self
}
pub fn max_fee_per_gas(mut self, fee: Wei) -> Self {
self.max_fee_per_gas = Some(fee);
self
}
pub fn gas_limit(mut self, limit: u64) -> Self {
self.gas_limit = Some(limit);
self
}
pub fn to(mut self, address: Address) -> Self {
self.to = Some(address);
self
}
pub fn value(mut self, value: Wei) -> Self {
self.value = Some(value);
self
}
pub fn data(mut self, data: Vec<u8>) -> Self {
self.data = data;
self
}
pub fn access_list(mut self, access_list: AccessList) -> Self {
self.access_list = access_list;
self
}
pub fn add_access_list_item(mut self, item: AccessListItem) -> Self {
self.access_list.push(item);
self
}
pub fn build(self) -> Result<Eip1559Transaction> {
let tx = Eip1559Transaction {
chain_id: self
.chain_id
.ok_or_else(|| Error::ValidationError("chain_id is required".to_string()))?,
nonce: self
.nonce
.ok_or_else(|| Error::ValidationError("nonce is required".to_string()))?,
max_priority_fee_per_gas: self.max_priority_fee_per_gas.ok_or_else(|| {
Error::ValidationError("max_priority_fee_per_gas is required".to_string())
})?,
max_fee_per_gas: self
.max_fee_per_gas
.ok_or_else(|| Error::ValidationError("max_fee_per_gas is required".to_string()))?,
gas_limit: self
.gas_limit
.ok_or_else(|| Error::ValidationError("gas_limit is required".to_string()))?,
to: self.to,
value: self.value.unwrap_or(Wei::ZERO),
data: self.data,
access_list: self.access_list,
};
tx.validate()?;
Ok(tx)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_address() -> Address {
"0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
.parse()
.unwrap()
}
#[test]
fn test_builder_minimal() {
let tx = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(21000)
.build()
.unwrap();
assert_eq!(tx.chain_id, ChainId::BscMainnet);
assert_eq!(tx.nonce, 0);
assert_eq!(tx.max_priority_fee_per_gas, Wei::from_gwei(1));
assert_eq!(tx.max_fee_per_gas, Wei::from_gwei(5));
assert_eq!(tx.gas_limit, 21000);
assert_eq!(tx.to, None);
assert_eq!(tx.value, Wei::ZERO);
assert!(tx.data.is_empty());
assert!(tx.access_list.is_empty());
}
#[test]
fn test_builder_full() {
let recipient = test_address();
let tx = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(5)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(21000)
.to(recipient)
.value(Wei::from_ether(1))
.data(vec![0x01, 0x02, 0x03])
.build()
.unwrap();
assert_eq!(tx.to, Some(recipient));
assert_eq!(tx.value, Wei::from_ether(1));
assert_eq!(tx.data, vec![0x01, 0x02, 0x03]);
}
#[test]
fn test_builder_with_access_list() {
let addr = test_address();
let item = AccessListItem::new(addr, vec![[1u8; 32]]);
let tx = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(21000)
.access_list(vec![item.clone()])
.build()
.unwrap();
assert_eq!(tx.access_list.len(), 1);
assert_eq!(tx.access_list[0], item);
}
#[test]
fn test_builder_add_access_list_item() {
let addr = test_address();
let item = AccessListItem::address_only(addr);
let tx = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(21000)
.add_access_list_item(item)
.build()
.unwrap();
assert_eq!(tx.access_list.len(), 1);
}
#[test]
fn test_builder_missing_chain_id() {
let result = Eip1559Transaction::builder()
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(21000)
.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("chain_id"));
}
#[test]
fn test_builder_missing_nonce() {
let result = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(21000)
.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("nonce"));
}
#[test]
fn test_builder_missing_max_priority_fee() {
let result = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(21000)
.build();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("max_priority_fee_per_gas"));
}
#[test]
fn test_builder_missing_max_fee() {
let result = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.gas_limit(21000)
.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("max_fee_per_gas"));
}
#[test]
fn test_builder_missing_gas_limit() {
let result = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("gas_limit"));
}
#[test]
fn test_validation_max_fee_less_than_priority_fee() {
let result = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(10))
.max_fee_per_gas(Wei::from_gwei(5)) .gas_limit(21000)
.build();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("max_fee_per_gas must be >= max_priority_fee_per_gas"));
}
#[test]
fn test_validation_gas_limit_too_low() {
let result = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(20000) .build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("gas_limit"));
}
#[test]
fn test_validation_equal_fees_ok() {
let result = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(5))
.max_fee_per_gas(Wei::from_gwei(5)) .gas_limit(21000)
.build();
assert!(result.is_ok());
}
#[test]
fn test_is_contract_creation() {
let tx = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(21000)
.build()
.unwrap();
assert!(tx.is_contract_creation());
assert!(!tx.is_transfer());
}
#[test]
fn test_is_transfer() {
let tx = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(21000)
.to(test_address())
.value(Wei::from_ether(1))
.build()
.unwrap();
assert!(!tx.is_contract_creation());
assert!(tx.is_transfer());
}
#[test]
fn test_is_contract_call() {
let tx = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(65000)
.to(test_address())
.data(vec![0xa9, 0x05, 0x9c, 0xbb]) .build()
.unwrap();
assert!(!tx.is_contract_creation());
assert!(!tx.is_transfer()); }
#[test]
fn test_transaction_type() {
assert_eq!(Eip1559Transaction::TYPE, 0x02);
}
#[test]
fn test_gas_constants() {
assert_eq!(TRANSFER_GAS, 21_000);
assert_eq!(TOKEN_TRANSFER_GAS, 65_000);
}
#[test]
fn test_transaction_clone() {
let tx = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(21000)
.to(test_address())
.value(Wei::from_ether(1))
.build()
.unwrap();
let cloned = tx.clone();
assert_eq!(tx, cloned);
}
#[test]
fn test_transaction_equality() {
let tx1 = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(21000)
.build()
.unwrap();
let tx2 = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(21000)
.build()
.unwrap();
let tx3 = Eip1559Transaction::builder()
.chain_id(ChainId::BscTestnet) .nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(21000)
.build()
.unwrap();
assert_eq!(tx1, tx2);
assert_ne!(tx1, tx3);
}
#[test]
fn test_bsc_mainnet_transaction() {
let tx = Eip1559Transaction::builder()
.chain_id(ChainId::BscMainnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(21000)
.to(test_address())
.value(Wei::from_ether(1))
.build()
.unwrap();
assert_eq!(u64::from(tx.chain_id), 56);
}
#[test]
fn test_bsc_testnet_transaction() {
let tx = Eip1559Transaction::builder()
.chain_id(ChainId::BscTestnet)
.nonce(0)
.max_priority_fee_per_gas(Wei::from_gwei(1))
.max_fee_per_gas(Wei::from_gwei(5))
.gas_limit(21000)
.to(test_address())
.value(Wei::from_ether(1))
.build()
.unwrap();
assert_eq!(u64::from(tx.chain_id), 97);
}
}