stratus/eth/primitives/
external_transaction.rs

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 v is 0x0 or 0x1, this is a type 2 (EIP-1559) transaction
50            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            // Check if this is a type 2 transaction
58            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                // For EIP-1559 transactions, ensure max_fee_per_gas and max_priority_fee_per_gas are present
63                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            // Check if this is a type 1 transaction
74            if let Some(Value::String(type_value)) = map.get("type")
75                && type_value == "0x1"
76            {
77                // For EIP-2930 transactions, ensure accessList is present
78                if !map.contains_key("accessList") {
79                    map.insert("accessList".to_string(), Value::Array(Vec::new()));
80                }
81            }
82        }
83
84        // Use the inner type's deserialization
85        let transaction = AlloyTransaction::deserialize(value).map_err(D::Error::custom)?;
86
87        Ok(ExternalTransaction(transaction))
88    }
89}
90
91impl ExternalTransaction {
92    /// Returns the block number where the transaction was mined.
93    pub fn block_number(&self) -> Result<BlockNumber> {
94        Ok(self.0.block_number.context("ExternalTransaction has no block_number")?.into())
95    }
96
97    /// Returns the transaction hash.
98    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
144// -----------------------------------------------------------------------------
145// Conversions: Other -> Self
146// -----------------------------------------------------------------------------
147impl From<AlloyTransaction> for ExternalTransaction {
148    fn from(value: AlloyTransaction) -> Self {
149        ExternalTransaction(value)
150    }
151}
152
153// -----------------------------------------------------------------------------
154// Tests
155// -----------------------------------------------------------------------------
156
157#[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            // accessList is missing
203        });
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            // maxFeePerGas, maxPriorityFeePerGas, and accessList are missing
226        });
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            // type field is missing, but v is 0x0 so it should be inferred as type 2
249        });
250
251        let tx: ExternalTransaction = serde_json::from_value(json).unwrap();
252
253        assert!(matches!(tx.0.inner.inner(), TxEnvelope::Eip1559(_)));
254
255        // Test with v = 0x1 as well
256        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            // type field is missing, but v is 0x1 so it should be inferred as type 2
270        });
271
272        let tx: ExternalTransaction = serde_json::from_value(json).unwrap();
273
274        assert!(matches!(tx.0.inner.inner(), TxEnvelope::Eip1559(_)));
275    }
276}