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    /// Field elements comprising the witness data.
189    #[serde(default)]
190    pub fields: Vec<Fr>,
191}
192
193/// Private data capsule passed alongside a transaction.
194#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
195pub struct Capsule {
196    /// Raw capsule bytes.
197    #[serde(default)]
198    pub data: Vec<u8>,
199}
200
201/// Pre-hashed values included in a transaction for oracle access.
202#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
203pub struct HashedValues {
204    /// Field elements to be hashed.
205    #[serde(default)]
206    pub values: Vec<Fr>,
207}
208
209/// A complete transaction execution payload.
210///
211/// Groups one or more [`FunctionCall`]s with their associated auth witnesses,
212/// capsules, hashed args, and an optional fee payer override.
213#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
214pub struct ExecutionPayload {
215    /// Function calls to execute.
216    #[serde(default)]
217    pub calls: Vec<FunctionCall>,
218    /// Authorization witnesses for the calls.
219    #[serde(default)]
220    pub auth_witnesses: Vec<AuthWitness>,
221    /// Private data capsules.
222    #[serde(default)]
223    pub capsules: Vec<Capsule>,
224    /// Extra hashed arguments for oracle access.
225    #[serde(default)]
226    pub extra_hashed_args: Vec<HashedValues>,
227    /// Override the fee payer for this payload.
228    pub fee_payer: Option<AztecAddress>,
229}
230
231#[cfg(test)]
232#[allow(clippy::expect_used, clippy::panic)]
233mod tests {
234    use super::*;
235
236    fn make_receipt(status: TxStatus, exec: Option<TxExecutionResult>) -> TxReceipt {
237        TxReceipt {
238            tx_hash: TxHash::zero(),
239            status,
240            execution_result: exec,
241            error: None,
242            transaction_fee: None,
243            block_hash: None,
244            block_number: None,
245            epoch_number: None,
246        }
247    }
248
249    #[test]
250    fn tx_hash_hex_roundtrip() {
251        let hash = TxHash([0xab; 32]);
252        let json = serde_json::to_string(&hash).expect("serialize TxHash");
253        assert!(json.contains("0xabab"), "should serialize as hex string");
254        let decoded: TxHash = serde_json::from_str(&json).expect("deserialize TxHash");
255        assert_eq!(decoded, hash);
256    }
257
258    #[test]
259    fn tx_hash_from_hex() {
260        let hash =
261            TxHash::from_hex("0x0000000000000000000000000000000000000000000000000000000000000001")
262                .expect("valid hex");
263        assert_eq!(hash.0[31], 1);
264        assert_eq!(hash.0[0], 0);
265    }
266
267    #[test]
268    fn tx_hash_display() {
269        let hash = TxHash::zero();
270        let s = hash.to_string();
271        assert_eq!(
272            s,
273            "0x0000000000000000000000000000000000000000000000000000000000000000"
274        );
275    }
276
277    #[test]
278    fn tx_status_roundtrip() {
279        let statuses = [
280            (TxStatus::Dropped, "\"dropped\""),
281            (TxStatus::Pending, "\"pending\""),
282            (TxStatus::Proposed, "\"proposed\""),
283            (TxStatus::Checkpointed, "\"checkpointed\""),
284            (TxStatus::Proven, "\"proven\""),
285            (TxStatus::Finalized, "\"finalized\""),
286        ];
287
288        for (status, expected_json) in statuses {
289            let json = serde_json::to_string(&status).expect("serialize TxStatus");
290            assert_eq!(json, expected_json);
291            let decoded: TxStatus = serde_json::from_str(&json).expect("deserialize TxStatus");
292            assert_eq!(decoded, status);
293        }
294    }
295
296    #[test]
297    fn tx_execution_result_roundtrip() {
298        let results = [
299            TxExecutionResult::Success,
300            TxExecutionResult::AppLogicReverted,
301            TxExecutionResult::TeardownReverted,
302            TxExecutionResult::BothReverted,
303        ];
304
305        for result in results {
306            let json = serde_json::to_string(&result).expect("serialize TxExecutionResult");
307            let decoded: TxExecutionResult =
308                serde_json::from_str(&json).expect("deserialize TxExecutionResult");
309            assert_eq!(decoded, result);
310        }
311    }
312
313    #[test]
314    fn receipt_mined_success() {
315        let receipt = TxReceipt {
316            tx_hash: TxHash::zero(),
317            status: TxStatus::Checkpointed,
318            execution_result: Some(TxExecutionResult::Success),
319            error: None,
320            transaction_fee: Some(1000),
321            block_hash: Some([0x11; 32]),
322            block_number: Some(42),
323            epoch_number: Some(1),
324        };
325
326        assert!(receipt.is_mined());
327        assert!(!receipt.is_pending());
328        assert!(!receipt.is_dropped());
329        assert!(receipt.has_execution_succeeded());
330        assert!(!receipt.has_execution_reverted());
331    }
332
333    #[test]
334    fn receipt_pending() {
335        let receipt = make_receipt(TxStatus::Pending, None);
336        assert!(!receipt.is_mined());
337        assert!(receipt.is_pending());
338        assert!(!receipt.is_dropped());
339        assert!(!receipt.has_execution_succeeded());
340        assert!(!receipt.has_execution_reverted());
341    }
342
343    #[test]
344    fn receipt_dropped() {
345        let receipt = make_receipt(TxStatus::Dropped, None);
346        assert!(!receipt.is_mined());
347        assert!(!receipt.is_pending());
348        assert!(receipt.is_dropped());
349    }
350
351    #[test]
352    fn receipt_reverted() {
353        let receipt = make_receipt(
354            TxStatus::Checkpointed,
355            Some(TxExecutionResult::AppLogicReverted),
356        );
357        assert!(receipt.is_mined());
358        assert!(!receipt.has_execution_succeeded());
359        assert!(receipt.has_execution_reverted());
360    }
361
362    #[test]
363    fn receipt_both_reverted() {
364        let receipt = make_receipt(
365            TxStatus::Checkpointed,
366            Some(TxExecutionResult::BothReverted),
367        );
368        assert!(receipt.has_execution_reverted());
369    }
370
371    #[test]
372    fn receipt_all_mined_statuses() {
373        for status in [
374            TxStatus::Proposed,
375            TxStatus::Checkpointed,
376            TxStatus::Proven,
377            TxStatus::Finalized,
378        ] {
379            let receipt = make_receipt(status, Some(TxExecutionResult::Success));
380            assert!(receipt.is_mined(), "{status:?} should count as mined");
381        }
382    }
383
384    #[test]
385    fn receipt_json_roundtrip() {
386        let receipt = TxReceipt {
387            tx_hash: TxHash::from_hex(
388                "0x00000000000000000000000000000000000000000000000000000000deadbeef",
389            )
390            .expect("valid hex"),
391            status: TxStatus::Finalized,
392            execution_result: Some(TxExecutionResult::Success),
393            error: None,
394            transaction_fee: Some(5000),
395            block_hash: Some([0xcc; 32]),
396            block_number: Some(100),
397            epoch_number: Some(10),
398        };
399
400        let json = serde_json::to_string(&receipt).expect("serialize receipt");
401        assert!(json.contains("deadbeef"), "tx_hash should be hex");
402        assert!(json.contains("0xcc"), "block_hash should be hex");
403
404        let decoded: TxReceipt = serde_json::from_str(&json).expect("deserialize receipt");
405        assert_eq!(decoded, receipt);
406    }
407
408    #[test]
409    fn receipt_json_roundtrip_with_nulls() {
410        let receipt = TxReceipt {
411            tx_hash: TxHash::zero(),
412            status: TxStatus::Pending,
413            execution_result: None,
414            error: None,
415            transaction_fee: None,
416            block_hash: None,
417            block_number: None,
418            epoch_number: None,
419        };
420
421        let json = serde_json::to_string(&receipt).expect("serialize receipt");
422        let decoded: TxReceipt = serde_json::from_str(&json).expect("deserialize receipt");
423        assert_eq!(decoded, receipt);
424    }
425
426    #[test]
427    fn payload_serializes() {
428        let payload = ExecutionPayload::default();
429        let json = serde_json::to_string(&payload).expect("serialize ExecutionPayload");
430        assert!(json.contains("\"calls\":[]"));
431    }
432
433    #[test]
434    fn payload_with_calls_roundtrip() {
435        let payload = ExecutionPayload {
436            calls: vec![FunctionCall {
437                to: AztecAddress(Fr::from(1u64)),
438                selector: crate::abi::FunctionSelector::from_hex("0xaabbccdd")
439                    .expect("valid selector"),
440                args: vec![AbiValue::Field(Fr::from(42u64))],
441                function_type: FunctionType::Private,
442                is_static: false,
443            }],
444            auth_witnesses: vec![AuthWitness {
445                fields: vec![Fr::from(1u64)],
446            }],
447            capsules: vec![],
448            extra_hashed_args: vec![],
449            fee_payer: Some(AztecAddress(Fr::from(99u64))),
450        };
451
452        let json = serde_json::to_string(&payload).expect("serialize payload");
453        let decoded: ExecutionPayload = serde_json::from_str(&json).expect("deserialize payload");
454        assert_eq!(decoded, payload);
455    }
456}