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::types::{decode_fixed_hex, encode_hex, AztecAddress, Fr};
6use crate::Error;
7
8/// A 32-byte transaction hash.
9#[derive(Clone, Copy, PartialEq, Eq, Hash)]
10pub struct TxHash(pub [u8; 32]);
11
12impl TxHash {
13    /// The zero hash.
14    pub const fn zero() -> Self {
15        Self([0u8; 32])
16    }
17
18    /// Parse from a hex string (e.g. `"0xdead..."`).
19    pub fn from_hex(value: &str) -> Result<Self, Error> {
20        Ok(Self(decode_fixed_hex::<32>(value)?))
21    }
22}
23
24impl fmt::Display for TxHash {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        f.write_str(&encode_hex(&self.0))
27    }
28}
29
30impl fmt::Debug for TxHash {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        write!(f, "TxHash({self})")
33    }
34}
35
36impl Serialize for TxHash {
37    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
38    where
39        S: Serializer,
40    {
41        serializer.serialize_str(&self.to_string())
42    }
43}
44
45impl<'de> Deserialize<'de> for TxHash {
46    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
47    where
48        D: Deserializer<'de>,
49    {
50        let s = String::deserialize(deserializer)?;
51        Self::from_hex(&s).map_err(serde::de::Error::custom)
52    }
53}
54
55/// Transaction lifecycle status.
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "lowercase")]
58pub enum TxStatus {
59    /// Transaction was dropped from the mempool.
60    Dropped,
61    /// Transaction is pending in the mempool.
62    Pending,
63    /// Transaction has been proposed in a block.
64    Proposed,
65    /// Transaction's block has been checkpointed to L1.
66    Checkpointed,
67    /// Transaction's block has been proven.
68    Proven,
69    /// Transaction's block has been finalized on L1.
70    Finalized,
71}
72
73/// Outcome of transaction execution within a block.
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum TxExecutionResult {
77    /// All phases executed successfully.
78    Success,
79    /// The app logic phase reverted.
80    AppLogicReverted,
81    /// The teardown phase reverted.
82    TeardownReverted,
83    /// Both app logic and teardown phases reverted.
84    BothReverted,
85}
86
87/// A transaction receipt returned by the node.
88#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
89pub struct TxReceipt {
90    /// Hash of the transaction.
91    pub tx_hash: TxHash,
92    /// Current lifecycle status.
93    pub status: TxStatus,
94    /// Execution outcome (present once the tx has been included in a block).
95    pub execution_result: Option<TxExecutionResult>,
96    /// Error message if the transaction failed.
97    pub error: Option<String>,
98    /// Total fee paid for the transaction.
99    pub transaction_fee: Option<u128>,
100    /// Hash of the block containing this transaction.
101    #[serde(default, with = "option_hex_bytes_32")]
102    pub block_hash: Option<[u8; 32]>,
103    /// Block number containing this transaction.
104    pub block_number: Option<u64>,
105    /// Epoch number containing this transaction.
106    pub epoch_number: Option<u64>,
107}
108
109mod option_hex_bytes_32 {
110    use serde::{Deserialize, Deserializer, Serializer};
111
112    use crate::types::{decode_fixed_hex, encode_hex};
113
114    #[allow(clippy::ref_option)]
115    pub fn serialize<S>(value: &Option<[u8; 32]>, serializer: S) -> Result<S::Ok, S::Error>
116    where
117        S: Serializer,
118    {
119        match value {
120            Some(bytes) => serializer.serialize_some(&encode_hex(bytes)),
121            None => serializer.serialize_none(),
122        }
123    }
124
125    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<[u8; 32]>, D::Error>
126    where
127        D: Deserializer<'de>,
128    {
129        let opt: Option<String> = Option::deserialize(deserializer)?;
130        match opt {
131            Some(s) => {
132                let bytes = decode_fixed_hex::<32>(&s).map_err(serde::de::Error::custom)?;
133                Ok(Some(bytes))
134            }
135            None => Ok(None),
136        }
137    }
138}
139
140impl TxReceipt {
141    /// Returns `true` if the transaction has been included in a block.
142    pub const fn is_mined(&self) -> bool {
143        matches!(
144            self.status,
145            TxStatus::Proposed | TxStatus::Checkpointed | TxStatus::Proven | TxStatus::Finalized
146        )
147    }
148
149    /// Returns `true` if the transaction is pending in the mempool.
150    pub fn is_pending(&self) -> bool {
151        self.status == TxStatus::Pending
152    }
153
154    /// Returns `true` if the transaction was dropped from the mempool.
155    pub fn is_dropped(&self) -> bool {
156        self.status == TxStatus::Dropped
157    }
158
159    /// Returns `true` if execution completed successfully.
160    pub fn has_execution_succeeded(&self) -> bool {
161        self.execution_result == Some(TxExecutionResult::Success)
162    }
163
164    /// Returns `true` if any execution phase reverted.
165    pub fn has_execution_reverted(&self) -> bool {
166        self.execution_result.is_some() && !self.has_execution_succeeded()
167    }
168}
169
170/// A single function call to a contract.
171#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
172pub struct FunctionCall {
173    /// Target contract address.
174    pub to: AztecAddress,
175    /// Function selector identifying the function to call.
176    pub selector: FunctionSelector,
177    /// Encoded function arguments.
178    pub args: Vec<AbiValue>,
179    /// The type of function being called.
180    pub function_type: FunctionType,
181    /// Whether this is a static (read-only) call.
182    pub is_static: bool,
183}
184
185/// An authorization witness proving the caller's intent.
186#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
187pub struct AuthWitness {
188    /// The message hash this witness authorizes.
189    #[serde(default)]
190    pub request_hash: Fr,
191    /// Field elements comprising the witness data.
192    #[serde(default)]
193    pub fields: Vec<Fr>,
194}
195
196/// Private data capsule passed alongside a transaction.
197///
198/// Structured capsule with contract address, storage slot, and field data.
199/// Used for passing auxiliary data (e.g., packed bytecode) to protocol contracts.
200#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
201pub struct Capsule {
202    /// The contract address this capsule targets.
203    pub contract_address: AztecAddress,
204    /// The storage slot within the target contract.
205    pub storage_slot: Fr,
206    /// Capsule data as field elements.
207    pub data: Vec<Fr>,
208}
209
210/// Pre-hashed values included in a transaction for oracle access.
211#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
212pub struct HashedValues {
213    /// Field elements to be hashed.
214    #[serde(default)]
215    pub values: Vec<Fr>,
216}
217
218/// A complete transaction execution payload.
219///
220/// Groups one or more [`FunctionCall`]s with their associated auth witnesses,
221/// capsules, hashed args, and an optional fee payer override.
222#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
223pub struct ExecutionPayload {
224    /// Function calls to execute.
225    #[serde(default)]
226    pub calls: Vec<FunctionCall>,
227    /// Authorization witnesses for the calls.
228    #[serde(default)]
229    pub auth_witnesses: Vec<AuthWitness>,
230    /// Private data capsules.
231    #[serde(default)]
232    pub capsules: Vec<Capsule>,
233    /// Extra hashed arguments for oracle access.
234    #[serde(default)]
235    pub extra_hashed_args: Vec<HashedValues>,
236    /// Override the fee payer for this payload.
237    pub fee_payer: Option<AztecAddress>,
238}
239
240impl ExecutionPayload {
241    /// Merge multiple execution payloads into a single payload.
242    ///
243    /// Combines all calls, auth witnesses, capsules, and hashed args.
244    /// If multiple payloads specify a `fee_payer`, they must all agree
245    /// on the same address — otherwise this returns an error.
246    pub fn merge(payloads: Vec<ExecutionPayload>) -> Result<Self, Error> {
247        let mut calls = Vec::new();
248        let mut auth_witnesses = Vec::new();
249        let mut capsules = Vec::new();
250        let mut extra_hashed_args = Vec::new();
251        let mut fee_payer: Option<AztecAddress> = None;
252
253        for payload in payloads {
254            calls.extend(payload.calls);
255            auth_witnesses.extend(payload.auth_witnesses);
256            capsules.extend(payload.capsules);
257            extra_hashed_args.extend(payload.extra_hashed_args);
258
259            if let Some(payer) = payload.fee_payer {
260                if let Some(existing) = fee_payer {
261                    if existing != payer {
262                        return Err(Error::InvalidData(format!(
263                            "conflicting fee payers: {existing} vs {payer}"
264                        )));
265                    }
266                }
267                fee_payer = Some(payer);
268            }
269        }
270
271        Ok(ExecutionPayload {
272            calls,
273            auth_witnesses,
274            capsules,
275            extra_hashed_args,
276            fee_payer,
277        })
278    }
279}
280
281#[cfg(test)]
282#[allow(clippy::expect_used, clippy::panic)]
283mod tests {
284    use super::*;
285
286    fn make_receipt(status: TxStatus, exec: Option<TxExecutionResult>) -> TxReceipt {
287        TxReceipt {
288            tx_hash: TxHash::zero(),
289            status,
290            execution_result: exec,
291            error: None,
292            transaction_fee: None,
293            block_hash: None,
294            block_number: None,
295            epoch_number: None,
296        }
297    }
298
299    #[test]
300    fn tx_hash_hex_roundtrip() {
301        let hash = TxHash([0xab; 32]);
302        let json = serde_json::to_string(&hash).expect("serialize TxHash");
303        assert!(json.contains("0xabab"), "should serialize as hex string");
304        let decoded: TxHash = serde_json::from_str(&json).expect("deserialize TxHash");
305        assert_eq!(decoded, hash);
306    }
307
308    #[test]
309    fn tx_hash_from_hex() {
310        let hash =
311            TxHash::from_hex("0x0000000000000000000000000000000000000000000000000000000000000001")
312                .expect("valid hex");
313        assert_eq!(hash.0[31], 1);
314        assert_eq!(hash.0[0], 0);
315    }
316
317    #[test]
318    fn tx_hash_display() {
319        let hash = TxHash::zero();
320        let s = hash.to_string();
321        assert_eq!(
322            s,
323            "0x0000000000000000000000000000000000000000000000000000000000000000"
324        );
325    }
326
327    #[test]
328    fn tx_status_roundtrip() {
329        let statuses = [
330            (TxStatus::Dropped, "\"dropped\""),
331            (TxStatus::Pending, "\"pending\""),
332            (TxStatus::Proposed, "\"proposed\""),
333            (TxStatus::Checkpointed, "\"checkpointed\""),
334            (TxStatus::Proven, "\"proven\""),
335            (TxStatus::Finalized, "\"finalized\""),
336        ];
337
338        for (status, expected_json) in statuses {
339            let json = serde_json::to_string(&status).expect("serialize TxStatus");
340            assert_eq!(json, expected_json);
341            let decoded: TxStatus = serde_json::from_str(&json).expect("deserialize TxStatus");
342            assert_eq!(decoded, status);
343        }
344    }
345
346    #[test]
347    fn tx_execution_result_roundtrip() {
348        let results = [
349            TxExecutionResult::Success,
350            TxExecutionResult::AppLogicReverted,
351            TxExecutionResult::TeardownReverted,
352            TxExecutionResult::BothReverted,
353        ];
354
355        for result in results {
356            let json = serde_json::to_string(&result).expect("serialize TxExecutionResult");
357            let decoded: TxExecutionResult =
358                serde_json::from_str(&json).expect("deserialize TxExecutionResult");
359            assert_eq!(decoded, result);
360        }
361    }
362
363    #[test]
364    fn receipt_mined_success() {
365        let receipt = TxReceipt {
366            tx_hash: TxHash::zero(),
367            status: TxStatus::Checkpointed,
368            execution_result: Some(TxExecutionResult::Success),
369            error: None,
370            transaction_fee: Some(1000),
371            block_hash: Some([0x11; 32]),
372            block_number: Some(42),
373            epoch_number: Some(1),
374        };
375
376        assert!(receipt.is_mined());
377        assert!(!receipt.is_pending());
378        assert!(!receipt.is_dropped());
379        assert!(receipt.has_execution_succeeded());
380        assert!(!receipt.has_execution_reverted());
381    }
382
383    #[test]
384    fn receipt_pending() {
385        let receipt = make_receipt(TxStatus::Pending, None);
386        assert!(!receipt.is_mined());
387        assert!(receipt.is_pending());
388        assert!(!receipt.is_dropped());
389        assert!(!receipt.has_execution_succeeded());
390        assert!(!receipt.has_execution_reverted());
391    }
392
393    #[test]
394    fn receipt_dropped() {
395        let receipt = make_receipt(TxStatus::Dropped, None);
396        assert!(!receipt.is_mined());
397        assert!(!receipt.is_pending());
398        assert!(receipt.is_dropped());
399    }
400
401    #[test]
402    fn receipt_reverted() {
403        let receipt = make_receipt(
404            TxStatus::Checkpointed,
405            Some(TxExecutionResult::AppLogicReverted),
406        );
407        assert!(receipt.is_mined());
408        assert!(!receipt.has_execution_succeeded());
409        assert!(receipt.has_execution_reverted());
410    }
411
412    #[test]
413    fn receipt_both_reverted() {
414        let receipt = make_receipt(
415            TxStatus::Checkpointed,
416            Some(TxExecutionResult::BothReverted),
417        );
418        assert!(receipt.has_execution_reverted());
419    }
420
421    #[test]
422    fn receipt_all_mined_statuses() {
423        for status in [
424            TxStatus::Proposed,
425            TxStatus::Checkpointed,
426            TxStatus::Proven,
427            TxStatus::Finalized,
428        ] {
429            let receipt = make_receipt(status, Some(TxExecutionResult::Success));
430            assert!(receipt.is_mined(), "{status:?} should count as mined");
431        }
432    }
433
434    #[test]
435    fn receipt_json_roundtrip() {
436        let receipt = TxReceipt {
437            tx_hash: TxHash::from_hex(
438                "0x00000000000000000000000000000000000000000000000000000000deadbeef",
439            )
440            .expect("valid hex"),
441            status: TxStatus::Finalized,
442            execution_result: Some(TxExecutionResult::Success),
443            error: None,
444            transaction_fee: Some(5000),
445            block_hash: Some([0xcc; 32]),
446            block_number: Some(100),
447            epoch_number: Some(10),
448        };
449
450        let json = serde_json::to_string(&receipt).expect("serialize receipt");
451        assert!(json.contains("deadbeef"), "tx_hash should be hex");
452        assert!(json.contains("0xcc"), "block_hash should be hex");
453
454        let decoded: TxReceipt = serde_json::from_str(&json).expect("deserialize receipt");
455        assert_eq!(decoded, receipt);
456    }
457
458    #[test]
459    fn receipt_json_roundtrip_with_nulls() {
460        let receipt = TxReceipt {
461            tx_hash: TxHash::zero(),
462            status: TxStatus::Pending,
463            execution_result: None,
464            error: None,
465            transaction_fee: None,
466            block_hash: None,
467            block_number: None,
468            epoch_number: None,
469        };
470
471        let json = serde_json::to_string(&receipt).expect("serialize receipt");
472        let decoded: TxReceipt = serde_json::from_str(&json).expect("deserialize receipt");
473        assert_eq!(decoded, receipt);
474    }
475
476    #[test]
477    fn payload_serializes() {
478        let payload = ExecutionPayload::default();
479        let json = serde_json::to_string(&payload).expect("serialize ExecutionPayload");
480        assert!(json.contains("\"calls\":[]"));
481    }
482
483    #[test]
484    fn merge_empty_payloads() {
485        let result = ExecutionPayload::merge(vec![]).expect("merge empty");
486        assert!(result.calls.is_empty());
487        assert!(result.auth_witnesses.is_empty());
488        assert!(result.capsules.is_empty());
489        assert!(result.extra_hashed_args.is_empty());
490        assert!(result.fee_payer.is_none());
491    }
492
493    #[test]
494    fn merge_single_payload() {
495        let payer = AztecAddress(Fr::from(1u64));
496        let payload = ExecutionPayload {
497            calls: vec![FunctionCall {
498                to: AztecAddress(Fr::from(2u64)),
499                selector: FunctionSelector::from_hex("0x11223344").expect("valid"),
500                args: vec![],
501                function_type: FunctionType::Private,
502                is_static: false,
503            }],
504            auth_witnesses: vec![AuthWitness {
505                fields: vec![Fr::from(9u64)],
506                ..Default::default()
507            }],
508            capsules: vec![],
509            extra_hashed_args: vec![],
510            fee_payer: Some(payer),
511        };
512
513        let merged = ExecutionPayload::merge(vec![payload]).expect("merge single");
514        assert_eq!(merged.calls.len(), 1);
515        assert_eq!(merged.fee_payer, Some(payer));
516    }
517
518    #[test]
519    fn merge_concatenates_fields() {
520        let p1 = ExecutionPayload {
521            calls: vec![FunctionCall {
522                to: AztecAddress(Fr::from(1u64)),
523                selector: FunctionSelector::from_hex("0x11111111").expect("valid"),
524                args: vec![],
525                function_type: FunctionType::Private,
526                is_static: false,
527            }],
528            auth_witnesses: vec![AuthWitness {
529                fields: vec![Fr::from(1u64)],
530                ..Default::default()
531            }],
532            capsules: vec![],
533            extra_hashed_args: vec![],
534            fee_payer: None,
535        };
536
537        let p2 = ExecutionPayload {
538            calls: vec![FunctionCall {
539                to: AztecAddress(Fr::from(2u64)),
540                selector: FunctionSelector::from_hex("0x22222222").expect("valid"),
541                args: vec![],
542                function_type: FunctionType::Public,
543                is_static: false,
544            }],
545            auth_witnesses: vec![AuthWitness {
546                fields: vec![Fr::from(2u64)],
547                ..Default::default()
548            }],
549            capsules: vec![],
550            extra_hashed_args: vec![],
551            fee_payer: None,
552        };
553
554        let merged = ExecutionPayload::merge(vec![p1, p2]).expect("merge two");
555        assert_eq!(merged.calls.len(), 2);
556        assert_eq!(merged.auth_witnesses.len(), 2);
557        assert!(merged.fee_payer.is_none());
558    }
559
560    #[test]
561    fn merge_same_fee_payer_succeeds() {
562        let payer = AztecAddress(Fr::from(5u64));
563        let p1 = ExecutionPayload {
564            fee_payer: Some(payer),
565            ..Default::default()
566        };
567        let p2 = ExecutionPayload {
568            fee_payer: Some(payer),
569            ..Default::default()
570        };
571
572        let merged = ExecutionPayload::merge(vec![p1, p2]).expect("same payer");
573        assert_eq!(merged.fee_payer, Some(payer));
574    }
575
576    #[test]
577    fn merge_conflicting_fee_payer_errors() {
578        let p1 = ExecutionPayload {
579            fee_payer: Some(AztecAddress(Fr::from(1u64))),
580            ..Default::default()
581        };
582        let p2 = ExecutionPayload {
583            fee_payer: Some(AztecAddress(Fr::from(2u64))),
584            ..Default::default()
585        };
586
587        let result = ExecutionPayload::merge(vec![p1, p2]);
588        assert!(result.is_err());
589    }
590
591    #[test]
592    fn merge_mixed_fee_payer_takes_defined() {
593        let payer = AztecAddress(Fr::from(3u64));
594        let p1 = ExecutionPayload {
595            fee_payer: None,
596            ..Default::default()
597        };
598        let p2 = ExecutionPayload {
599            fee_payer: Some(payer),
600            ..Default::default()
601        };
602
603        let merged = ExecutionPayload::merge(vec![p1, p2]).expect("mixed payer");
604        assert_eq!(merged.fee_payer, Some(payer));
605    }
606
607    #[test]
608    fn payload_with_calls_roundtrip() {
609        let payload = ExecutionPayload {
610            calls: vec![FunctionCall {
611                to: AztecAddress(Fr::from(1u64)),
612                selector: crate::abi::FunctionSelector::from_hex("0xaabbccdd")
613                    .expect("valid selector"),
614                args: vec![AbiValue::Field(Fr::from(42u64))],
615                function_type: FunctionType::Private,
616                is_static: false,
617            }],
618            auth_witnesses: vec![AuthWitness {
619                fields: vec![Fr::from(1u64)],
620                ..Default::default()
621            }],
622            capsules: vec![],
623            extra_hashed_args: vec![],
624            fee_payer: Some(AztecAddress(Fr::from(99u64))),
625        };
626
627        let json = serde_json::to_string(&payload).expect("serialize payload");
628        let decoded: ExecutionPayload = serde_json::from_str(&json).expect("deserialize payload");
629        assert_eq!(decoded, payload);
630    }
631}