openzeppelin_monitor/models/blockchain/evm/
transaction.rs

1//! EVM transaction data structures.
2
3use std::{collections::HashMap, ops::Deref};
4
5use serde::{Deserialize, Serialize};
6
7use alloy::{
8	consensus::Transaction as AlloyConsensusTransaction,
9	primitives::{Address, Bytes, B256, U256, U64},
10	rpc::types::{AccessList, Index, Transaction as AlloyTransaction},
11};
12
13/// L2-specific transaction fields
14#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
15pub struct BaseL2Transaction {
16	/// Deposit receipt version (for L2 transactions)
17	#[serde(
18		rename = "depositReceiptVersion",
19		default,
20		skip_serializing_if = "Option::is_none"
21	)]
22	pub deposit_receipt_version: Option<U64>,
23
24	/// Source hash (for L2 transactions)
25	#[serde(
26		rename = "sourceHash",
27		default,
28		skip_serializing_if = "Option::is_none"
29	)]
30	pub source_hash: Option<B256>,
31
32	/// Mint amount (for L2 transactions)
33	#[serde(default, skip_serializing_if = "Option::is_none")]
34	pub mint: Option<U256>,
35
36	/// Y parity (alternative to v in some implementations)
37	#[serde(rename = "yParity", default, skip_serializing_if = "Option::is_none")]
38	pub y_parity: Option<U64>,
39}
40
41/// Base Transaction struct
42/// Copied from web3 crate (now deprecated) and slightly modified for alloy compatibility
43#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
44pub struct BaseTransaction {
45	/// Hash
46	pub hash: B256,
47	/// Nonce
48	pub nonce: U256,
49	/// Block hash. None when pending.
50	#[serde(rename = "blockHash")]
51	pub block_hash: Option<B256>,
52	/// Block number. None when pending.
53	#[serde(rename = "blockNumber")]
54	pub block_number: Option<U64>,
55	/// Transaction Index. None when pending.
56	#[serde(rename = "transactionIndex")]
57	pub transaction_index: Option<Index>,
58	/// Sender
59	#[serde(default, skip_serializing_if = "Option::is_none")]
60	pub from: Option<Address>,
61	/// Recipient (None when contract creation)
62	pub to: Option<Address>,
63	/// Transferred value
64	pub value: U256,
65	/// Gas Price
66	#[serde(rename = "gasPrice")]
67	pub gas_price: Option<U256>,
68	/// Gas amount
69	pub gas: U256,
70	/// Input data
71	pub input: Bytes,
72	/// ECDSA recovery id
73	#[serde(default, skip_serializing_if = "Option::is_none")]
74	pub v: Option<U64>,
75	/// ECDSA signature r, 32 bytes
76	#[serde(default, skip_serializing_if = "Option::is_none")]
77	pub r: Option<U256>,
78	/// ECDSA signature s, 32 bytes
79	#[serde(default, skip_serializing_if = "Option::is_none")]
80	pub s: Option<U256>,
81	/// Raw transaction data
82	#[serde(default, skip_serializing_if = "Option::is_none")]
83	pub raw: Option<Bytes>,
84	/// Transaction type, Some(1) for AccessList transaction, None for Legacy
85	#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
86	pub transaction_type: Option<U64>,
87	/// Access list
88	#[serde(
89		rename = "accessList",
90		default,
91		skip_serializing_if = "Option::is_none"
92	)]
93	pub access_list: Option<AccessList>,
94	/// Max fee per gas
95	#[serde(rename = "maxFeePerGas", skip_serializing_if = "Option::is_none")]
96	pub max_fee_per_gas: Option<U256>,
97	/// miner bribe
98	#[serde(
99		rename = "maxPriorityFeePerGas",
100		skip_serializing_if = "Option::is_none"
101	)]
102	pub max_priority_fee_per_gas: Option<U256>,
103
104	/// L2-specific transaction fields
105	#[serde(flatten)]
106	pub l2: BaseL2Transaction,
107
108	/// Catch-all for non-standard fields
109	#[serde(flatten)]
110	pub extra: HashMap<String, serde_json::Value>,
111}
112
113/// Wrapper around Base Transaction that implements additional functionality
114///
115/// This type provides a convenient interface for working with EVM transactions
116/// while maintaining compatibility with the base types.
117#[derive(Debug, Clone, Serialize, Deserialize, Default)]
118pub struct Transaction(pub BaseTransaction);
119
120impl Transaction {
121	/// Get the transaction value (amount of ETH transferred)
122	pub fn value(&self) -> &U256 {
123		&self.0.value
124	}
125
126	/// Get the transaction sender address
127	pub fn sender(&self) -> Option<&Address> {
128		self.0.from.as_ref()
129	}
130
131	/// Get the transaction recipient address (None for contract creation)
132	pub fn to(&self) -> Option<&Address> {
133		self.0.to.as_ref()
134	}
135
136	/// Get the gas limit for the transaction
137	pub fn gas(&self) -> &U256 {
138		&self.0.gas
139	}
140
141	/// Get the gas price (None for EIP-1559 transactions)
142	pub fn gas_price(&self) -> Option<&U256> {
143		self.0.gas_price.as_ref()
144	}
145
146	/// Get the transaction nonce
147	pub fn nonce(&self) -> &U256 {
148		&self.0.nonce
149	}
150
151	/// Get the transaction hash
152	pub fn hash(&self) -> &B256 {
153		&self.0.hash
154	}
155}
156
157impl From<BaseTransaction> for Transaction {
158	fn from(tx: BaseTransaction) -> Self {
159		Self(tx)
160	}
161}
162
163impl From<AlloyTransaction> for Transaction {
164	fn from(tx: AlloyTransaction) -> Self {
165		let tx = BaseTransaction {
166			hash: *tx.inner.tx_hash(),
167			nonce: U256::from(tx.inner.nonce()),
168			block_hash: tx.block_hash,
169			block_number: tx.block_number.map(U64::from),
170			transaction_index: tx.transaction_index.map(|i| Index::from(i as usize)),
171			from: Some(tx.inner.signer()),
172			to: tx.inner.to(),
173			value: tx.inner.value(),
174			gas_price: tx.inner.gas_price().map(U256::from),
175			gas: U256::from(tx.inner.gas_limit()),
176			input: tx.inner.input().clone(),
177			v: Some(U64::from(u64::from(tx.inner.signature().v()))),
178			r: Some(U256::from(tx.inner.signature().r())),
179			s: Some(U256::from(tx.inner.signature().s())),
180			raw: None,
181			transaction_type: Some(U64::from(tx.inner.tx_type() as u64)),
182			access_list: tx.inner.access_list().cloned(),
183			max_fee_per_gas: Some(U256::from(tx.inner.max_fee_per_gas())),
184			max_priority_fee_per_gas: Some(U256::from(
185				tx.inner.max_priority_fee_per_gas().unwrap_or(0),
186			)),
187			l2: BaseL2Transaction {
188				deposit_receipt_version: None,
189				source_hash: None,
190				mint: None,
191				y_parity: None,
192			},
193			extra: HashMap::new(),
194		};
195		Self(tx)
196	}
197}
198
199impl Deref for Transaction {
200	type Target = BaseTransaction;
201
202	fn deref(&self) -> &Self::Target {
203		&self.0
204	}
205}
206
207#[cfg(test)]
208mod tests {
209	use super::*;
210	use crate::utils::tests::builders::evm::transaction::TransactionBuilder;
211	use alloy::primitives::{Address, B256, U256};
212
213	#[test]
214	fn test_value() {
215		let value = U256::from(100);
216		let tx = TransactionBuilder::new().value(value).build();
217		assert_eq!(*tx.value(), value);
218	}
219
220	#[test]
221	fn test_sender() {
222		let address = Address::with_last_byte(5);
223		let tx = TransactionBuilder::new().from(address).build();
224		assert_eq!(tx.sender(), Some(&address));
225	}
226
227	#[test]
228	fn test_recipient() {
229		let address = Address::with_last_byte(6);
230		let tx = TransactionBuilder::new().to(address).build();
231		assert_eq!(tx.to(), Some(&address));
232	}
233
234	#[test]
235	fn test_gas() {
236		let default_tx = TransactionBuilder::new().build(); // Default gas is 21000
237		assert_eq!(*default_tx.gas(), U256::from(21000));
238
239		// Set custom gas limit
240		let gas = U256::from(45000);
241		let tx = TransactionBuilder::new().gas_limit(gas).build();
242		assert_eq!(*tx.gas(), gas);
243	}
244
245	#[test]
246	fn test_gas_price() {
247		let gas_price = U256::from(20);
248		let tx = TransactionBuilder::new().gas_price(gas_price).build();
249		assert_eq!(tx.gas_price(), Some(&gas_price));
250	}
251
252	#[test]
253	fn test_nonce() {
254		let nonce = U256::from(2);
255		let tx = TransactionBuilder::new().nonce(nonce).build();
256		assert_eq!(*tx.nonce(), nonce);
257	}
258
259	#[test]
260	fn test_hash() {
261		let hash = B256::with_last_byte(1);
262		let tx = TransactionBuilder::new().hash(hash).build();
263		assert_eq!(*tx.hash(), hash);
264	}
265
266	#[test]
267	fn test_from_base_transaction() {
268		let base_tx = TransactionBuilder::new().build().0;
269		let tx: Transaction = base_tx.clone().into();
270		assert_eq!(tx.0, base_tx);
271	}
272
273	#[test]
274	fn test_deref() {
275		let base_tx = TransactionBuilder::new().build().0;
276		let tx = Transaction(base_tx.clone());
277		assert_eq!(*tx, base_tx);
278	}
279}