openzeppelin_monitor/models/blockchain/evm/
transaction.rs

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