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#[derive(Clone, Copy, PartialEq, Eq, Hash)]
10pub struct TxHash(pub [u8; 32]);
11
12impl TxHash {
13 pub const fn zero() -> Self {
15 Self([0u8; 32])
16 }
17
18 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "lowercase")]
58pub enum TxStatus {
59 Dropped,
61 Pending,
63 Proposed,
65 Checkpointed,
67 Proven,
69 Finalized,
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum TxExecutionResult {
77 Success,
79 AppLogicReverted,
81 TeardownReverted,
83 BothReverted,
85}
86
87#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
89pub struct TxReceipt {
90 pub tx_hash: TxHash,
92 pub status: TxStatus,
94 pub execution_result: Option<TxExecutionResult>,
96 pub error: Option<String>,
98 pub transaction_fee: Option<u128>,
100 #[serde(default, with = "option_hex_bytes_32")]
102 pub block_hash: Option<[u8; 32]>,
103 pub block_number: Option<u64>,
105 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 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 pub fn is_pending(&self) -> bool {
151 self.status == TxStatus::Pending
152 }
153
154 pub fn is_dropped(&self) -> bool {
156 self.status == TxStatus::Dropped
157 }
158
159 pub fn has_execution_succeeded(&self) -> bool {
161 self.execution_result == Some(TxExecutionResult::Success)
162 }
163
164 pub fn has_execution_reverted(&self) -> bool {
166 self.execution_result.is_some() && !self.has_execution_succeeded()
167 }
168}
169
170#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
172pub struct FunctionCall {
173 pub to: AztecAddress,
175 pub selector: FunctionSelector,
177 pub args: Vec<AbiValue>,
179 pub function_type: FunctionType,
181 pub is_static: bool,
183}
184
185#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
187pub struct AuthWitness {
188 #[serde(default)]
190 pub fields: Vec<Fr>,
191}
192
193#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
195pub struct Capsule {
196 #[serde(default)]
198 pub data: Vec<u8>,
199}
200
201#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
203pub struct HashedValues {
204 #[serde(default)]
206 pub values: Vec<Fr>,
207}
208
209#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
214pub struct ExecutionPayload {
215 #[serde(default)]
217 pub calls: Vec<FunctionCall>,
218 #[serde(default)]
220 pub auth_witnesses: Vec<AuthWitness>,
221 #[serde(default)]
223 pub capsules: Vec<Capsule>,
224 #[serde(default)]
226 pub extra_hashed_args: Vec<HashedValues>,
227 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}