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