1#[cfg(test)]
2use alloy_consensus::Signed;
3#[cfg(test)]
4use alloy_consensus::TxEnvelope;
5#[cfg(test)]
6use alloy_consensus::TxLegacy;
7#[cfg(test)]
8use alloy_consensus::transaction::Recovered;
9#[cfg(test)]
10use alloy_primitives::Bytes;
11#[cfg(test)]
12use alloy_primitives::Signature;
13#[cfg(test)]
14use alloy_primitives::TxKind;
15#[cfg(test)]
16use alloy_primitives::U256;
17use anyhow::Context;
18use anyhow::Result;
19#[cfg(test)]
20use fake::Dummy;
21#[cfg(test)]
22use fake::Fake;
23#[cfg(test)]
24use fake::Faker;
25
26use crate::alias::AlloyTransaction;
27#[cfg(test)]
28use crate::eth::primitives::Address;
29use crate::eth::primitives::BlockNumber;
30use crate::eth::primitives::Hash;
31#[cfg(test)]
32use crate::eth::primitives::Wei;
33
34#[derive(Debug, Clone, PartialEq, derive_more::Deref, serde::Serialize)]
35#[serde(transparent)]
36pub struct ExternalTransaction(#[deref] pub AlloyTransaction);
37
38impl<'de> serde::Deserialize<'de> for ExternalTransaction {
39 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
40 where
41 D: serde::Deserializer<'de>,
42 {
43 use serde::de::Error;
44 use serde_json::Value;
45
46 let mut value = Value::deserialize(deserializer)?;
47
48 if let Value::Object(ref mut map) = value {
49 if let Some(Value::String(v_value)) = map.get("v")
51 && (v_value == "0x0" || v_value == "0x1")
52 && !map.contains_key("type")
53 {
54 map.insert("type".to_string(), Value::String("0x2".to_string()));
55 }
56
57 if let Some(Value::String(type_value)) = map.get("type")
59 && type_value == "0x2"
60 {
61 let gas_price = map.get("gasPrice").cloned().unwrap_or(Value::String("0x0".to_string()));
62 if !map.contains_key("maxFeePerGas") {
64 map.insert("maxFeePerGas".to_string(), gas_price.clone());
65 }
66 if !map.contains_key("maxPriorityFeePerGas") {
67 map.insert("maxPriorityFeePerGas".to_string(), gas_price);
68 }
69 if !map.contains_key("accessList") {
70 map.insert("accessList".to_string(), Value::Array(Vec::new()));
71 }
72 }
73 if let Some(Value::String(type_value)) = map.get("type")
75 && type_value == "0x1"
76 {
77 if !map.contains_key("accessList") {
79 map.insert("accessList".to_string(), Value::Array(Vec::new()));
80 }
81 }
82 }
83
84 let transaction = AlloyTransaction::deserialize(value).map_err(D::Error::custom)?;
86
87 Ok(ExternalTransaction(transaction))
88 }
89}
90
91impl ExternalTransaction {
92 pub fn block_number(&self) -> Result<BlockNumber> {
94 Ok(self.0.block_number.context("ExternalTransaction has no block_number")?.into())
95 }
96
97 pub fn hash(&self) -> Hash {
99 Hash::from(*self.0.inner.tx_hash())
100 }
101}
102
103#[cfg(test)]
104impl Dummy<Faker> for ExternalTransaction {
105 fn dummy_with_rng<R: rand::Rng + ?Sized>(faker: &Faker, rng: &mut R) -> Self {
106 let from: Address = faker.fake_with_rng(rng);
107 let to: Address = faker.fake_with_rng(rng);
108
109 let block_hash: Hash = faker.fake_with_rng(rng);
110
111 let gas_price: u128 = faker.fake_with_rng(rng);
112 let value: Wei = Wei::from(rng.next_u64());
113
114 let tx = TxLegacy {
115 chain_id: Some(1),
116 nonce: rng.next_u64(),
117 gas_price,
118 gas_limit: rng.next_u64(),
119 to: TxKind::Call(from.into()),
120 value: value.into(),
121 input: Bytes::default(),
122 };
123
124 let r = U256::from(rng.next_u64());
125 let s = U256::from(rng.next_u64());
126 let v = rng.next_u64() % 2 == 0;
127 let signature = Signature::new(r, s, v);
128
129 let hash: Hash = faker.fake_with_rng(rng);
130 let inner_tx = TxEnvelope::Legacy(Signed::new_unchecked(tx, signature, hash.into()));
131
132 let inner = alloy_rpc_types_eth::Transaction {
133 inner: Recovered::new_unchecked(inner_tx, to.into()),
134 block_hash: Some(block_hash.into()),
135 block_number: Some(rng.next_u64()),
136 transaction_index: Some(rng.next_u64()),
137 effective_gas_price: Some(gas_price),
138 };
139
140 ExternalTransaction(inner)
141 }
142}
143
144impl From<AlloyTransaction> for ExternalTransaction {
148 fn from(value: AlloyTransaction) -> Self {
149 ExternalTransaction(value)
150 }
151}
152
153#[cfg(test)]
158mod tests {
159 use serde_json::json;
160
161 use super::*;
162
163 #[test]
164 fn test_deserialize_type0_transaction() {
165 let json = json!({
166 "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
167 "type": "0x0",
168 "from": "0x1234567890123456789012345678901234567890",
169 "to": "0x0987654321098765432109876543210987654321",
170 "gas": "0x76c0",
171 "gasPrice": "0x9184e72a000",
172 "nonce": "0x1",
173 "value": "0x9184e72a",
174 "input": "0x",
175 "chainId": "0x1",
176 "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
177 "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
178 "v": "0x1b"
179 });
180
181 let tx: ExternalTransaction = serde_json::from_value(json).unwrap();
182
183 assert!(matches!(tx.0.inner.inner(), TxEnvelope::Legacy(_)));
184 }
185
186 #[test]
187 fn test_deserialize_type1_transaction_with_missing_access_list() {
188 let json = json!({
189 "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
190 "type": "0x1",
191 "from": "0x1234567890123456789012345678901234567890",
192 "to": "0x0987654321098765432109876543210987654321",
193 "gas": "0x76c0",
194 "gasPrice": "0x9184e72a000",
195 "nonce": "0x1",
196 "value": "0x9184e72a",
197 "input": "0x",
198 "chainId": "0x1",
199 "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
200 "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
201 "v": "0x0"
202 });
204
205 let tx: ExternalTransaction = serde_json::from_value(json).unwrap();
206
207 assert!(matches!(tx.0.inner.inner(), TxEnvelope::Eip2930(_)));
208 }
209
210 #[test]
211 fn test_deserialize_type2_transaction_with_missing_fields() {
212 let json = json!({
213 "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
214 "type": "0x2",
215 "from": "0x1234567890123456789012345678901234567890",
216 "to": "0x0987654321098765432109876543210987654321",
217 "gas": "0x76c0",
218 "nonce": "0x1",
219 "value": "0x9184e72a",
220 "input": "0x",
221 "chainId": "0x1",
222 "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
223 "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
224 "v": "0x1"
225 });
227
228 let tx: ExternalTransaction = serde_json::from_value(json).unwrap();
229
230 assert!(matches!(tx.0.inner.inner(), TxEnvelope::Eip1559(_)));
231 }
232
233 #[test]
234 fn test_deserialize_type2_inferred_from_v_value() {
235 let json = json!({
236 "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
237 "from": "0x1234567890123456789012345678901234567890",
238 "to": "0x0987654321098765432109876543210987654321",
239 "gas": "0x76c0",
240 "gasPrice": "0x9184e72a000",
241 "nonce": "0x1",
242 "value": "0x9184e72a",
243 "input": "0x",
244 "chainId": "0x1",
245 "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
246 "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
247 "v": "0x0"
248 });
250
251 let tx: ExternalTransaction = serde_json::from_value(json).unwrap();
252
253 assert!(matches!(tx.0.inner.inner(), TxEnvelope::Eip1559(_)));
254
255 let json = json!({
257 "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
258 "from": "0x1234567890123456789012345678901234567890",
259 "to": "0x0987654321098765432109876543210987654321",
260 "gas": "0x76c0",
261 "gasPrice": "0x9184e72a000",
262 "nonce": "0x1",
263 "value": "0x9184e72a",
264 "input": "0x",
265 "chainId": "0x1",
266 "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
267 "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
268 "v": "0x1"
269 });
271
272 let tx: ExternalTransaction = serde_json::from_value(json).unwrap();
273
274 assert!(matches!(tx.0.inner.inner(), TxEnvelope::Eip1559(_)));
275 }
276}