stratus/eth/primitives/
transaction_input.rs

1use alloy_consensus::Signed;
2use alloy_consensus::Transaction;
3use alloy_consensus::TxEip1559;
4use alloy_consensus::TxEip2930;
5use alloy_consensus::TxEip4844;
6use alloy_consensus::TxEip4844Variant;
7use alloy_consensus::TxEip7702;
8use alloy_consensus::TxEnvelope;
9use alloy_consensus::TxLegacy;
10use alloy_consensus::transaction::Recovered;
11use alloy_consensus::transaction::SignerRecoverable;
12use alloy_eips::eip2718::Decodable2718;
13use alloy_primitives::Signature as AlloySignature;
14use alloy_primitives::TxKind;
15use alloy_primitives::U64;
16use alloy_primitives::U256;
17use alloy_rpc_types_eth::AccessList;
18use anyhow::bail;
19use display_json::DebugAsJson;
20use rlp::Decodable;
21
22use crate::alias::AlloyTransaction;
23use crate::eth::primitives::Address;
24use crate::eth::primitives::Bytes;
25use crate::eth::primitives::ChainId;
26use crate::eth::primitives::ExternalTransaction;
27use crate::eth::primitives::Gas;
28use crate::eth::primitives::Hash;
29use crate::eth::primitives::Nonce;
30use crate::eth::primitives::Wei;
31use crate::eth::primitives::signature_component::SignatureComponent;
32use crate::ext::RuintExt;
33
34#[derive(DebugAsJson, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
35#[cfg_attr(test, derive(fake::Dummy))]
36pub struct TransactionInfo {
37    #[cfg_attr(test, dummy(expr = "crate::utils::test_utils::fake_option_uint()"))]
38    pub tx_type: Option<U64>,
39    pub hash: Hash,
40}
41
42#[derive(DebugAsJson, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
43#[cfg_attr(test, derive(fake::Dummy))]
44pub struct ExecutionInfo {
45    #[cfg_attr(test, dummy(expr = "crate::utils::test_utils::fake_option::<ChainId>()"))]
46    pub chain_id: Option<ChainId>,
47    pub nonce: Nonce,
48    pub signer: Address,
49    #[cfg_attr(test, dummy(expr = "crate::utils::test_utils::fake_option::<Address>()"))]
50    pub to: Option<Address>,
51    pub value: Wei,
52    pub input: Bytes,
53    pub gas_limit: Gas,
54    pub gas_price: u128,
55}
56
57#[derive(DebugAsJson, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
58#[cfg_attr(test, derive(fake::Dummy))]
59pub struct Signature {
60    #[cfg_attr(test, dummy(expr = "crate::utils::test_utils::fake_uint()"))]
61    pub v: U64,
62    #[cfg_attr(test, dummy(expr = "crate::utils::test_utils::fake_uint()"))]
63    pub r: U256,
64    #[cfg_attr(test, dummy(expr = "crate::utils::test_utils::fake_uint()"))]
65    pub s: U256,
66}
67
68impl From<Signature> for AlloySignature {
69    fn from(value: Signature) -> Self {
70        AlloySignature::new(SignatureComponent(value.r).into(), SignatureComponent(value.s).into(), value.v == U64::ONE)
71    }
72}
73
74#[derive(DebugAsJson, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
75#[cfg_attr(test, derive(fake::Dummy))]
76pub struct TransactionInput {
77    pub transaction_info: TransactionInfo,
78    pub execution_info: ExecutionInfo,
79    pub signature: Signature,
80}
81
82// -----------------------------------------------------------------------------
83// Serialization / Deserialization
84// -----------------------------------------------------------------------------
85
86impl Decodable for TransactionInput {
87    fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
88        fn convert_tx(envelope: TxEnvelope) -> Result<TransactionInput, rlp::DecoderError> {
89            TransactionInput::try_from(alloy_rpc_types_eth::Transaction {
90                inner: envelope.try_into_recovered().map_err(|_| rlp::DecoderError::Custom("signature error"))?,
91                block_hash: None,
92                block_number: None,
93                transaction_index: None,
94                effective_gas_price: None,
95            })
96            .map_err(|_| rlp::DecoderError::Custom("failed to convert transaction"))
97        }
98
99        let raw_bytes = rlp.as_raw();
100
101        if raw_bytes.is_empty() {
102            return Err(rlp::DecoderError::Custom("empty transaction bytes"));
103        }
104
105        if rlp.is_list() {
106            // Legacy transaction
107            let mut bytes = raw_bytes;
108            TxEnvelope::fallback_decode(&mut bytes)
109                .map_err(|_| rlp::DecoderError::Custom("failed to decode legacy transaction"))
110                .and_then(convert_tx)
111        } else {
112            // Typed transaction (EIP-2718)
113            let first_byte = raw_bytes[0];
114            let mut remaining_bytes = &raw_bytes[1..];
115            TxEnvelope::typed_decode(first_byte, &mut remaining_bytes)
116                .map_err(|_| rlp::DecoderError::Custom("failed to decode transaction envelope"))
117                .and_then(convert_tx)
118        }
119    }
120}
121
122// -----------------------------------------------------------------------------
123// Conversion: Other -> Self
124// -----------------------------------------------------------------------------
125impl TryFrom<ExternalTransaction> for TransactionInput {
126    type Error = anyhow::Error;
127
128    fn try_from(value: ExternalTransaction) -> anyhow::Result<Self> {
129        try_from_alloy_transaction(value.0)
130    }
131}
132
133impl TryFrom<AlloyTransaction> for TransactionInput {
134    type Error = anyhow::Error;
135
136    fn try_from(value: AlloyTransaction) -> anyhow::Result<Self> {
137        try_from_alloy_transaction(value)
138    }
139}
140
141fn try_from_alloy_transaction(value: alloy_rpc_types_eth::Transaction) -> anyhow::Result<TransactionInput> {
142    // extract signer
143    let signer: Address = match value.inner.recover_signer() {
144        Ok(signer) => Address::from(signer),
145        Err(e) => {
146            tracing::warn!(reason = ?e, "failed to recover transaction signer");
147            bail!("Transaction signer cannot be recovered. Check the transaction signature is valid.");
148        }
149    };
150
151    // Get signature components from the envelope
152    let signature = value.inner.signature();
153    let signature = Signature {
154        r: signature.r(),
155        s: signature.s(),
156        v: if signature.v() { U64::ONE } else { U64::ZERO },
157    };
158
159    Ok(TransactionInput {
160        transaction_info: TransactionInfo {
161            tx_type: Some(U64::from(value.inner.tx_type() as u8)),
162            hash: Hash::from(*value.inner.tx_hash()),
163        },
164        execution_info: ExecutionInfo {
165            chain_id: value.inner.chain_id().map(Into::into),
166            nonce: Nonce::from(value.inner.nonce()),
167            signer,
168            to: match value.inner.kind() {
169                TxKind::Call(addr) => Some(Address::from(addr)),
170                TxKind::Create => None,
171            },
172            value: Wei::from(value.inner.value()),
173            input: Bytes::from(value.inner.input().clone()),
174            gas_limit: Gas::from(value.inner.gas_limit()),
175            gas_price: value.inner.max_fee_per_gas(),
176        },
177        signature,
178    })
179}
180
181// -----------------------------------------------------------------------------
182// Conversions: Self -> Other
183// -----------------------------------------------------------------------------
184
185impl From<TransactionInput> for AlloyTransaction {
186    fn from(value: TransactionInput) -> Self {
187        let signature = value.signature.into();
188
189        let tx_type = value.transaction_info.tx_type.map(|t| t.as_u64()).unwrap_or(0);
190
191        let inner = match tx_type {
192            // EIP-2930
193            1 => TxEnvelope::Eip2930(Signed::new_unchecked(
194                TxEip2930 {
195                    chain_id: value.execution_info.chain_id.unwrap_or_default().into(),
196                    nonce: value.execution_info.nonce.into(),
197                    gas_price: value.execution_info.gas_price,
198                    gas_limit: value.execution_info.gas_limit.into(),
199                    to: TxKind::from(value.execution_info.to.map(Into::into)),
200                    value: value.execution_info.value.into(),
201                    input: value.execution_info.input.clone().into(),
202                    access_list: AccessList::default(),
203                },
204                signature,
205                value.transaction_info.hash.into(),
206            )),
207
208            // EIP-1559
209            2 => TxEnvelope::Eip1559(Signed::new_unchecked(
210                TxEip1559 {
211                    chain_id: value.execution_info.chain_id.unwrap_or_default().into(),
212                    nonce: value.execution_info.nonce.into(),
213                    max_fee_per_gas: value.execution_info.gas_price,
214                    max_priority_fee_per_gas: value.execution_info.gas_price,
215                    gas_limit: value.execution_info.gas_limit.into(),
216                    to: TxKind::from(value.execution_info.to.map(Into::into)),
217                    value: value.execution_info.value.into(),
218                    input: value.execution_info.input.clone().into(),
219                    access_list: AccessList::default(),
220                },
221                signature,
222                value.transaction_info.hash.into(),
223            )),
224
225            // EIP-4844
226            3 => TxEnvelope::Eip4844(Signed::new_unchecked(
227                TxEip4844Variant::TxEip4844(TxEip4844 {
228                    chain_id: value.execution_info.chain_id.unwrap_or_default().into(),
229                    nonce: value.execution_info.nonce.into(),
230                    max_fee_per_gas: value.execution_info.gas_price,
231                    max_priority_fee_per_gas: value.execution_info.gas_price,
232                    gas_limit: value.execution_info.gas_limit.into(),
233                    to: value.execution_info.to.map(Into::into).unwrap_or_default(),
234                    value: value.execution_info.value.into(),
235                    input: value.execution_info.input.clone().into(),
236                    access_list: AccessList::default(),
237                    blob_versioned_hashes: Vec::new(),
238                    max_fee_per_blob_gas: 0u64.into(),
239                }),
240                signature,
241                value.transaction_info.hash.into(),
242            )),
243
244            // EIP-7702
245            4 => TxEnvelope::Eip7702(Signed::new_unchecked(
246                TxEip7702 {
247                    chain_id: value.execution_info.chain_id.unwrap_or_default().into(),
248                    nonce: value.execution_info.nonce.into(),
249                    gas_limit: value.execution_info.gas_limit.into(),
250                    max_fee_per_gas: value.execution_info.gas_price,
251                    max_priority_fee_per_gas: value.execution_info.gas_price,
252                    to: value.execution_info.to.map(Into::into).unwrap_or_default(),
253                    value: value.execution_info.value.into(),
254                    input: value.execution_info.input.clone().into(),
255                    access_list: AccessList::default(),
256                    authorization_list: Vec::new(),
257                },
258                signature,
259                value.transaction_info.hash.into(),
260            )),
261
262            // Legacy (default)
263            _ => TxEnvelope::Legacy(Signed::new_unchecked(
264                TxLegacy {
265                    chain_id: value.execution_info.chain_id.map(Into::into),
266                    nonce: value.execution_info.nonce.into(),
267                    gas_price: value.execution_info.gas_price,
268                    gas_limit: value.execution_info.gas_limit.into(),
269                    to: TxKind::from(value.execution_info.to.map(Into::into)),
270                    value: value.execution_info.value.into(),
271                    input: value.execution_info.input.clone().into(),
272                },
273                signature,
274                value.transaction_info.hash.into(),
275            )),
276        };
277
278        Self {
279            inner: Recovered::new_unchecked(inner, value.execution_info.signer.into()),
280            block_hash: None,
281            block_number: None,
282            transaction_index: None,
283            effective_gas_price: Some(value.execution_info.gas_price),
284        }
285    }
286}