Skip to main content

chains_sdk/ethereum/
transaction.rs

1//! Ethereum transaction types with RLP encoding and signing.
2//!
3//! Supports Legacy (pre-EIP-2718), Type 1 (EIP-2930), and Type 2 (EIP-1559) transactions.
4//! Each transaction type can be built, serialized, signed, and exported as raw hex for broadcasting.
5//!
6//! # Example
7//! ```no_run
8//! use chains_sdk::ethereum::transaction::EIP1559Transaction;
9//! use chains_sdk::ethereum::EthereumSigner;
10//! use chains_sdk::traits::KeyPair;
11//!
12//! fn example() -> Result<(), Box<dyn std::error::Error>> {
13//!     let signer = EthereumSigner::generate()?;
14//!     let tx = EIP1559Transaction {
15//!         chain_id: 1,
16//!         nonce: 0,
17//!         max_priority_fee_per_gas: 2_000_000_000, // 2 Gwei
18//!         max_fee_per_gas: 100_000_000_000,        // 100 Gwei
19//!         gas_limit: 21_000,
20//!         to: Some([0xAA; 20]),
21//!         value: 1_000_000_000_000_000_000,        // 1 ETH
22//!         data: vec![],
23//!         access_list: vec![],
24//!     };
25//!     let signed = tx.sign(&signer)?;
26//!     println!("Raw tx: 0x{}", hex::encode(&signed.raw_tx()));
27//!     println!("Tx hash: 0x{}", hex::encode(signed.tx_hash()));
28//!     Ok(())
29//! }
30//! ```
31
32use super::rlp;
33use super::EthereumSigner;
34use crate::error::SignerError;
35use sha3::{Digest, Keccak256};
36
37// ─── Signed Transaction ────────────────────────────────────────────
38
39/// A signed Ethereum transaction ready for broadcast.
40#[derive(Debug, Clone)]
41pub struct SignedTransaction {
42    /// The raw signed transaction bytes (for `eth_sendRawTransaction`).
43    raw: Vec<u8>,
44}
45
46impl SignedTransaction {
47    /// Return the raw signed transaction bytes.
48    ///
49    /// This is what you pass to `eth_sendRawTransaction`.
50    #[must_use]
51    pub fn raw_tx(&self) -> &[u8] {
52        &self.raw
53    }
54
55    /// Compute the transaction hash (keccak256 of the raw signed tx).
56    #[must_use]
57    pub fn tx_hash(&self) -> [u8; 32] {
58        let mut out = [0u8; 32];
59        out.copy_from_slice(&Keccak256::digest(&self.raw));
60        out
61    }
62
63    /// Return the raw transaction as a `0x`-prefixed hex string.
64    #[must_use]
65    pub fn raw_tx_hex(&self) -> String {
66        format!("0x{}", hex::encode(&self.raw))
67    }
68}
69
70// ─── Legacy Transaction (pre-EIP-2718) ─────────────────────────────
71
72/// A Legacy (Type 0) Ethereum transaction.
73///
74/// Uses EIP-155 replay protection via `chain_id` in the signing payload.
75#[derive(Debug, Clone)]
76pub struct LegacyTransaction {
77    /// The nonce of the sender.
78    pub nonce: u64,
79    /// Gas price in wei.
80    pub gas_price: u128,
81    /// Gas limit.
82    pub gas_limit: u64,
83    /// Recipient address. `None` for contract creation.
84    pub to: Option<[u8; 20]>,
85    /// Value in wei.
86    pub value: u128,
87    /// Call data.
88    pub data: Vec<u8>,
89    /// Chain ID for EIP-155 replay protection.
90    pub chain_id: u64,
91}
92
93impl LegacyTransaction {
94    /// Serialize the unsigned transaction for signing (EIP-155).
95    ///
96    /// `RLP([nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0])`
97    fn signing_payload(&self) -> Vec<u8> {
98        let mut items = Vec::new();
99        items.extend_from_slice(&rlp::encode_u64(self.nonce));
100        items.extend_from_slice(&rlp::encode_u128(self.gas_price));
101        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
102        items.extend_from_slice(&encode_address(&self.to));
103        items.extend_from_slice(&rlp::encode_u128(self.value));
104        items.extend_from_slice(&rlp::encode_bytes(&self.data));
105        // EIP-155: chain_id, 0, 0
106        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
107        items.extend_from_slice(&rlp::encode_u64(0));
108        items.extend_from_slice(&rlp::encode_u64(0));
109        rlp::encode_list(&items)
110    }
111
112    /// Sign this transaction with the given signer.
113    pub fn sign(&self, signer: &EthereumSigner) -> Result<SignedTransaction, SignerError> {
114        let payload = self.signing_payload();
115        let hash = keccak256(&payload);
116        let sig = signer.sign_digest(&hash)?;
117
118        // EIP-155: v = {0,1} + chain_id * 2 + 35
119        let v = (sig.v as u64 - 27) + self.chain_id * 2 + 35;
120
121        let mut items = Vec::new();
122        items.extend_from_slice(&rlp::encode_u64(self.nonce));
123        items.extend_from_slice(&rlp::encode_u128(self.gas_price));
124        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
125        items.extend_from_slice(&encode_address(&self.to));
126        items.extend_from_slice(&rlp::encode_u128(self.value));
127        items.extend_from_slice(&rlp::encode_bytes(&self.data));
128        items.extend_from_slice(&rlp::encode_u64(v));
129        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.r)));
130        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.s)));
131
132        Ok(SignedTransaction {
133            raw: rlp::encode_list(&items),
134        })
135    }
136}
137
138// ─── EIP-2930 Transaction (Type 1) ─────────────────────────────────
139
140/// An EIP-2930 (Type 1) transaction with access list.
141///
142/// Introduced by Berlin hard fork. Uses EIP-2718 typed transaction envelope.
143#[derive(Debug, Clone)]
144pub struct EIP2930Transaction {
145    /// Chain ID (required, not optional like Legacy).
146    pub chain_id: u64,
147    /// Sender nonce.
148    pub nonce: u64,
149    /// Gas price in wei.
150    pub gas_price: u128,
151    /// Gas limit.
152    pub gas_limit: u64,
153    /// Recipient. `None` for contract creation.
154    pub to: Option<[u8; 20]>,
155    /// Value in wei.
156    pub value: u128,
157    /// Call data.
158    pub data: Vec<u8>,
159    /// Access list: `[(address, [storage_key, ...])]`.
160    pub access_list: Vec<([u8; 20], Vec<[u8; 32]>)>,
161}
162
163impl EIP2930Transaction {
164    /// Signing payload: `keccak256(0x01 || RLP([chainId, nonce, gasPrice, gasLimit, to, value, data, accessList]))`
165    fn signing_hash(&self) -> [u8; 32] {
166        let mut items = Vec::new();
167        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
168        items.extend_from_slice(&rlp::encode_u64(self.nonce));
169        items.extend_from_slice(&rlp::encode_u128(self.gas_price));
170        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
171        items.extend_from_slice(&encode_address(&self.to));
172        items.extend_from_slice(&rlp::encode_u128(self.value));
173        items.extend_from_slice(&rlp::encode_bytes(&self.data));
174        items.extend_from_slice(&rlp::encode_access_list(&self.access_list));
175
176        let mut payload = vec![0x01]; // Type 1
177        payload.extend_from_slice(&rlp::encode_list(&items));
178        keccak256(&payload)
179    }
180
181    /// Sign this transaction.
182    pub fn sign(&self, signer: &EthereumSigner) -> Result<SignedTransaction, SignerError> {
183        let hash = self.signing_hash();
184        let sig = signer.sign_digest(&hash)?;
185        let y_parity = sig.v - 27; // 0 or 1
186
187        let mut items = Vec::new();
188        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
189        items.extend_from_slice(&rlp::encode_u64(self.nonce));
190        items.extend_from_slice(&rlp::encode_u128(self.gas_price));
191        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
192        items.extend_from_slice(&encode_address(&self.to));
193        items.extend_from_slice(&rlp::encode_u128(self.value));
194        items.extend_from_slice(&rlp::encode_bytes(&self.data));
195        items.extend_from_slice(&rlp::encode_access_list(&self.access_list));
196        items.extend_from_slice(&rlp::encode_u64(y_parity));
197        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.r)));
198        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.s)));
199
200        let mut raw = vec![0x01]; // Type prefix
201        raw.extend_from_slice(&rlp::encode_list(&items));
202
203        Ok(SignedTransaction { raw })
204    }
205}
206
207// ─── EIP-1559 Transaction (Type 2) ─────────────────────────────────
208
209/// An EIP-1559 (Type 2) dynamic fee transaction.
210///
211/// The de facto standard since the London hard fork. Uses `maxFeePerGas` and
212/// `maxPriorityFeePerGas` instead of a single `gasPrice`.
213#[derive(Debug, Clone)]
214pub struct EIP1559Transaction {
215    /// Chain ID (required).
216    pub chain_id: u64,
217    /// Sender nonce.
218    pub nonce: u64,
219    /// Maximum priority fee (tip) per gas in wei.
220    pub max_priority_fee_per_gas: u128,
221    /// Maximum total fee per gas in wei.
222    pub max_fee_per_gas: u128,
223    /// Gas limit.
224    pub gas_limit: u64,
225    /// Recipient. `None` for contract creation.
226    pub to: Option<[u8; 20]>,
227    /// Value in wei.
228    pub value: u128,
229    /// Call data.
230    pub data: Vec<u8>,
231    /// Access list: `[(address, [storage_key, ...])]`.
232    pub access_list: Vec<([u8; 20], Vec<[u8; 32]>)>,
233}
234
235impl EIP1559Transaction {
236    /// Signing hash: `keccak256(0x02 || RLP([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList]))`
237    fn signing_hash(&self) -> [u8; 32] {
238        let mut items = Vec::new();
239        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
240        items.extend_from_slice(&rlp::encode_u64(self.nonce));
241        items.extend_from_slice(&rlp::encode_u128(self.max_priority_fee_per_gas));
242        items.extend_from_slice(&rlp::encode_u128(self.max_fee_per_gas));
243        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
244        items.extend_from_slice(&encode_address(&self.to));
245        items.extend_from_slice(&rlp::encode_u128(self.value));
246        items.extend_from_slice(&rlp::encode_bytes(&self.data));
247        items.extend_from_slice(&rlp::encode_access_list(&self.access_list));
248
249        let mut payload = vec![0x02]; // Type 2
250        payload.extend_from_slice(&rlp::encode_list(&items));
251        keccak256(&payload)
252    }
253
254    /// Sign this transaction.
255    pub fn sign(&self, signer: &EthereumSigner) -> Result<SignedTransaction, SignerError> {
256        let hash = self.signing_hash();
257        let sig = signer.sign_digest(&hash)?;
258        let y_parity = sig.v - 27; // 0 or 1
259
260        let mut items = Vec::new();
261        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
262        items.extend_from_slice(&rlp::encode_u64(self.nonce));
263        items.extend_from_slice(&rlp::encode_u128(self.max_priority_fee_per_gas));
264        items.extend_from_slice(&rlp::encode_u128(self.max_fee_per_gas));
265        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
266        items.extend_from_slice(&encode_address(&self.to));
267        items.extend_from_slice(&rlp::encode_u128(self.value));
268        items.extend_from_slice(&rlp::encode_bytes(&self.data));
269        items.extend_from_slice(&rlp::encode_access_list(&self.access_list));
270        items.extend_from_slice(&rlp::encode_u64(y_parity));
271        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.r)));
272        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.s)));
273
274        let mut raw = vec![0x02]; // Type prefix
275        raw.extend_from_slice(&rlp::encode_list(&items));
276
277        Ok(SignedTransaction { raw })
278    }
279}
280
281// ─── EIP-4844 Transaction (Type 3) ─────────────────────────────────
282
283/// An EIP-4844 (Type 3) blob transaction.
284///
285/// Carries blob versioned hashes for rollup data availability.
286/// Note: the actual blob data and KZG proofs are sidecar data, not
287/// part of the transaction itself.
288#[derive(Debug, Clone)]
289pub struct EIP4844Transaction {
290    /// Chain ID (required).
291    pub chain_id: u64,
292    /// Sender nonce.
293    pub nonce: u64,
294    /// Maximum priority fee (tip) per gas in wei.
295    pub max_priority_fee_per_gas: u128,
296    /// Maximum total fee per gas in wei.
297    pub max_fee_per_gas: u128,
298    /// Gas limit.
299    pub gas_limit: u64,
300    /// Recipient address (required — no contract creation).
301    pub to: [u8; 20],
302    /// Value in wei.
303    pub value: u128,
304    /// Call data.
305    pub data: Vec<u8>,
306    /// Access list.
307    pub access_list: Vec<([u8; 20], Vec<[u8; 32]>)>,
308    /// Maximum fee per blob gas in wei.
309    pub max_fee_per_blob_gas: u128,
310    /// Blob versioned hashes (32 bytes each, version byte 0x01).
311    pub blob_versioned_hashes: Vec<[u8; 32]>,
312}
313
314impl EIP4844Transaction {
315    /// Signing hash: `keccak256(0x03 || RLP([...fields, max_fee_per_blob_gas, blob_versioned_hashes]))`
316    fn signing_hash(&self) -> [u8; 32] {
317        let mut items = Vec::new();
318        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
319        items.extend_from_slice(&rlp::encode_u64(self.nonce));
320        items.extend_from_slice(&rlp::encode_u128(self.max_priority_fee_per_gas));
321        items.extend_from_slice(&rlp::encode_u128(self.max_fee_per_gas));
322        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
323        items.extend_from_slice(&rlp::encode_bytes(&self.to));
324        items.extend_from_slice(&rlp::encode_u128(self.value));
325        items.extend_from_slice(&rlp::encode_bytes(&self.data));
326        items.extend_from_slice(&rlp::encode_access_list(&self.access_list));
327        items.extend_from_slice(&rlp::encode_u128(self.max_fee_per_blob_gas));
328        // blob_versioned_hashes as RLP list of 32-byte strings
329        let mut hash_items = Vec::new();
330        for h in &self.blob_versioned_hashes {
331            hash_items.extend_from_slice(&rlp::encode_bytes(h));
332        }
333        items.extend_from_slice(&rlp::encode_list(&hash_items));
334
335        let mut payload = vec![0x03]; // Type 3
336        payload.extend_from_slice(&rlp::encode_list(&items));
337        keccak256(&payload)
338    }
339
340    /// Sign this transaction.
341    pub fn sign(&self, signer: &EthereumSigner) -> Result<SignedTransaction, SignerError> {
342        let hash = self.signing_hash();
343        let sig = signer.sign_digest(&hash)?;
344        let y_parity = sig.v - 27;
345
346        let mut items = Vec::new();
347        items.extend_from_slice(&rlp::encode_u64(self.chain_id));
348        items.extend_from_slice(&rlp::encode_u64(self.nonce));
349        items.extend_from_slice(&rlp::encode_u128(self.max_priority_fee_per_gas));
350        items.extend_from_slice(&rlp::encode_u128(self.max_fee_per_gas));
351        items.extend_from_slice(&rlp::encode_u64(self.gas_limit));
352        items.extend_from_slice(&rlp::encode_bytes(&self.to));
353        items.extend_from_slice(&rlp::encode_u128(self.value));
354        items.extend_from_slice(&rlp::encode_bytes(&self.data));
355        items.extend_from_slice(&rlp::encode_access_list(&self.access_list));
356        items.extend_from_slice(&rlp::encode_u128(self.max_fee_per_blob_gas));
357        let mut hash_items = Vec::new();
358        for h in &self.blob_versioned_hashes {
359            hash_items.extend_from_slice(&rlp::encode_bytes(h));
360        }
361        items.extend_from_slice(&rlp::encode_list(&hash_items));
362        items.extend_from_slice(&rlp::encode_u64(y_parity));
363        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.r)));
364        items.extend_from_slice(&rlp::encode_bytes(&strip_leading_zeros(&sig.s)));
365
366        let mut raw = vec![0x03];
367        raw.extend_from_slice(&rlp::encode_list(&items));
368
369        Ok(SignedTransaction { raw })
370    }
371}
372
373// ─── Contract Address Prediction ───────────────────────────────────
374
375/// Predict the contract address deployed via CREATE.
376///
377/// `keccak256(RLP([sender, nonce]))[12..32]`
378pub fn create_address(sender: &[u8; 20], nonce: u64) -> [u8; 20] {
379    let mut items = Vec::new();
380    items.extend_from_slice(&rlp::encode_bytes(sender));
381    items.extend_from_slice(&rlp::encode_u64(nonce));
382    let rlp_data = rlp::encode_list(&items);
383    let hash = keccak256(&rlp_data);
384    let mut addr = [0u8; 20];
385    addr.copy_from_slice(&hash[12..]);
386    addr
387}
388
389/// Predict the contract address deployed via CREATE2 (EIP-1014).
390///
391/// `keccak256(0xFF || sender || salt || keccak256(init_code))[12..32]`
392pub fn create2_address(sender: &[u8; 20], salt: &[u8; 32], init_code: &[u8]) -> [u8; 20] {
393    let code_hash = keccak256(init_code);
394    let mut buf = Vec::with_capacity(1 + 20 + 32 + 32);
395    buf.push(0xFF);
396    buf.extend_from_slice(sender);
397    buf.extend_from_slice(salt);
398    buf.extend_from_slice(&code_hash);
399    let hash = keccak256(&buf);
400    let mut addr = [0u8; 20];
401    addr.copy_from_slice(&hash[12..]);
402    addr
403}
404
405// ─── EIP-1271: Contract Signature ──────────────────────────────────
406
407/// EIP-1271 magic value returned by `isValidSignature` on success.
408pub const EIP1271_MAGIC: [u8; 4] = [0x16, 0x26, 0xBA, 0x7E];
409
410/// Encode an `isValidSignature(bytes32, bytes)` call for EIP-1271.
411///
412/// Returns the ABI-encoded calldata suitable for `eth_call`.
413pub fn encode_is_valid_signature(hash: &[u8; 32], signature: &[u8]) -> Vec<u8> {
414    // Function selector: keccak256("isValidSignature(bytes32,bytes)")[..4]
415    let selector = &keccak256(b"isValidSignature(bytes32,bytes)")[..4];
416
417    let mut calldata = Vec::new();
418    calldata.extend_from_slice(selector);
419    // hash (bytes32) — padded to 32 bytes
420    calldata.extend_from_slice(hash);
421    // offset to bytes data (64 bytes from start of params)
422    let mut offset = [0u8; 32];
423    offset[31] = 64;
424    calldata.extend_from_slice(&offset);
425    // length of signature
426    let mut len_buf = [0u8; 32];
427    len_buf[28..32].copy_from_slice(&(signature.len() as u32).to_be_bytes());
428    calldata.extend_from_slice(&len_buf);
429    // signature data (padded to 32-byte boundary)
430    calldata.extend_from_slice(signature);
431    let padding = (32 - (signature.len() % 32)) % 32;
432    calldata.extend_from_slice(&vec![0u8; padding]);
433
434    calldata
435}
436
437// ─── Helpers ───────────────────────────────────────────────────────
438
439fn keccak256(data: &[u8]) -> [u8; 32] {
440    let mut out = [0u8; 32];
441    out.copy_from_slice(&Keccak256::digest(data));
442    out
443}
444
445fn encode_address(to: &Option<[u8; 20]>) -> Vec<u8> {
446    match to {
447        Some(addr) => rlp::encode_bytes(addr),
448        None => rlp::encode_bytes(&[]),
449    }
450}
451
452fn strip_leading_zeros(data: &[u8; 32]) -> Vec<u8> {
453    let start = data.iter().position(|b| *b != 0).unwrap_or(31);
454    data[start..].to_vec()
455}
456
457// ─── Signed Transaction Decoding ───────────────────────────────────
458
459/// The type of an Ethereum transaction.
460#[derive(Debug, Clone, Copy, PartialEq, Eq)]
461pub enum TxType {
462    /// Pre-EIP-2718 legacy transaction.
463    Legacy,
464    /// EIP-2930 (Type 1) — access list transaction.
465    Type1AccessList,
466    /// EIP-1559 (Type 2) — dynamic fee transaction.
467    Type2DynamicFee,
468    /// EIP-4844 (Type 3) — blob transaction.
469    Type3Blob,
470}
471
472/// A decoded signed Ethereum transaction.
473#[derive(Debug, Clone)]
474pub struct DecodedTransaction {
475    /// Transaction type.
476    pub tx_type: TxType,
477    /// Chain ID.
478    pub chain_id: u64,
479    /// Sender nonce.
480    pub nonce: u64,
481    /// Recipient address (`None` for contract creation).
482    pub to: Option<[u8; 20]>,
483    /// Value in wei (as raw bytes, big-endian).
484    pub value: Vec<u8>,
485    /// Calldata.
486    pub data: Vec<u8>,
487    /// Gas limit.
488    pub gas_limit: u64,
489    /// Gas price (Legacy/Type 1) or max_fee_per_gas (Type 2/3).
490    pub gas_price_or_max_fee: Vec<u8>,
491    /// Max priority fee per gas (Type 2/3 only, empty for Legacy/Type 1).
492    pub max_priority_fee: Vec<u8>,
493    /// Signature v / y_parity.
494    pub v: u64,
495    /// Signature r (32 bytes).
496    pub r: [u8; 32],
497    /// Signature s (32 bytes).
498    pub s: [u8; 32],
499    /// Recovered signer address (20 bytes).
500    pub from: [u8; 20],
501    /// Transaction hash.
502    pub tx_hash: [u8; 32],
503}
504
505/// Decode a signed transaction from raw bytes and recover the signer.
506///
507/// Supports Legacy, Type 1 (EIP-2930), Type 2 (EIP-1559), and Type 3 (EIP-4844).
508///
509/// # Example
510/// ```no_run
511/// use chains_sdk::ethereum::transaction::decode_signed_tx;
512///
513/// fn example(raw_tx: &[u8]) {
514///     let decoded = decode_signed_tx(raw_tx).unwrap();
515///     println!("From: 0x{}", hex::encode(decoded.from));
516///     println!("Type: {:?}", decoded.tx_type);
517///     println!("Nonce: {}", decoded.nonce);
518/// }
519/// ```
520pub fn decode_signed_tx(raw: &[u8]) -> Result<DecodedTransaction, SignerError> {
521    if raw.is_empty() {
522        return Err(SignerError::ParseError("empty transaction".into()));
523    }
524
525    let tx_hash = keccak256(raw);
526
527    match raw[0] {
528        // EIP-2718 typed transactions: first byte < 0x7F
529        0x01 => decode_type1_tx(raw, tx_hash),
530        0x02 => decode_type2_tx(raw, tx_hash),
531        0x03 => decode_type3_tx(raw, tx_hash),
532        // Legacy: first byte >= 0xC0 (RLP list prefix)
533        0xC0..=0xFF => decode_legacy_tx(raw, tx_hash),
534        b => Err(SignerError::ParseError(format!(
535            "unknown tx type byte: 0x{b:02x}"
536        ))),
537    }
538}
539
540fn decode_legacy_tx(raw: &[u8], tx_hash: [u8; 32]) -> Result<DecodedTransaction, SignerError> {
541    let items = rlp::decode_list_items(raw)?;
542    if items.len() != 9 {
543        return Err(SignerError::ParseError(format!(
544            "legacy tx: expected 9 RLP items, got {}",
545            items.len()
546        )));
547    }
548
549    let nonce = items[0].as_u64()?;
550    let gas_price = items[1].as_bytes()?.to_vec();
551    let gas_limit = items[2].as_u64()?;
552    let to_bytes = items[3].as_bytes()?;
553    let to = if to_bytes.len() == 20 {
554        let mut addr = [0u8; 20];
555        addr.copy_from_slice(to_bytes);
556        Some(addr)
557    } else {
558        None
559    };
560    let value = items[4].as_bytes()?.to_vec();
561    let data = items[5].as_bytes()?.to_vec();
562    let v = items[6].as_u64()?;
563    let r = pad_to_32(items[7].as_bytes()?);
564    let s = pad_to_32(items[8].as_bytes()?);
565
566    // EIP-155: chain_id = (v - 35) / 2
567    let chain_id = if v >= 35 { (v - 35) / 2 } else { 0 };
568    let recovery_id = if v >= 35 { (v - 35) % 2 } else { v - 27 };
569
570    // Reconstruct signing payload for ecrecover
571    let mut sign_items = Vec::new();
572    sign_items.extend_from_slice(&rlp::encode_u64(nonce));
573    sign_items.extend_from_slice(&rlp::encode_bytes(&gas_price));
574    sign_items.extend_from_slice(&rlp::encode_u64(gas_limit));
575    sign_items.extend_from_slice(&encode_address(&to));
576    sign_items.extend_from_slice(&rlp::encode_bytes(&value));
577    sign_items.extend_from_slice(&rlp::encode_bytes(&data));
578    if chain_id > 0 {
579        sign_items.extend_from_slice(&rlp::encode_u64(chain_id));
580        sign_items.extend_from_slice(&rlp::encode_u64(0));
581        sign_items.extend_from_slice(&rlp::encode_u64(0));
582    }
583    let signing_hash = keccak256(&rlp::encode_list(&sign_items));
584
585    let from = recover_signer(&signing_hash, &r, &s, recovery_id as u8)?;
586
587    Ok(DecodedTransaction {
588        tx_type: TxType::Legacy,
589        chain_id,
590        nonce,
591        to,
592        value,
593        data,
594        gas_limit,
595        gas_price_or_max_fee: gas_price,
596        max_priority_fee: vec![],
597        v,
598        r,
599        s,
600        from,
601        tx_hash,
602    })
603}
604
605fn decode_type1_tx(raw: &[u8], tx_hash: [u8; 32]) -> Result<DecodedTransaction, SignerError> {
606    let items = rlp::decode_list_items(&raw[1..])?;
607    if items.len() != 11 {
608        return Err(SignerError::ParseError(format!(
609            "type1 tx: expected 11 items, got {}",
610            items.len()
611        )));
612    }
613
614    let chain_id = items[0].as_u64()?;
615    let nonce = items[1].as_u64()?;
616    let gas_price = items[2].as_bytes()?.to_vec();
617    let gas_limit = items[3].as_u64()?;
618    let to_bytes = items[4].as_bytes()?;
619    let to = if to_bytes.len() == 20 {
620        let mut a = [0u8; 20];
621        a.copy_from_slice(to_bytes);
622        Some(a)
623    } else {
624        None
625    };
626    let value = items[5].as_bytes()?.to_vec();
627    let data = items[6].as_bytes()?.to_vec();
628    // items[7] = access_list (skip for decode)
629    let y_parity = items[8].as_u64()?;
630    let r = pad_to_32(items[9].as_bytes()?);
631    let s = pad_to_32(items[10].as_bytes()?);
632
633    // Reconstruct signing hash
634    let mut sign_items = Vec::new();
635    sign_items.extend_from_slice(&rlp::encode_u64(chain_id));
636    sign_items.extend_from_slice(&rlp::encode_u64(nonce));
637    sign_items.extend_from_slice(&rlp::encode_bytes(&gas_price));
638    sign_items.extend_from_slice(&rlp::encode_u64(gas_limit));
639    sign_items.extend_from_slice(&encode_address(&to));
640    sign_items.extend_from_slice(&rlp::encode_bytes(&value));
641    sign_items.extend_from_slice(&rlp::encode_bytes(&data));
642    // Re-encode the access list from the decoded items
643    sign_items.extend_from_slice(&re_encode_rlp_item(&items[7]));
644    let mut payload = vec![0x01];
645    payload.extend_from_slice(&rlp::encode_list(&sign_items));
646    let signing_hash = keccak256(&payload);
647
648    let from = recover_signer(&signing_hash, &r, &s, y_parity as u8)?;
649
650    Ok(DecodedTransaction {
651        tx_type: TxType::Type1AccessList,
652        chain_id,
653        nonce,
654        to,
655        value,
656        data,
657        gas_limit,
658        gas_price_or_max_fee: gas_price,
659        max_priority_fee: vec![],
660        v: y_parity,
661        r,
662        s,
663        from,
664        tx_hash,
665    })
666}
667
668fn decode_type2_tx(raw: &[u8], tx_hash: [u8; 32]) -> Result<DecodedTransaction, SignerError> {
669    let items = rlp::decode_list_items(&raw[1..])?;
670    if items.len() != 12 {
671        return Err(SignerError::ParseError(format!(
672            "type2 tx: expected 12 items, got {}",
673            items.len()
674        )));
675    }
676
677    let chain_id = items[0].as_u64()?;
678    let nonce = items[1].as_u64()?;
679    let max_priority_fee = items[2].as_bytes()?.to_vec();
680    let max_fee = items[3].as_bytes()?.to_vec();
681    let gas_limit = items[4].as_u64()?;
682    let to_bytes = items[5].as_bytes()?;
683    let to = if to_bytes.len() == 20 {
684        let mut a = [0u8; 20];
685        a.copy_from_slice(to_bytes);
686        Some(a)
687    } else {
688        None
689    };
690    let value = items[6].as_bytes()?.to_vec();
691    let data = items[7].as_bytes()?.to_vec();
692    // items[8] = access_list
693    let y_parity = items[9].as_u64()?;
694    let r = pad_to_32(items[10].as_bytes()?);
695    let s = pad_to_32(items[11].as_bytes()?);
696
697    // Reconstruct signing hash
698    let mut sign_items = Vec::new();
699    sign_items.extend_from_slice(&rlp::encode_u64(chain_id));
700    sign_items.extend_from_slice(&rlp::encode_u64(nonce));
701    sign_items.extend_from_slice(&rlp::encode_bytes(&max_priority_fee));
702    sign_items.extend_from_slice(&rlp::encode_bytes(&max_fee));
703    sign_items.extend_from_slice(&rlp::encode_u64(gas_limit));
704    sign_items.extend_from_slice(&encode_address(&to));
705    sign_items.extend_from_slice(&rlp::encode_bytes(&value));
706    sign_items.extend_from_slice(&rlp::encode_bytes(&data));
707    sign_items.extend_from_slice(&re_encode_rlp_item(&items[8]));
708    let mut payload = vec![0x02];
709    payload.extend_from_slice(&rlp::encode_list(&sign_items));
710    let signing_hash = keccak256(&payload);
711
712    let from = recover_signer(&signing_hash, &r, &s, y_parity as u8)?;
713
714    Ok(DecodedTransaction {
715        tx_type: TxType::Type2DynamicFee,
716        chain_id,
717        nonce,
718        to,
719        value,
720        data,
721        gas_limit,
722        gas_price_or_max_fee: max_fee,
723        max_priority_fee,
724        v: y_parity,
725        r,
726        s,
727        from,
728        tx_hash,
729    })
730}
731
732fn decode_type3_tx(raw: &[u8], tx_hash: [u8; 32]) -> Result<DecodedTransaction, SignerError> {
733    let items = rlp::decode_list_items(&raw[1..])?;
734    if items.len() != 14 {
735        return Err(SignerError::ParseError(format!(
736            "type3 tx: expected 14 items, got {}",
737            items.len()
738        )));
739    }
740
741    let chain_id = items[0].as_u64()?;
742    let nonce = items[1].as_u64()?;
743    let max_priority_fee = items[2].as_bytes()?.to_vec();
744    let max_fee = items[3].as_bytes()?.to_vec();
745    let gas_limit = items[4].as_u64()?;
746    let to_bytes = items[5].as_bytes()?;
747    let to = if to_bytes.len() == 20 {
748        let mut a = [0u8; 20];
749        a.copy_from_slice(to_bytes);
750        Some(a)
751    } else {
752        None
753    };
754    let value = items[6].as_bytes()?.to_vec();
755    let data = items[7].as_bytes()?.to_vec();
756    // items[8] = access_list, items[9] = max_fee_per_blob_gas, items[10] = blob_hashes
757    let y_parity = items[11].as_u64()?;
758    let r = pad_to_32(items[12].as_bytes()?);
759    let s = pad_to_32(items[13].as_bytes()?);
760
761    // Reconstruct signing hash
762    let mut sign_items = Vec::new();
763    sign_items.extend_from_slice(&rlp::encode_u64(chain_id));
764    sign_items.extend_from_slice(&rlp::encode_u64(nonce));
765    sign_items.extend_from_slice(&rlp::encode_bytes(&max_priority_fee));
766    sign_items.extend_from_slice(&rlp::encode_bytes(&max_fee));
767    sign_items.extend_from_slice(&rlp::encode_u64(gas_limit));
768    sign_items.extend_from_slice(&encode_address(&to));
769    sign_items.extend_from_slice(&rlp::encode_bytes(&value));
770    sign_items.extend_from_slice(&rlp::encode_bytes(&data));
771    sign_items.extend_from_slice(&re_encode_rlp_item(&items[8]));
772    sign_items.extend_from_slice(&re_encode_rlp_item(&items[9]));
773    sign_items.extend_from_slice(&re_encode_rlp_item(&items[10]));
774    let mut payload = vec![0x03];
775    payload.extend_from_slice(&rlp::encode_list(&sign_items));
776    let signing_hash = keccak256(&payload);
777
778    let from = recover_signer(&signing_hash, &r, &s, y_parity as u8)?;
779
780    Ok(DecodedTransaction {
781        tx_type: TxType::Type3Blob,
782        chain_id,
783        nonce,
784        to,
785        value,
786        data,
787        gas_limit,
788        gas_price_or_max_fee: max_fee,
789        max_priority_fee,
790        v: y_parity,
791        r,
792        s,
793        from,
794        tx_hash,
795    })
796}
797
798/// Re-encode a decoded RLP item back to bytes.
799fn re_encode_rlp_item(item: &rlp::RlpItem) -> Vec<u8> {
800    match item {
801        rlp::RlpItem::Bytes(b) => rlp::encode_bytes(b),
802        rlp::RlpItem::List(items) => {
803            let mut inner = Vec::new();
804            for i in items {
805                inner.extend_from_slice(&re_encode_rlp_item(i));
806            }
807            rlp::encode_list(&inner)
808        }
809    }
810}
811
812/// Recover the signer address from a message hash and ECDSA signature.
813fn recover_signer(
814    hash: &[u8; 32],
815    r: &[u8; 32],
816    s: &[u8; 32],
817    recovery_id: u8,
818) -> Result<[u8; 20], SignerError> {
819    use k256::ecdsa::{RecoveryId, Signature as K256Signature, VerifyingKey};
820
821    let mut sig_bytes = [0u8; 64];
822    sig_bytes[..32].copy_from_slice(r);
823    sig_bytes[32..].copy_from_slice(s);
824    let sig = K256Signature::from_bytes((&sig_bytes).into())
825        .map_err(|e| SignerError::InvalidSignature(format!("invalid sig: {e}")))?;
826    let rid = RecoveryId::new(recovery_id & 1 != 0, false);
827    let key = VerifyingKey::recover_from_prehash(hash, &sig, rid)
828        .map_err(|e| SignerError::InvalidSignature(format!("ecrecover: {e}")))?;
829
830    let uncompressed = key.to_encoded_point(false);
831    let pub_bytes = &uncompressed.as_bytes()[1..]; // skip 0x04 prefix
832    let addr_hash = keccak256(pub_bytes);
833    let mut addr = [0u8; 20];
834    addr.copy_from_slice(&addr_hash[12..]);
835    Ok(addr)
836}
837
838fn pad_to_32(data: &[u8]) -> [u8; 32] {
839    let mut buf = [0u8; 32];
840    if data.len() <= 32 {
841        buf[32 - data.len()..].copy_from_slice(data);
842    }
843    buf
844}
845
846#[cfg(test)]
847#[allow(clippy::unwrap_used, clippy::expect_used)]
848mod tests {
849    use super::*;
850    use crate::traits::KeyPair;
851
852    #[test]
853    fn test_legacy_tx_sign_recoverable() {
854        let signer = EthereumSigner::generate().unwrap();
855        let tx = LegacyTransaction {
856            nonce: 0,
857            gas_price: 20_000_000_000, // 20 Gwei
858            gas_limit: 21_000,
859            to: Some([0xBB; 20]),
860            value: 1_000_000_000_000_000_000, // 1 ETH
861            data: vec![],
862            chain_id: 1,
863        };
864        let signed = tx.sign(&signer).unwrap();
865        let raw = signed.raw_tx();
866        assert!(!raw.is_empty());
867        // Must be valid RLP
868        let decoded = rlp::decode(raw).unwrap();
869        let items = decoded.as_list().unwrap();
870        assert_eq!(items.len(), 9); // nonce, gasPrice, gasLimit, to, value, data, v, r, s
871    }
872
873    #[test]
874    fn test_legacy_tx_hash_deterministic() {
875        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
876        let tx = LegacyTransaction {
877            nonce: 5,
878            gas_price: 30_000_000_000,
879            gas_limit: 21_000,
880            to: Some([0xCC; 20]),
881            value: 0,
882            data: vec![0xDE, 0xAD],
883            chain_id: 1,
884        };
885        let signed1 = tx.sign(&signer).unwrap();
886        let signed2 = tx.sign(&signer).unwrap();
887        // RFC 6979 deterministic: same tx + same key = same signature
888        assert_eq!(signed1.tx_hash(), signed2.tx_hash());
889    }
890
891    #[test]
892    fn test_legacy_contract_creation() {
893        let signer = EthereumSigner::generate().unwrap();
894        let tx = LegacyTransaction {
895            nonce: 0,
896            gas_price: 20_000_000_000,
897            gas_limit: 1_000_000,
898            to: None, // contract creation
899            value: 0,
900            data: vec![0x60, 0x00], // minimal bytecode
901            chain_id: 1,
902        };
903        let signed = tx.sign(&signer).unwrap();
904        assert!(!signed.raw_tx().is_empty());
905    }
906
907    #[test]
908    fn test_eip2930_tx_type1_prefix() {
909        let signer = EthereumSigner::generate().unwrap();
910        let tx = EIP2930Transaction {
911            chain_id: 1,
912            nonce: 0,
913            gas_price: 20_000_000_000,
914            gas_limit: 21_000,
915            to: Some([0xAA; 20]),
916            value: 1_000_000_000_000_000_000,
917            data: vec![],
918            access_list: vec![([0xDD; 20], vec![[0xEE; 32]])],
919        };
920        let signed = tx.sign(&signer).unwrap();
921        assert_eq!(signed.raw_tx()[0], 0x01, "Type 1 prefix");
922    }
923
924    #[test]
925    fn test_eip1559_tx_type2_prefix() {
926        let signer = EthereumSigner::generate().unwrap();
927        let tx = EIP1559Transaction {
928            chain_id: 1,
929            nonce: 0,
930            max_priority_fee_per_gas: 2_000_000_000,
931            max_fee_per_gas: 100_000_000_000,
932            gas_limit: 21_000,
933            to: Some([0xAA; 20]),
934            value: 1_000_000_000_000_000_000,
935            data: vec![],
936            access_list: vec![],
937        };
938        let signed = tx.sign(&signer).unwrap();
939        assert_eq!(signed.raw_tx()[0], 0x02, "Type 2 prefix");
940    }
941
942    #[test]
943    fn test_eip1559_different_nonces_different_hashes() {
944        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
945        let base = EIP1559Transaction {
946            chain_id: 1,
947            nonce: 0,
948            max_priority_fee_per_gas: 2_000_000_000,
949            max_fee_per_gas: 100_000_000_000,
950            gas_limit: 21_000,
951            to: Some([0xAA; 20]),
952            value: 0,
953            data: vec![],
954            access_list: vec![],
955        };
956        let mut tx2 = base.clone();
957        tx2.nonce = 1;
958        let h1 = base.sign(&signer).unwrap().tx_hash();
959        let h2 = tx2.sign(&signer).unwrap().tx_hash();
960        assert_ne!(h1, h2);
961    }
962
963    #[test]
964    fn test_eip4844_tx_type3_prefix() {
965        let signer = EthereumSigner::generate().unwrap();
966        let tx = EIP4844Transaction {
967            chain_id: 1,
968            nonce: 0,
969            max_priority_fee_per_gas: 2_000_000_000,
970            max_fee_per_gas: 100_000_000_000,
971            gas_limit: 21_000,
972            to: [0xAA; 20],
973            value: 0,
974            data: vec![],
975            access_list: vec![],
976            max_fee_per_blob_gas: 1_000_000_000,
977            blob_versioned_hashes: vec![[0x01; 32]],
978        };
979        let signed = tx.sign(&signer).unwrap();
980        assert_eq!(signed.raw_tx()[0], 0x03, "Type 3 prefix");
981    }
982
983    #[test]
984    fn test_create_address_known_vector() {
985        // Known: sender 0x0000...0000 nonce 0 → specific address
986        let sender = [0u8; 20];
987        let addr = create_address(&sender, 0);
988        assert_eq!(addr.len(), 20);
989        // Verify it's deterministic
990        assert_eq!(addr, create_address(&sender, 0));
991        // Different nonce → different address
992        assert_ne!(addr, create_address(&sender, 1));
993    }
994
995    #[test]
996    fn test_create2_address_eip1014_vector() {
997        // EIP-1014 test vector #1:
998        // sender = 0x0000000000000000000000000000000000000000
999        // salt = 0x00...00
1000        // init_code = 0x00
1001        // expected = keccak256(0xff ++ sender ++ salt ++ keccak256(init_code))[12:]
1002        let sender = [0u8; 20];
1003        let salt = [0u8; 32];
1004        let addr = create2_address(&sender, &salt, &[0x00]);
1005        // Verify determinism
1006        assert_eq!(addr, create2_address(&sender, &salt, &[0x00]));
1007        // Different init_code → different address
1008        assert_ne!(addr, create2_address(&sender, &salt, &[0x01]));
1009    }
1010
1011    #[test]
1012    fn test_eip1271_encode() {
1013        let hash = [0xAA; 32];
1014        let sig = vec![0xBB; 65];
1015        let calldata = encode_is_valid_signature(&hash, &sig);
1016        // First 4 bytes = function selector
1017        assert_eq!(
1018            &calldata[..4],
1019            &keccak256(b"isValidSignature(bytes32,bytes)")[..4]
1020        );
1021        // Next 32 bytes = hash
1022        assert_eq!(&calldata[4..36], &hash);
1023    }
1024
1025    #[test]
1026    fn test_raw_tx_hex_format() {
1027        let signer = EthereumSigner::generate().unwrap();
1028        let tx = EIP1559Transaction {
1029            chain_id: 1,
1030            nonce: 0,
1031            max_priority_fee_per_gas: 0,
1032            max_fee_per_gas: 0,
1033            gas_limit: 21_000,
1034            to: Some([0; 20]),
1035            value: 0,
1036            data: vec![],
1037            access_list: vec![],
1038        };
1039        let hex = tx.sign(&signer).unwrap().raw_tx_hex();
1040        assert!(hex.starts_with("0x02"), "should start with 0x02");
1041    }
1042
1043    #[test]
1044    fn test_signed_tx_hash_is_keccak_of_raw() {
1045        let signer = EthereumSigner::generate().unwrap();
1046        let tx = EIP1559Transaction {
1047            chain_id: 1,
1048            nonce: 42,
1049            max_priority_fee_per_gas: 1_000_000,
1050            max_fee_per_gas: 50_000_000_000,
1051            gas_limit: 100_000,
1052            to: Some([0xFF; 20]),
1053            value: 500_000_000_000_000,
1054            data: vec![0x01, 0x02, 0x03],
1055            access_list: vec![],
1056        };
1057        let signed = tx.sign(&signer).unwrap();
1058        let expected = keccak256(signed.raw_tx());
1059        assert_eq!(signed.tx_hash(), expected);
1060    }
1061
1062    // ─── Signed Transaction Decoding Tests ─────────────────────────
1063
1064    #[test]
1065    fn test_decode_legacy_roundtrip() {
1066        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1067        let tx = LegacyTransaction {
1068            nonce: 7,
1069            gas_price: 20_000_000_000,
1070            gas_limit: 21_000,
1071            to: Some([0xBB; 20]),
1072            value: 1_000_000_000_000_000_000,
1073            data: vec![0xAB, 0xCD],
1074            chain_id: 1,
1075        };
1076        let signed = tx.sign(&signer).unwrap();
1077        let decoded = decode_signed_tx(signed.raw_tx()).unwrap();
1078
1079        assert_eq!(decoded.tx_type, TxType::Legacy);
1080        assert_eq!(decoded.chain_id, 1);
1081        assert_eq!(decoded.nonce, 7);
1082        assert_eq!(decoded.gas_limit, 21_000);
1083        assert_eq!(decoded.to, Some([0xBB; 20]));
1084        assert_eq!(decoded.data, vec![0xAB, 0xCD]);
1085        assert_eq!(decoded.from, signer.address());
1086        assert_eq!(decoded.tx_hash, signed.tx_hash());
1087    }
1088
1089    #[test]
1090    fn test_decode_type1_roundtrip() {
1091        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1092        let tx = EIP2930Transaction {
1093            chain_id: 1,
1094            nonce: 3,
1095            gas_price: 30_000_000_000,
1096            gas_limit: 50_000,
1097            to: Some([0xCC; 20]),
1098            value: 0,
1099            data: vec![0x01],
1100            access_list: vec![([0xDD; 20], vec![[0xEE; 32]])],
1101        };
1102        let signed = tx.sign(&signer).unwrap();
1103        let decoded = decode_signed_tx(signed.raw_tx()).unwrap();
1104
1105        assert_eq!(decoded.tx_type, TxType::Type1AccessList);
1106        assert_eq!(decoded.chain_id, 1);
1107        assert_eq!(decoded.nonce, 3);
1108        assert_eq!(decoded.from, signer.address());
1109    }
1110
1111    #[test]
1112    fn test_decode_type2_roundtrip() {
1113        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1114        let tx = EIP1559Transaction {
1115            chain_id: 1,
1116            nonce: 42,
1117            max_priority_fee_per_gas: 2_000_000_000,
1118            max_fee_per_gas: 100_000_000_000,
1119            gas_limit: 21_000,
1120            to: Some([0xAA; 20]),
1121            value: 500_000_000_000_000,
1122            data: vec![],
1123            access_list: vec![],
1124        };
1125        let signed = tx.sign(&signer).unwrap();
1126        let decoded = decode_signed_tx(signed.raw_tx()).unwrap();
1127
1128        assert_eq!(decoded.tx_type, TxType::Type2DynamicFee);
1129        assert_eq!(decoded.chain_id, 1);
1130        assert_eq!(decoded.nonce, 42);
1131        assert_eq!(decoded.gas_limit, 21_000);
1132        assert_eq!(decoded.to, Some([0xAA; 20]));
1133        assert_eq!(decoded.from, signer.address());
1134        assert_eq!(decoded.tx_hash, signed.tx_hash());
1135    }
1136
1137    #[test]
1138    fn test_decode_type3_roundtrip() {
1139        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1140        let tx = EIP4844Transaction {
1141            chain_id: 1,
1142            nonce: 0,
1143            max_priority_fee_per_gas: 1_000_000_000,
1144            max_fee_per_gas: 50_000_000_000,
1145            gas_limit: 100_000,
1146            to: [0xFF; 20],
1147            value: 0,
1148            data: vec![],
1149            access_list: vec![],
1150            max_fee_per_blob_gas: 1_000_000_000,
1151            blob_versioned_hashes: vec![[0x01; 32]],
1152        };
1153        let signed = tx.sign(&signer).unwrap();
1154        let decoded = decode_signed_tx(signed.raw_tx()).unwrap();
1155
1156        assert_eq!(decoded.tx_type, TxType::Type3Blob);
1157        assert_eq!(decoded.from, signer.address());
1158        assert_eq!(decoded.chain_id, 1);
1159    }
1160
1161    #[test]
1162    fn test_decode_contract_creation() {
1163        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1164        let tx = LegacyTransaction {
1165            nonce: 0,
1166            gas_price: 20_000_000_000,
1167            gas_limit: 1_000_000,
1168            to: None,
1169            value: 0,
1170            data: vec![0x60, 0x00],
1171            chain_id: 1,
1172        };
1173        let signed = tx.sign(&signer).unwrap();
1174        let decoded = decode_signed_tx(signed.raw_tx()).unwrap();
1175
1176        assert_eq!(decoded.to, None, "contract creation has no 'to'");
1177        assert_eq!(decoded.from, signer.address());
1178    }
1179
1180    #[test]
1181    fn test_decode_empty_tx_rejected() {
1182        assert!(decode_signed_tx(&[]).is_err());
1183    }
1184
1185    #[test]
1186    fn test_decode_unknown_type_rejected() {
1187        assert!(decode_signed_tx(&[0x04, 0x00]).is_err());
1188    }
1189
1190    #[test]
1191    fn test_decode_signer_matches_across_types() {
1192        // Same signer, same nonce → different tx types should all recover same address
1193        let signer = EthereumSigner::from_bytes(&[0x42; 32]).unwrap();
1194        let expected_addr = signer.address();
1195
1196        let legacy = LegacyTransaction {
1197            nonce: 0,
1198            gas_price: 1,
1199            gas_limit: 21000,
1200            to: Some([0xAA; 20]),
1201            value: 0,
1202            data: vec![],
1203            chain_id: 1,
1204        }
1205        .sign(&signer)
1206        .unwrap();
1207
1208        let type2 = EIP1559Transaction {
1209            chain_id: 1,
1210            nonce: 0,
1211            max_priority_fee_per_gas: 1,
1212            max_fee_per_gas: 1,
1213            gas_limit: 21000,
1214            to: Some([0xAA; 20]),
1215            value: 0,
1216            data: vec![],
1217            access_list: vec![],
1218        }
1219        .sign(&signer)
1220        .unwrap();
1221
1222        assert_eq!(
1223            decode_signed_tx(legacy.raw_tx()).unwrap().from,
1224            expected_addr
1225        );
1226        assert_eq!(
1227            decode_signed_tx(type2.raw_tx()).unwrap().from,
1228            expected_addr
1229        );
1230    }
1231}