alloy_zksync/network/
tx_envelope.rs

1use alloy::consensus::{Signed, Typed2718};
2use alloy::network::eip2718::{Decodable2718, Encodable2718};
3use alloy::rlp::{Encodable, Header};
4use serde::{Deserialize, Serialize};
5
6use super::tx_type::TxType;
7use super::unsigned_tx::eip712::TxEip712;
8/// Transaction envelope is a wrapper around the transaction data.
9/// See [`alloy::consensus::TxEnvelope`](https://docs.rs/alloy/latest/alloy/consensus/enum.TxEnvelope.html) for more details.
10#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(
12    into = "serde_from::TaggedTxEnvelope",
13    from = "serde_from::MaybeTaggedTxEnvelope"
14)]
15pub enum TxEnvelope {
16    /// Ethereum-native transaction.
17    Native(alloy::consensus::TxEnvelope),
18    /// ZKsync-native EIP712 transaction.
19    Eip712(Signed<TxEip712>),
20}
21
22impl TxEnvelope {
23    /// Returns true if the transaction is a legacy transaction.
24    #[inline]
25    pub const fn is_legacy(&self) -> bool {
26        match self {
27            Self::Native(inner) => inner.is_legacy(),
28            Self::Eip712(_) => false,
29        }
30    }
31
32    /// Returns true if the transaction is an EIP-2930 transaction.
33    #[inline]
34    pub const fn is_eip2930(&self) -> bool {
35        match self {
36            Self::Native(inner) => inner.is_eip2930(),
37            Self::Eip712(_) => false,
38        }
39    }
40
41    /// Returns true if the transaction is an EIP-1559 transaction.
42    #[inline]
43    pub const fn is_eip1559(&self) -> bool {
44        match self {
45            Self::Native(inner) => inner.is_eip1559(),
46            Self::Eip712(_) => false,
47        }
48    }
49
50    /// Returns true if the transaction is an EIP-4844 transaction.
51    #[inline]
52    pub const fn is_eip4844(&self) -> bool {
53        match self {
54            Self::Native(inner) => inner.is_eip4844(),
55            Self::Eip712(_) => false,
56        }
57    }
58
59    /// Returns true if the transaction is an EIP-7702 transaction.
60    #[inline]
61    pub const fn is_eip7702(&self) -> bool {
62        match self {
63            Self::Native(inner) => inner.is_eip7702(),
64            Self::Eip712(_) => false,
65        }
66    }
67
68    /// Returns true if the transaction is an EIP-712 transaction.
69    #[inline]
70    pub const fn is_eip712(&self) -> bool {
71        matches!(self, Self::Eip712(_))
72    }
73
74    /// Returns true if the transaction is replay protected.
75    ///
76    /// All non-legacy transactions are replay protected, as the chain id is
77    /// included in the transaction body. Legacy transactions are considered
78    /// replay protected if the `v` value is not 27 or 28, according to the
79    /// rules of [EIP-155].
80    ///
81    /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155
82    #[inline]
83    pub const fn is_replay_protected(&self) -> bool {
84        match self {
85            Self::Native(inner) => inner.is_replay_protected(),
86            Self::Eip712(_) => true,
87        }
88    }
89
90    /// Returns the [`TxLegacy`] variant if the transaction is a legacy transaction.
91    pub const fn as_legacy(&self) -> Option<&Signed<alloy::consensus::TxLegacy>> {
92        match self {
93            Self::Native(inner) => inner.as_legacy(),
94            Self::Eip712(_) => None,
95        }
96    }
97
98    /// Returns the [`TxEip2930`] variant if the transaction is an EIP-2930 transaction.
99    pub const fn as_eip2930(&self) -> Option<&Signed<alloy::consensus::TxEip2930>> {
100        match self {
101            Self::Native(inner) => inner.as_eip2930(),
102            Self::Eip712(_) => None,
103        }
104    }
105
106    /// Returns the [`TxEip1559`] variant if the transaction is an EIP-1559 transaction.
107    pub const fn as_eip1559(&self) -> Option<&Signed<alloy::consensus::TxEip1559>> {
108        match self {
109            Self::Native(inner) => inner.as_eip1559(),
110            Self::Eip712(_) => None,
111        }
112    }
113
114    /// Returns the [`TxEip4844`] variant if the transaction is an EIP-4844 transaction.
115    pub const fn as_eip4844(&self) -> Option<&Signed<alloy::consensus::TxEip4844Variant>> {
116        match self {
117            Self::Native(inner) => inner.as_eip4844(),
118            Self::Eip712(_) => None,
119        }
120    }
121
122    /// Returns the [`TxEip7702`] variant if the transaction is an EIP-7702 transaction.
123    pub const fn as_eip7702(&self) -> Option<&Signed<alloy::consensus::TxEip7702>> {
124        match self {
125            Self::Native(inner) => inner.as_eip7702(),
126            Self::Eip712(_) => None,
127        }
128    }
129
130    /// Returns the [`TxEip712`] variant if the transaction is an EIP-712 transaction.
131    pub const fn as_eip712(&self) -> Option<&Signed<TxEip712>> {
132        match self {
133            Self::Native(_) => None,
134            Self::Eip712(inner) => Some(inner),
135        }
136    }
137
138    /// Calculate the signing hash for the transaction.
139    pub fn signature_hash(&self) -> alloy::primitives::B256 {
140        match self {
141            Self::Native(inner) => inner.signature_hash(),
142            Self::Eip712(inner) => inner.signature_hash(),
143        }
144    }
145
146    /// Return the reference to signature.
147    pub const fn signature(&self) -> &alloy::primitives::Signature {
148        match self {
149            Self::Native(inner) => inner.signature(),
150            Self::Eip712(inner) => inner.signature(),
151        }
152    }
153
154    /// Return the hash of the inner Signed.
155    #[doc(alias = "transaction_hash")]
156    pub fn tx_hash(&self) -> &alloy::primitives::B256 {
157        match self {
158            Self::Native(inner) => inner.tx_hash(),
159            Self::Eip712(inner) => inner.hash(),
160        }
161    }
162
163    /// Return the [`TxType`] of the inner txn.
164    #[doc(alias = "transaction_type")]
165    pub const fn tx_type(&self) -> crate::network::tx_type::TxType {
166        match self {
167            Self::Native(inner) => match inner.tx_type() {
168                alloy::consensus::TxType::Legacy => crate::network::tx_type::TxType::Legacy,
169                alloy::consensus::TxType::Eip2930 => crate::network::tx_type::TxType::Eip2930,
170                alloy::consensus::TxType::Eip1559 => crate::network::tx_type::TxType::Eip1559,
171                alloy::consensus::TxType::Eip4844 => crate::network::tx_type::TxType::Eip4844,
172                alloy::consensus::TxType::Eip7702 => crate::network::tx_type::TxType::Eip7702,
173            },
174            Self::Eip712(_) => crate::network::tx_type::TxType::Eip712,
175        }
176    }
177
178    /// Return the length of the inner txn, including type byte length
179    pub fn eip2718_encoded_length(&self) -> usize {
180        match self {
181            Self::Native(inner) => inner.eip2718_encoded_length(),
182            Self::Eip712(inner) => inner.tx().encoded_length(inner.signature()),
183        }
184    }
185}
186
187impl Typed2718 for TxEnvelope {
188    fn ty(&self) -> u8 {
189        match self {
190            Self::Native(inner) => inner.ty(),
191            Self::Eip712(inner) => inner.tx().tx_type() as u8,
192        }
193    }
194}
195
196impl Encodable2718 for TxEnvelope {
197    fn type_flag(&self) -> Option<u8> {
198        match self {
199            Self::Native(inner) => inner.type_flag(),
200            Self::Eip712(inner) => Some(inner.tx().tx_type() as u8),
201        }
202    }
203
204    fn encode_2718_len(&self) -> usize {
205        match self {
206            Self::Native(inner) => inner.encode_2718_len(),
207            Self::Eip712(inner) => {
208                let payload_length = inner.tx().fields_len()
209                    + inner.signature().rlp_rs_len()
210                    + inner.signature().v().length();
211                Header {
212                    list: true,
213                    payload_length,
214                }
215                .length()
216                    + payload_length
217            }
218        }
219    }
220
221    fn encode_2718(&self, out: &mut dyn alloy::primitives::bytes::BufMut) {
222        match self {
223            Self::Native(inner) => inner.encode_2718(out),
224            Self::Eip712(tx) => {
225                tx.tx().encode_with_signature(tx.signature(), out);
226            }
227        }
228    }
229}
230
231impl Decodable2718 for TxEnvelope {
232    fn typed_decode(ty: u8, buf: &mut &[u8]) -> alloy::network::eip2718::Eip2718Result<Self> {
233        match ty {
234            _ if ty == (TxType::Eip712 as u8) => {
235                let tx = TxEip712::decode_signed_fields(buf)?;
236                Ok(Self::Eip712(tx))
237            }
238            _ => {
239                let inner = alloy::consensus::TxEnvelope::typed_decode(ty, buf)?;
240                Ok(Self::Native(inner))
241            }
242        }
243    }
244
245    fn fallback_decode(buf: &mut &[u8]) -> alloy::network::eip2718::Eip2718Result<Self> {
246        let inner = alloy::consensus::TxEnvelope::fallback_decode(buf)?;
247        Ok(Self::Native(inner))
248    }
249}
250
251impl AsRef<dyn alloy::consensus::Transaction> for TxEnvelope {
252    fn as_ref(&self) -> &dyn alloy::consensus::Transaction {
253        match self {
254            TxEnvelope::Native(inner) => inner,
255            TxEnvelope::Eip712(signed_inner) => signed_inner.tx(),
256        }
257    }
258}
259
260impl alloy::consensus::Transaction for TxEnvelope {
261    fn chain_id(&self) -> Option<alloy::primitives::ChainId> {
262        self.as_ref().chain_id()
263    }
264
265    fn nonce(&self) -> u64 {
266        self.as_ref().nonce()
267    }
268
269    fn gas_limit(&self) -> u64 {
270        self.as_ref().gas_limit()
271    }
272
273    fn gas_price(&self) -> Option<u128> {
274        self.as_ref().gas_price()
275    }
276
277    fn max_fee_per_gas(&self) -> u128 {
278        self.as_ref().max_fee_per_gas()
279    }
280
281    fn max_priority_fee_per_gas(&self) -> Option<u128> {
282        self.as_ref().max_priority_fee_per_gas()
283    }
284
285    fn max_fee_per_blob_gas(&self) -> Option<u128> {
286        self.as_ref().max_fee_per_blob_gas()
287    }
288
289    fn priority_fee_or_price(&self) -> u128 {
290        self.as_ref().priority_fee_or_price()
291    }
292
293    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
294        self.as_ref().effective_gas_price(base_fee)
295    }
296
297    fn is_dynamic_fee(&self) -> bool {
298        self.as_ref().is_dynamic_fee()
299    }
300
301    fn kind(&self) -> alloy::primitives::TxKind {
302        self.as_ref().kind()
303    }
304
305    fn is_create(&self) -> bool {
306        self.as_ref().is_create()
307    }
308
309    fn value(&self) -> alloy::primitives::U256 {
310        self.as_ref().value()
311    }
312
313    fn input(&self) -> &alloy::primitives::Bytes {
314        self.as_ref().input()
315    }
316
317    fn access_list(&self) -> Option<&alloy::rpc::types::AccessList> {
318        self.as_ref().access_list()
319    }
320
321    fn blob_versioned_hashes(&self) -> Option<&[alloy::primitives::B256]> {
322        self.as_ref().blob_versioned_hashes()
323    }
324
325    fn authorization_list(&self) -> Option<&[alloy::eips::eip7702::SignedAuthorization]> {
326        self.as_ref().authorization_list()
327    }
328}
329
330mod serde_from {
331    //! NB: Why do we need this?
332    //!
333    //! We are following the same approach as [`alloy::consensus::TxEnvelope`] but with an additional
334    //! ZKsync-specific transaction type (`type: "0x71"`).
335    //!
336    //! Because the tag may be missing, we need an abstraction over tagged (with
337    //! type) and untagged (always legacy). This is [`MaybeTaggedTxEnvelope`].
338    //!
339    //! The tagged variant is [`TaggedTxEnvelope`], which always has a type tag.
340    //!
341    //! We serialize via [`TaggedTxEnvelope`] and deserialize via
342    //! [`MaybeTaggedTxEnvelope`].
343    use crate::network::tx_envelope::TxEnvelope;
344    use crate::network::unsigned_tx::eip712::TxEip712;
345    use alloy::consensus::{Signed, TxEip1559, TxEip2930, TxEip4844Variant, TxEip7702, TxLegacy};
346
347    #[derive(Debug, serde::Deserialize)]
348    #[serde(untagged)]
349    pub(crate) enum MaybeTaggedTxEnvelope {
350        Tagged(TaggedTxEnvelope),
351        Untagged {
352            #[serde(
353                default,
354                rename = "type",
355                deserialize_with = "alloy::serde::reject_if_some"
356            )]
357            _ty: Option<()>,
358            #[serde(flatten, with = "alloy::consensus::transaction::signed_legacy_serde")]
359            tx: Signed<TxLegacy>,
360        },
361    }
362
363    #[derive(Debug, serde::Serialize, serde::Deserialize)]
364    #[serde(tag = "type")]
365    pub(crate) enum TaggedTxEnvelope {
366        // Native transaction types below
367        #[serde(
368            rename = "0x0",
369            alias = "0x00",
370            with = "alloy::consensus::transaction::signed_legacy_serde"
371        )]
372        Legacy(Signed<TxLegacy>),
373        #[serde(rename = "0x1", alias = "0x01")]
374        Eip2930(Signed<TxEip2930>),
375        #[serde(rename = "0x2", alias = "0x02")]
376        Eip1559(Signed<TxEip1559>),
377        #[serde(rename = "0x3", alias = "0x03")]
378        Eip4844(Signed<TxEip4844Variant>),
379        #[serde(rename = "0x4", alias = "0x04")]
380        Eip7702(Signed<TxEip7702>),
381        // Custom ZKsync transaction type
382        #[serde(rename = "0x71")]
383        Eip712(Signed<TxEip712>),
384    }
385
386    impl From<MaybeTaggedTxEnvelope> for TxEnvelope {
387        fn from(value: MaybeTaggedTxEnvelope) -> Self {
388            match value {
389                MaybeTaggedTxEnvelope::Tagged(tagged) => tagged.into(),
390                MaybeTaggedTxEnvelope::Untagged { tx, .. } => {
391                    Self::Native(alloy::consensus::TxEnvelope::Legacy(tx))
392                }
393            }
394        }
395    }
396
397    impl From<TaggedTxEnvelope> for TxEnvelope {
398        fn from(value: TaggedTxEnvelope) -> Self {
399            match value {
400                TaggedTxEnvelope::Legacy(signed) => {
401                    Self::Native(alloy::consensus::TxEnvelope::Legacy(signed))
402                }
403                TaggedTxEnvelope::Eip2930(signed) => {
404                    Self::Native(alloy::consensus::TxEnvelope::Eip2930(signed))
405                }
406                TaggedTxEnvelope::Eip1559(signed) => {
407                    Self::Native(alloy::consensus::TxEnvelope::Eip1559(signed))
408                }
409                TaggedTxEnvelope::Eip4844(signed) => {
410                    Self::Native(alloy::consensus::TxEnvelope::Eip4844(signed))
411                }
412                TaggedTxEnvelope::Eip7702(signed) => {
413                    Self::Native(alloy::consensus::TxEnvelope::Eip7702(signed))
414                }
415                TaggedTxEnvelope::Eip712(signed) => Self::Eip712(signed),
416            }
417        }
418    }
419
420    impl From<TxEnvelope> for TaggedTxEnvelope {
421        fn from(value: TxEnvelope) -> Self {
422            match value {
423                TxEnvelope::Native(alloy::consensus::TxEnvelope::Legacy(signed)) => {
424                    Self::Legacy(signed)
425                }
426                TxEnvelope::Native(alloy::consensus::TxEnvelope::Eip2930(signed)) => {
427                    Self::Eip2930(signed)
428                }
429                TxEnvelope::Native(alloy::consensus::TxEnvelope::Eip1559(signed)) => {
430                    Self::Eip1559(signed)
431                }
432                TxEnvelope::Native(alloy::consensus::TxEnvelope::Eip4844(signed)) => {
433                    Self::Eip4844(signed)
434                }
435                TxEnvelope::Native(alloy::consensus::TxEnvelope::Eip7702(signed)) => {
436                    Self::Eip7702(signed)
437                }
438                TxEnvelope::Eip712(signed) => Self::Eip712(signed),
439            }
440        }
441    }
442}