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
82impl 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 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 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
122impl 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 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 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
181impl 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 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 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 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 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 _ => 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}