stratus/eth/primitives/
external_transaction.rs

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 v is 0x0 or 0x1, this is a type 2 (EIP-1559) transaction
37            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            // Check if this is a type 2 transaction
45            if let Some(Value::String(type_value)) = map.get("type")
46                && type_value == "0x2"
47            {
48                // For EIP-1559 transactions, ensure max_fee_per_gas and max_priority_fee_per_gas are present
49                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            // Check if this is a type 1 transaction
60            if let Some(Value::String(type_value)) = map.get("type")
61                && type_value == "0x1"
62            {
63                // For EIP-2930 transactions, ensure accessList is present
64                if !map.contains_key("accessList") {
65                    map.insert("accessList".to_string(), Value::Array(Vec::new()));
66                }
67            }
68        }
69
70        // Use the inner type's deserialization
71        let transaction = AlloyTransaction::deserialize(value).map_err(D::Error::custom)?;
72
73        Ok(ExternalTransaction(transaction))
74    }
75}
76
77impl ExternalTransaction {
78    /// Returns the block number where the transaction was mined.
79    pub fn block_number(&self) -> Result<BlockNumber> {
80        Ok(self.0.block_number.context("ExternalTransaction has no block_number")?.into())
81    }
82
83    /// Returns the transaction hash.
84    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
129// -----------------------------------------------------------------------------
130// Conversions: Other -> Self
131// -----------------------------------------------------------------------------
132impl From<AlloyTransaction> for ExternalTransaction {
133    fn from(value: AlloyTransaction) -> Self {
134        ExternalTransaction(value)
135    }
136}
137
138// -----------------------------------------------------------------------------
139// Tests
140// -----------------------------------------------------------------------------
141
142#[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            // accessList is missing
188        });
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            // maxFeePerGas, maxPriorityFeePerGas, and accessList are missing
211        });
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            // type field is missing, but v is 0x0 so it should be inferred as type 2
234        });
235
236        let tx: ExternalTransaction = serde_json::from_value(json).unwrap();
237
238        assert!(matches!(tx.0.inner.inner(), TxEnvelope::Eip1559(_)));
239
240        // Test with v = 0x1 as well
241        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            // type field is missing, but v is 0x1 so it should be inferred as type 2
255        });
256
257        let tx: ExternalTransaction = serde_json::from_value(json).unwrap();
258
259        assert!(matches!(tx.0.inner.inner(), TxEnvelope::Eip1559(_)));
260    }
261}