Skip to main content

aztec_core/
tx.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::fmt;
3
4use crate::abi::{AbiValue, FunctionSelector, FunctionType};
5use crate::constants::domain_separator;
6use crate::fee::GasSettings;
7use crate::hash::poseidon2_hash_with_separator;
8#[allow(unused_imports)]
9// Used by TypedTx; flagged unused when TypedTx has no constructors yet
10use crate::kernel_types::PrivateKernelTailPublicInputs;
11use crate::types::{decode_fixed_hex, encode_hex, AztecAddress, Fr};
12use crate::Error;
13
14/// A 32-byte transaction hash.
15#[derive(Clone, Copy, PartialEq, Eq, Hash)]
16pub struct TxHash(pub [u8; 32]);
17
18impl TxHash {
19    /// The zero hash.
20    pub const fn zero() -> Self {
21        Self([0u8; 32])
22    }
23
24    /// Parse from a hex string (e.g. `"0xdead..."`).
25    pub fn from_hex(value: &str) -> Result<Self, Error> {
26        Ok(Self(decode_fixed_hex::<32>(value)?))
27    }
28}
29
30impl fmt::Display for TxHash {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        f.write_str(&encode_hex(&self.0))
33    }
34}
35
36impl fmt::Debug for TxHash {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(f, "TxHash({self})")
39    }
40}
41
42impl Serialize for TxHash {
43    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
44    where
45        S: Serializer,
46    {
47        serializer.serialize_str(&self.to_string())
48    }
49}
50
51impl<'de> Deserialize<'de> for TxHash {
52    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
53    where
54        D: Deserializer<'de>,
55    {
56        let s = String::deserialize(deserializer)?;
57        Self::from_hex(&s).map_err(serde::de::Error::custom)
58    }
59}
60
61/// Transaction lifecycle status.
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "lowercase")]
64pub enum TxStatus {
65    /// Transaction was dropped from the mempool.
66    Dropped,
67    /// Transaction is pending in the mempool.
68    Pending,
69    /// Transaction has been proposed in a block.
70    Proposed,
71    /// Transaction's block has been checkpointed to L1.
72    Checkpointed,
73    /// Transaction's block has been proven.
74    Proven,
75    /// Transaction's block has been finalized on L1.
76    Finalized,
77}
78
79/// Outcome of transaction execution within a block.
80#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(rename_all = "snake_case")]
82pub enum TxExecutionResult {
83    /// All phases executed successfully.
84    Success,
85    /// The app logic phase reverted.
86    AppLogicReverted,
87    /// The teardown phase reverted.
88    TeardownReverted,
89    /// Both app logic and teardown phases reverted.
90    BothReverted,
91}
92
93/// A transaction receipt returned by the node.
94#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct TxReceipt {
97    /// Hash of the transaction.
98    pub tx_hash: TxHash,
99    /// Current lifecycle status.
100    pub status: TxStatus,
101    /// Execution outcome (present once the tx has been included in a block).
102    pub execution_result: Option<TxExecutionResult>,
103    /// Error message if the transaction failed.
104    pub error: Option<String>,
105    /// Total fee paid for the transaction.
106    #[serde(default, deserialize_with = "option_u128_from_string_or_number")]
107    pub transaction_fee: Option<u128>,
108    /// Hash of the block containing this transaction.
109    #[serde(default, with = "option_hex_bytes_32")]
110    pub block_hash: Option<[u8; 32]>,
111    /// Block number containing this transaction.
112    pub block_number: Option<u64>,
113    /// Epoch number containing this transaction.
114    pub epoch_number: Option<u64>,
115}
116
117mod option_hex_bytes_32 {
118    use serde::{Deserialize, Deserializer, Serializer};
119
120    use crate::types::{decode_fixed_hex, encode_hex};
121
122    #[allow(clippy::ref_option)]
123    pub fn serialize<S>(value: &Option<[u8; 32]>, serializer: S) -> Result<S::Ok, S::Error>
124    where
125        S: Serializer,
126    {
127        match value {
128            Some(bytes) => serializer.serialize_some(&encode_hex(bytes)),
129            None => serializer.serialize_none(),
130        }
131    }
132
133    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<[u8; 32]>, D::Error>
134    where
135        D: Deserializer<'de>,
136    {
137        let opt: Option<String> = Option::deserialize(deserializer)?;
138        match opt {
139            Some(s) => {
140                let bytes = decode_fixed_hex::<32>(&s).map_err(serde::de::Error::custom)?;
141                Ok(Some(bytes))
142            }
143            None => Ok(None),
144        }
145    }
146}
147
148fn option_u128_from_string_or_number<'de, D>(deserializer: D) -> Result<Option<u128>, D::Error>
149where
150    D: serde::Deserializer<'de>,
151{
152    let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
153    match value {
154        None | Some(serde_json::Value::Null) => Ok(None),
155        Some(serde_json::Value::Number(n)) => n
156            .as_u64()
157            .map(|v| Some(v as u128))
158            .ok_or_else(|| serde::de::Error::custom("invalid numeric transactionFee")),
159        Some(serde_json::Value::String(s)) => s
160            .parse::<u128>()
161            .map(Some)
162            .map_err(serde::de::Error::custom),
163        Some(other) => Err(serde::de::Error::custom(format!(
164            "invalid transactionFee value: {other}"
165        ))),
166    }
167}
168
169mod base64_buffer {
170    use base64::Engine as _;
171    use serde::{Deserialize, Deserializer, Serializer};
172
173    pub fn serialize<S>(value: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
174    where
175        S: Serializer,
176    {
177        serializer.serialize_str(&base64::engine::general_purpose::STANDARD.encode(value))
178    }
179
180    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
181    where
182        D: Deserializer<'de>,
183    {
184        let encoded = String::deserialize(deserializer)?;
185        base64::engine::general_purpose::STANDARD
186            .decode(encoded)
187            .map_err(serde::de::Error::custom)
188    }
189}
190
191impl TxReceipt {
192    /// Returns `true` if the transaction has been included in a block.
193    pub const fn is_mined(&self) -> bool {
194        matches!(
195            self.status,
196            TxStatus::Proposed | TxStatus::Checkpointed | TxStatus::Proven | TxStatus::Finalized
197        )
198    }
199
200    /// Returns `true` if the transaction is pending in the mempool.
201    pub fn is_pending(&self) -> bool {
202        self.status == TxStatus::Pending
203    }
204
205    /// Returns `true` if the transaction was dropped from the mempool.
206    pub fn is_dropped(&self) -> bool {
207        self.status == TxStatus::Dropped
208    }
209
210    /// Returns `true` if execution completed successfully.
211    pub fn has_execution_succeeded(&self) -> bool {
212        self.execution_result == Some(TxExecutionResult::Success)
213    }
214
215    /// Returns `true` if any execution phase reverted.
216    pub fn has_execution_reverted(&self) -> bool {
217        self.execution_result.is_some() && !self.has_execution_succeeded()
218    }
219}
220
221/// A single function call to a contract.
222#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
223pub struct FunctionCall {
224    /// Target contract address.
225    pub to: AztecAddress,
226    /// Function selector identifying the function to call.
227    pub selector: FunctionSelector,
228    /// Encoded function arguments.
229    pub args: Vec<AbiValue>,
230    /// The type of function being called.
231    pub function_type: FunctionType,
232    /// Whether this is a static (read-only) call.
233    pub is_static: bool,
234    /// Whether the msg_sender should be hidden from the callee.
235    #[serde(default)]
236    pub hide_msg_sender: bool,
237}
238
239impl FunctionCall {
240    /// The canonical empty function call, used for padding entrypoint payloads.
241    pub fn empty() -> Self {
242        Self {
243            to: AztecAddress::zero(),
244            selector: FunctionSelector::empty(),
245            args: vec![],
246            function_type: FunctionType::Private,
247            is_static: false,
248            hide_msg_sender: false,
249        }
250    }
251
252    /// Returns `true` if this is the canonical empty call.
253    pub fn is_empty(&self) -> bool {
254        self.to == AztecAddress::zero() && self.selector == FunctionSelector::empty()
255    }
256}
257
258/// An authorization witness proving the caller's intent.
259#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
260pub struct AuthWitness {
261    /// The message hash this witness authorizes.
262    #[serde(default)]
263    pub request_hash: Fr,
264    /// Field elements comprising the witness data.
265    #[serde(default)]
266    pub fields: Vec<Fr>,
267}
268
269/// Private data capsule passed alongside a transaction.
270///
271/// Structured capsule with contract address, storage slot, and field data.
272/// Used for passing auxiliary data (e.g., packed bytecode) to protocol contracts.
273#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
274pub struct Capsule {
275    /// The contract address this capsule targets.
276    pub contract_address: AztecAddress,
277    /// The storage slot within the target contract.
278    pub storage_slot: Fr,
279    /// Capsule data as field elements.
280    pub data: Vec<Fr>,
281}
282
283/// Transaction context.
284///
285/// Carries the replay-protection metadata and gas settings used to build a
286/// transaction execution request.
287#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct TxContext {
290    /// L1 chain ID used for replay protection.
291    pub chain_id: Fr,
292    /// Rollup protocol version used for replay protection.
293    pub version: Fr,
294    /// Gas settings for the transaction.
295    pub gas_settings: GasSettings,
296}
297
298impl TxContext {
299    /// Flatten into field elements using the upstream stdlib ordering.
300    pub fn to_fields(&self) -> Vec<Fr> {
301        let mut fields = Vec::with_capacity(10);
302        fields.push(self.chain_id);
303        fields.push(self.version);
304
305        let gas_limits = self.gas_settings.gas_limits.clone().unwrap_or_default();
306        fields.push(Fr::from(gas_limits.da_gas));
307        fields.push(Fr::from(gas_limits.l2_gas));
308
309        let teardown = self
310            .gas_settings
311            .teardown_gas_limits
312            .clone()
313            .unwrap_or_default();
314        fields.push(Fr::from(teardown.da_gas));
315        fields.push(Fr::from(teardown.l2_gas));
316
317        let max_fee = self
318            .gas_settings
319            .max_fee_per_gas
320            .clone()
321            .unwrap_or_default();
322        fields.push(Fr::from(max_fee.fee_per_da_gas));
323        fields.push(Fr::from(max_fee.fee_per_l2_gas));
324
325        let max_priority = self
326            .gas_settings
327            .max_priority_fee_per_gas
328            .clone()
329            .unwrap_or_default();
330        fields.push(Fr::from(max_priority.fee_per_da_gas));
331        fields.push(Fr::from(max_priority.fee_per_l2_gas));
332
333        fields
334    }
335}
336
337/// Compute the canonical tx-request hash used for the protocol nullifier.
338pub fn compute_tx_request_hash(
339    origin: AztecAddress,
340    args_hash: Fr,
341    tx_context: &TxContext,
342    function_selector: FunctionSelector,
343    is_private: bool,
344    salt: Fr,
345) -> Fr {
346    // Field order matches TS: [origin, argsHash, txContext, functionData(selector, isPrivate), salt]
347    let mut fields = Vec::with_capacity(15);
348    fields.push(origin.0);
349    fields.push(args_hash);
350    fields.extend(tx_context.to_fields());
351    fields.push(function_selector.to_field());
352    fields.push(Fr::from(is_private));
353    fields.push(salt);
354    poseidon2_hash_with_separator(&fields, domain_separator::TX_REQUEST)
355}
356
357/// Pre-hashed values included in a transaction for oracle access.
358#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
359#[serde(rename_all = "camelCase")]
360pub struct HashedValues {
361    /// Field elements to be hashed.
362    #[serde(default)]
363    pub values: Vec<Fr>,
364    /// Pre-computed hash of `values`.
365    #[serde(default)]
366    pub hash: Fr,
367}
368
369impl HashedValues {
370    /// Create hashed values from raw argument fields.
371    pub fn from_args(args: Vec<Fr>) -> Self {
372        let hash = crate::hash::compute_var_args_hash(&args);
373        Self { values: args, hash }
374    }
375
376    /// Create hashed values from calldata (selector + args for public calls).
377    pub fn from_calldata(calldata: Vec<Fr>) -> Self {
378        let hash = crate::hash::compute_calldata_hash(&calldata);
379        Self {
380            values: calldata,
381            hash,
382        }
383    }
384
385    /// Return the stored hash of the contained values.
386    pub fn hash(&self) -> Fr {
387        self.hash
388    }
389}
390
391/// Preimage fields for a contract class log carried by a transaction.
392#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
393pub struct ContractClassLogFields {
394    /// Fixed-width field array serialized through JSON-RPC.
395    #[serde(default)]
396    pub fields: Vec<Fr>,
397}
398
399impl ContractClassLogFields {
400    /// Construct from already-emitted fields, padding to the protocol width.
401    pub fn from_emitted_fields(mut emitted_fields: Vec<Fr>) -> Self {
402        const CONTRACT_CLASS_LOG_SIZE_IN_FIELDS: usize = 3023;
403        if emitted_fields.len() < CONTRACT_CLASS_LOG_SIZE_IN_FIELDS {
404            emitted_fields.resize(CONTRACT_CLASS_LOG_SIZE_IN_FIELDS, Fr::zero());
405        }
406        Self {
407            fields: emitted_fields,
408        }
409    }
410
411    /// Returns the prefix of non-empty emitted fields.
412    pub fn emitted_fields(&self) -> &[Fr] {
413        let last = self.fields.iter().rposition(|field| *field != Fr::zero());
414        match last {
415            Some(index) => &self.fields[..=index],
416            None => &[],
417        }
418    }
419}
420
421/// Serialized private-kernel tail public inputs.
422///
423/// Upstream stdlib encodes this as a buffer over JSON-RPC, so Rust stores the
424/// already-serialized bytes and emits them as base64.
425#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
426#[serde(transparent)]
427pub struct PrivateKernelTailCircuitPublicInputs {
428    /// Serialized stdlib buffer bytes.
429    #[serde(with = "base64_buffer")]
430    pub bytes: Vec<u8>,
431}
432
433impl PrivateKernelTailCircuitPublicInputs {
434    /// Create from raw serialized bytes.
435    pub fn from_bytes(bytes: Vec<u8>) -> Self {
436        Self { bytes }
437    }
438}
439
440/// Serialized Chonk proof.
441///
442/// Upstream stdlib also transports this as a buffer over JSON-RPC.
443#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
444#[serde(transparent)]
445pub struct ChonkProof {
446    /// Serialized stdlib buffer bytes.
447    #[serde(with = "base64_buffer")]
448    pub bytes: Vec<u8>,
449}
450
451impl ChonkProof {
452    /// Create from raw serialized bytes.
453    pub fn from_bytes(bytes: Vec<u8>) -> Self {
454        Self { bytes }
455    }
456}
457
458/// Node-facing transaction envelope matching the upstream stdlib `Tx` JSON shape.
459#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
460#[serde(rename_all = "camelCase")]
461pub struct Tx {
462    /// Private kernel tail public inputs, serialized as a stdlib buffer.
463    pub data: PrivateKernelTailCircuitPublicInputs,
464    /// Chonk proof, serialized as a stdlib buffer.
465    pub chonk_proof: ChonkProof,
466    /// Contract-class log preimages corresponding to the log hashes in `data`.
467    #[serde(default)]
468    pub contract_class_log_fields: Vec<ContractClassLogFields>,
469    /// Calldata preimages for enqueued public calls.
470    #[serde(default)]
471    pub public_function_calldata: Vec<HashedValues>,
472}
473
474impl Tx {
475    /// Convert to a JSON-RPC payload accepted by the node.
476    pub fn to_json_value(&self) -> Result<serde_json::Value, Error> {
477        serde_json::to_value(self).map_err(Error::from)
478    }
479}
480
481/// A typed transaction with parsed public inputs, for validation and inspection.
482///
483/// This carries the same wire data as [`Tx`] but with the kernel public inputs
484/// deserialized into proper types for programmatic access and validation.
485#[derive(Clone, Debug, PartialEq, Eq)]
486pub struct TypedTx {
487    /// Transaction hash computed from public inputs.
488    pub tx_hash: TxHash,
489    /// Typed private kernel tail circuit public inputs.
490    pub data: PrivateKernelTailPublicInputs,
491    /// Chonk proof.
492    pub chonk_proof: ChonkProof,
493    /// Contract class log preimage fields.
494    pub contract_class_log_fields: Vec<ContractClassLogFields>,
495    /// Calldata preimages for enqueued public calls.
496    pub public_function_calldata: Vec<HashedValues>,
497}
498
499impl TypedTx {
500    /// Number of enqueued public calls in this transaction.
501    pub fn number_of_public_calls(&self) -> usize {
502        self.data.number_of_public_calls()
503    }
504
505    /// Total calldata field count across all public calls.
506    pub fn get_total_public_calldata_count(&self) -> usize {
507        self.public_function_calldata
508            .iter()
509            .map(|hv| hv.values.len())
510            .sum()
511    }
512
513    /// Get all public call requests paired with their calldata.
514    pub fn get_public_call_requests_with_calldata(
515        &self,
516    ) -> Vec<(&crate::kernel_types::PublicCallRequest, &HashedValues)> {
517        let requests = self.data.get_all_public_call_requests();
518        requests
519            .into_iter()
520            .zip(self.public_function_calldata.iter())
521            .collect()
522    }
523}
524
525/// A complete transaction execution payload.
526///
527/// Groups one or more [`FunctionCall`]s with their associated auth witnesses,
528/// capsules, hashed args, and an optional fee payer override.
529#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
530pub struct ExecutionPayload {
531    /// Function calls to execute.
532    #[serde(default)]
533    pub calls: Vec<FunctionCall>,
534    /// Authorization witnesses for the calls.
535    #[serde(default)]
536    pub auth_witnesses: Vec<AuthWitness>,
537    /// Private data capsules.
538    #[serde(default)]
539    pub capsules: Vec<Capsule>,
540    /// Extra hashed arguments for oracle access.
541    #[serde(default)]
542    pub extra_hashed_args: Vec<HashedValues>,
543    /// Override the fee payer for this payload.
544    pub fee_payer: Option<AztecAddress>,
545}
546
547impl ExecutionPayload {
548    /// Merge multiple execution payloads into a single payload.
549    ///
550    /// Combines all calls, auth witnesses, capsules, and hashed args.
551    /// If multiple payloads specify a `fee_payer`, they must all agree
552    /// on the same address — otherwise this returns an error.
553    pub fn merge(payloads: Vec<ExecutionPayload>) -> Result<Self, Error> {
554        let mut calls = Vec::new();
555        let mut auth_witnesses = Vec::new();
556        let mut capsules = Vec::new();
557        let mut extra_hashed_args = Vec::new();
558        let mut fee_payer: Option<AztecAddress> = None;
559
560        for payload in payloads {
561            calls.extend(payload.calls);
562            auth_witnesses.extend(payload.auth_witnesses);
563            capsules.extend(payload.capsules);
564            extra_hashed_args.extend(payload.extra_hashed_args);
565
566            if let Some(payer) = payload.fee_payer {
567                if let Some(existing) = fee_payer {
568                    if existing != payer {
569                        return Err(Error::InvalidData(format!(
570                            "conflicting fee payers: {existing} vs {payer}"
571                        )));
572                    }
573                }
574                fee_payer = Some(payer);
575            }
576        }
577
578        Ok(ExecutionPayload {
579            calls,
580            auth_witnesses,
581            capsules,
582            extra_hashed_args,
583            fee_payer,
584        })
585    }
586}
587
588#[cfg(test)]
589#[allow(clippy::expect_used, clippy::panic)]
590mod tests {
591    use super::*;
592
593    fn make_receipt(status: TxStatus, exec: Option<TxExecutionResult>) -> TxReceipt {
594        TxReceipt {
595            tx_hash: TxHash::zero(),
596            status,
597            execution_result: exec,
598            error: None,
599            transaction_fee: None,
600            block_hash: None,
601            block_number: None,
602            epoch_number: None,
603        }
604    }
605
606    #[test]
607    fn tx_hash_hex_roundtrip() {
608        let hash = TxHash([0xab; 32]);
609        let json = serde_json::to_string(&hash).expect("serialize TxHash");
610        assert!(json.contains("0xabab"), "should serialize as hex string");
611        let decoded: TxHash = serde_json::from_str(&json).expect("deserialize TxHash");
612        assert_eq!(decoded, hash);
613    }
614
615    #[test]
616    fn tx_hash_from_hex() {
617        let hash =
618            TxHash::from_hex("0x0000000000000000000000000000000000000000000000000000000000000001")
619                .expect("valid hex");
620        assert_eq!(hash.0[31], 1);
621        assert_eq!(hash.0[0], 0);
622    }
623
624    #[test]
625    fn tx_hash_display() {
626        let hash = TxHash::zero();
627        let s = hash.to_string();
628        assert_eq!(
629            s,
630            "0x0000000000000000000000000000000000000000000000000000000000000000"
631        );
632    }
633
634    #[test]
635    fn tx_status_roundtrip() {
636        let statuses = [
637            (TxStatus::Dropped, "\"dropped\""),
638            (TxStatus::Pending, "\"pending\""),
639            (TxStatus::Proposed, "\"proposed\""),
640            (TxStatus::Checkpointed, "\"checkpointed\""),
641            (TxStatus::Proven, "\"proven\""),
642            (TxStatus::Finalized, "\"finalized\""),
643        ];
644
645        for (status, expected_json) in statuses {
646            let json = serde_json::to_string(&status).expect("serialize TxStatus");
647            assert_eq!(json, expected_json);
648            let decoded: TxStatus = serde_json::from_str(&json).expect("deserialize TxStatus");
649            assert_eq!(decoded, status);
650        }
651    }
652
653    #[test]
654    fn tx_execution_result_roundtrip() {
655        let results = [
656            TxExecutionResult::Success,
657            TxExecutionResult::AppLogicReverted,
658            TxExecutionResult::TeardownReverted,
659            TxExecutionResult::BothReverted,
660        ];
661
662        for result in results {
663            let json = serde_json::to_string(&result).expect("serialize TxExecutionResult");
664            let decoded: TxExecutionResult =
665                serde_json::from_str(&json).expect("deserialize TxExecutionResult");
666            assert_eq!(decoded, result);
667        }
668    }
669
670    #[test]
671    fn receipt_mined_success() {
672        let receipt = TxReceipt {
673            tx_hash: TxHash::zero(),
674            status: TxStatus::Checkpointed,
675            execution_result: Some(TxExecutionResult::Success),
676            error: None,
677            transaction_fee: Some(1000),
678            block_hash: Some([0x11; 32]),
679            block_number: Some(42),
680            epoch_number: Some(1),
681        };
682
683        assert!(receipt.is_mined());
684        assert!(!receipt.is_pending());
685        assert!(!receipt.is_dropped());
686        assert!(receipt.has_execution_succeeded());
687        assert!(!receipt.has_execution_reverted());
688    }
689
690    #[test]
691    fn receipt_pending() {
692        let receipt = make_receipt(TxStatus::Pending, None);
693        assert!(!receipt.is_mined());
694        assert!(receipt.is_pending());
695        assert!(!receipt.is_dropped());
696        assert!(!receipt.has_execution_succeeded());
697        assert!(!receipt.has_execution_reverted());
698    }
699
700    #[test]
701    fn receipt_dropped() {
702        let receipt = make_receipt(TxStatus::Dropped, None);
703        assert!(!receipt.is_mined());
704        assert!(!receipt.is_pending());
705        assert!(receipt.is_dropped());
706    }
707
708    #[test]
709    fn receipt_reverted() {
710        let receipt = make_receipt(
711            TxStatus::Checkpointed,
712            Some(TxExecutionResult::AppLogicReverted),
713        );
714        assert!(receipt.is_mined());
715        assert!(!receipt.has_execution_succeeded());
716        assert!(receipt.has_execution_reverted());
717    }
718
719    #[test]
720    fn receipt_both_reverted() {
721        let receipt = make_receipt(
722            TxStatus::Checkpointed,
723            Some(TxExecutionResult::BothReverted),
724        );
725        assert!(receipt.has_execution_reverted());
726    }
727
728    #[test]
729    fn receipt_all_mined_statuses() {
730        for status in [
731            TxStatus::Proposed,
732            TxStatus::Checkpointed,
733            TxStatus::Proven,
734            TxStatus::Finalized,
735        ] {
736            let receipt = make_receipt(status, Some(TxExecutionResult::Success));
737            assert!(receipt.is_mined(), "{status:?} should count as mined");
738        }
739    }
740
741    #[test]
742    fn receipt_json_roundtrip() {
743        let receipt = TxReceipt {
744            tx_hash: TxHash::from_hex(
745                "0x00000000000000000000000000000000000000000000000000000000deadbeef",
746            )
747            .expect("valid hex"),
748            status: TxStatus::Finalized,
749            execution_result: Some(TxExecutionResult::Success),
750            error: None,
751            transaction_fee: Some(5000),
752            block_hash: Some([0xcc; 32]),
753            block_number: Some(100),
754            epoch_number: Some(10),
755        };
756
757        let json = serde_json::to_string(&receipt).expect("serialize receipt");
758        assert!(json.contains("deadbeef"), "tx_hash should be hex");
759        assert!(json.contains("0xcc"), "block_hash should be hex");
760
761        let decoded: TxReceipt = serde_json::from_str(&json).expect("deserialize receipt");
762        assert_eq!(decoded, receipt);
763    }
764
765    #[test]
766    fn receipt_json_roundtrip_with_nulls() {
767        let receipt = TxReceipt {
768            tx_hash: TxHash::zero(),
769            status: TxStatus::Pending,
770            execution_result: None,
771            error: None,
772            transaction_fee: None,
773            block_hash: None,
774            block_number: None,
775            epoch_number: None,
776        };
777
778        let json = serde_json::to_string(&receipt).expect("serialize receipt");
779        let decoded: TxReceipt = serde_json::from_str(&json).expect("deserialize receipt");
780        assert_eq!(decoded, receipt);
781    }
782
783    #[test]
784    fn payload_serializes() {
785        let payload = ExecutionPayload::default();
786        let json = serde_json::to_string(&payload).expect("serialize ExecutionPayload");
787        assert!(json.contains("\"calls\":[]"));
788    }
789
790    #[test]
791    fn merge_empty_payloads() {
792        let result = ExecutionPayload::merge(vec![]).expect("merge empty");
793        assert!(result.calls.is_empty());
794        assert!(result.auth_witnesses.is_empty());
795        assert!(result.capsules.is_empty());
796        assert!(result.extra_hashed_args.is_empty());
797        assert!(result.fee_payer.is_none());
798    }
799
800    #[test]
801    fn merge_single_payload() {
802        let payer = AztecAddress(Fr::from(1u64));
803        let payload = ExecutionPayload {
804            calls: vec![FunctionCall {
805                to: AztecAddress(Fr::from(2u64)),
806                selector: FunctionSelector::from_hex("0x11223344").expect("valid"),
807                args: vec![],
808                function_type: FunctionType::Private,
809                is_static: false,
810                hide_msg_sender: false,
811            }],
812            auth_witnesses: vec![AuthWitness {
813                fields: vec![Fr::from(9u64)],
814                ..Default::default()
815            }],
816            capsules: vec![],
817            extra_hashed_args: vec![],
818            fee_payer: Some(payer),
819        };
820
821        let merged = ExecutionPayload::merge(vec![payload]).expect("merge single");
822        assert_eq!(merged.calls.len(), 1);
823        assert_eq!(merged.fee_payer, Some(payer));
824    }
825
826    #[test]
827    fn merge_concatenates_fields() {
828        let p1 = ExecutionPayload {
829            calls: vec![FunctionCall {
830                to: AztecAddress(Fr::from(1u64)),
831                selector: FunctionSelector::from_hex("0x11111111").expect("valid"),
832                args: vec![],
833                function_type: FunctionType::Private,
834                is_static: false,
835                hide_msg_sender: false,
836            }],
837            auth_witnesses: vec![AuthWitness {
838                fields: vec![Fr::from(1u64)],
839                ..Default::default()
840            }],
841            capsules: vec![],
842            extra_hashed_args: vec![],
843            fee_payer: None,
844        };
845
846        let p2 = ExecutionPayload {
847            calls: vec![FunctionCall {
848                to: AztecAddress(Fr::from(2u64)),
849                selector: FunctionSelector::from_hex("0x22222222").expect("valid"),
850                args: vec![],
851                function_type: FunctionType::Public,
852                is_static: false,
853                hide_msg_sender: false,
854            }],
855            auth_witnesses: vec![AuthWitness {
856                fields: vec![Fr::from(2u64)],
857                ..Default::default()
858            }],
859            capsules: vec![],
860            extra_hashed_args: vec![],
861            fee_payer: None,
862        };
863
864        let merged = ExecutionPayload::merge(vec![p1, p2]).expect("merge two");
865        assert_eq!(merged.calls.len(), 2);
866        assert_eq!(merged.auth_witnesses.len(), 2);
867        assert!(merged.fee_payer.is_none());
868    }
869
870    #[test]
871    fn merge_same_fee_payer_succeeds() {
872        let payer = AztecAddress(Fr::from(5u64));
873        let p1 = ExecutionPayload {
874            fee_payer: Some(payer),
875            ..Default::default()
876        };
877        let p2 = ExecutionPayload {
878            fee_payer: Some(payer),
879            ..Default::default()
880        };
881
882        let merged = ExecutionPayload::merge(vec![p1, p2]).expect("same payer");
883        assert_eq!(merged.fee_payer, Some(payer));
884    }
885
886    #[test]
887    fn merge_conflicting_fee_payer_errors() {
888        let p1 = ExecutionPayload {
889            fee_payer: Some(AztecAddress(Fr::from(1u64))),
890            ..Default::default()
891        };
892        let p2 = ExecutionPayload {
893            fee_payer: Some(AztecAddress(Fr::from(2u64))),
894            ..Default::default()
895        };
896
897        let result = ExecutionPayload::merge(vec![p1, p2]);
898        assert!(result.is_err());
899    }
900
901    #[test]
902    fn merge_mixed_fee_payer_takes_defined() {
903        let payer = AztecAddress(Fr::from(3u64));
904        let p1 = ExecutionPayload {
905            fee_payer: None,
906            ..Default::default()
907        };
908        let p2 = ExecutionPayload {
909            fee_payer: Some(payer),
910            ..Default::default()
911        };
912
913        let merged = ExecutionPayload::merge(vec![p1, p2]).expect("mixed payer");
914        assert_eq!(merged.fee_payer, Some(payer));
915    }
916
917    #[test]
918    fn payload_with_calls_roundtrip() {
919        let payload = ExecutionPayload {
920            calls: vec![FunctionCall {
921                to: AztecAddress(Fr::from(1u64)),
922                selector: crate::abi::FunctionSelector::from_hex("0xaabbccdd")
923                    .expect("valid selector"),
924                args: vec![AbiValue::Field(Fr::from(42u64))],
925                function_type: FunctionType::Private,
926                is_static: false,
927                hide_msg_sender: false,
928            }],
929            auth_witnesses: vec![AuthWitness {
930                fields: vec![Fr::from(1u64)],
931                ..Default::default()
932            }],
933            capsules: vec![],
934            extra_hashed_args: vec![],
935            fee_payer: Some(AztecAddress(Fr::from(99u64))),
936        };
937
938        let json = serde_json::to_string(&payload).expect("serialize payload");
939        let decoded: ExecutionPayload = serde_json::from_str(&json).expect("deserialize payload");
940        assert_eq!(decoded, payload);
941    }
942}