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
231impl ExecutionPayload {
232 pub fn merge(payloads: Vec<ExecutionPayload>) -> Result<Self, Error> {
238 let mut calls = Vec::new();
239 let mut auth_witnesses = Vec::new();
240 let mut capsules = Vec::new();
241 let mut extra_hashed_args = Vec::new();
242 let mut fee_payer: Option<AztecAddress> = None;
243
244 for payload in payloads {
245 calls.extend(payload.calls);
246 auth_witnesses.extend(payload.auth_witnesses);
247 capsules.extend(payload.capsules);
248 extra_hashed_args.extend(payload.extra_hashed_args);
249
250 if let Some(payer) = payload.fee_payer {
251 if let Some(existing) = fee_payer {
252 if existing != payer {
253 return Err(Error::InvalidData(format!(
254 "conflicting fee payers: {existing} vs {payer}"
255 )));
256 }
257 }
258 fee_payer = Some(payer);
259 }
260 }
261
262 Ok(ExecutionPayload {
263 calls,
264 auth_witnesses,
265 capsules,
266 extra_hashed_args,
267 fee_payer,
268 })
269 }
270}
271
272#[cfg(test)]
273#[allow(clippy::expect_used, clippy::panic)]
274mod tests {
275 use super::*;
276
277 fn make_receipt(status: TxStatus, exec: Option<TxExecutionResult>) -> TxReceipt {
278 TxReceipt {
279 tx_hash: TxHash::zero(),
280 status,
281 execution_result: exec,
282 error: None,
283 transaction_fee: None,
284 block_hash: None,
285 block_number: None,
286 epoch_number: None,
287 }
288 }
289
290 #[test]
291 fn tx_hash_hex_roundtrip() {
292 let hash = TxHash([0xab; 32]);
293 let json = serde_json::to_string(&hash).expect("serialize TxHash");
294 assert!(json.contains("0xabab"), "should serialize as hex string");
295 let decoded: TxHash = serde_json::from_str(&json).expect("deserialize TxHash");
296 assert_eq!(decoded, hash);
297 }
298
299 #[test]
300 fn tx_hash_from_hex() {
301 let hash =
302 TxHash::from_hex("0x0000000000000000000000000000000000000000000000000000000000000001")
303 .expect("valid hex");
304 assert_eq!(hash.0[31], 1);
305 assert_eq!(hash.0[0], 0);
306 }
307
308 #[test]
309 fn tx_hash_display() {
310 let hash = TxHash::zero();
311 let s = hash.to_string();
312 assert_eq!(
313 s,
314 "0x0000000000000000000000000000000000000000000000000000000000000000"
315 );
316 }
317
318 #[test]
319 fn tx_status_roundtrip() {
320 let statuses = [
321 (TxStatus::Dropped, "\"dropped\""),
322 (TxStatus::Pending, "\"pending\""),
323 (TxStatus::Proposed, "\"proposed\""),
324 (TxStatus::Checkpointed, "\"checkpointed\""),
325 (TxStatus::Proven, "\"proven\""),
326 (TxStatus::Finalized, "\"finalized\""),
327 ];
328
329 for (status, expected_json) in statuses {
330 let json = serde_json::to_string(&status).expect("serialize TxStatus");
331 assert_eq!(json, expected_json);
332 let decoded: TxStatus = serde_json::from_str(&json).expect("deserialize TxStatus");
333 assert_eq!(decoded, status);
334 }
335 }
336
337 #[test]
338 fn tx_execution_result_roundtrip() {
339 let results = [
340 TxExecutionResult::Success,
341 TxExecutionResult::AppLogicReverted,
342 TxExecutionResult::TeardownReverted,
343 TxExecutionResult::BothReverted,
344 ];
345
346 for result in results {
347 let json = serde_json::to_string(&result).expect("serialize TxExecutionResult");
348 let decoded: TxExecutionResult =
349 serde_json::from_str(&json).expect("deserialize TxExecutionResult");
350 assert_eq!(decoded, result);
351 }
352 }
353
354 #[test]
355 fn receipt_mined_success() {
356 let receipt = TxReceipt {
357 tx_hash: TxHash::zero(),
358 status: TxStatus::Checkpointed,
359 execution_result: Some(TxExecutionResult::Success),
360 error: None,
361 transaction_fee: Some(1000),
362 block_hash: Some([0x11; 32]),
363 block_number: Some(42),
364 epoch_number: Some(1),
365 };
366
367 assert!(receipt.is_mined());
368 assert!(!receipt.is_pending());
369 assert!(!receipt.is_dropped());
370 assert!(receipt.has_execution_succeeded());
371 assert!(!receipt.has_execution_reverted());
372 }
373
374 #[test]
375 fn receipt_pending() {
376 let receipt = make_receipt(TxStatus::Pending, None);
377 assert!(!receipt.is_mined());
378 assert!(receipt.is_pending());
379 assert!(!receipt.is_dropped());
380 assert!(!receipt.has_execution_succeeded());
381 assert!(!receipt.has_execution_reverted());
382 }
383
384 #[test]
385 fn receipt_dropped() {
386 let receipt = make_receipt(TxStatus::Dropped, None);
387 assert!(!receipt.is_mined());
388 assert!(!receipt.is_pending());
389 assert!(receipt.is_dropped());
390 }
391
392 #[test]
393 fn receipt_reverted() {
394 let receipt = make_receipt(
395 TxStatus::Checkpointed,
396 Some(TxExecutionResult::AppLogicReverted),
397 );
398 assert!(receipt.is_mined());
399 assert!(!receipt.has_execution_succeeded());
400 assert!(receipt.has_execution_reverted());
401 }
402
403 #[test]
404 fn receipt_both_reverted() {
405 let receipt = make_receipt(
406 TxStatus::Checkpointed,
407 Some(TxExecutionResult::BothReverted),
408 );
409 assert!(receipt.has_execution_reverted());
410 }
411
412 #[test]
413 fn receipt_all_mined_statuses() {
414 for status in [
415 TxStatus::Proposed,
416 TxStatus::Checkpointed,
417 TxStatus::Proven,
418 TxStatus::Finalized,
419 ] {
420 let receipt = make_receipt(status, Some(TxExecutionResult::Success));
421 assert!(receipt.is_mined(), "{status:?} should count as mined");
422 }
423 }
424
425 #[test]
426 fn receipt_json_roundtrip() {
427 let receipt = TxReceipt {
428 tx_hash: TxHash::from_hex(
429 "0x00000000000000000000000000000000000000000000000000000000deadbeef",
430 )
431 .expect("valid hex"),
432 status: TxStatus::Finalized,
433 execution_result: Some(TxExecutionResult::Success),
434 error: None,
435 transaction_fee: Some(5000),
436 block_hash: Some([0xcc; 32]),
437 block_number: Some(100),
438 epoch_number: Some(10),
439 };
440
441 let json = serde_json::to_string(&receipt).expect("serialize receipt");
442 assert!(json.contains("deadbeef"), "tx_hash should be hex");
443 assert!(json.contains("0xcc"), "block_hash should be hex");
444
445 let decoded: TxReceipt = serde_json::from_str(&json).expect("deserialize receipt");
446 assert_eq!(decoded, receipt);
447 }
448
449 #[test]
450 fn receipt_json_roundtrip_with_nulls() {
451 let receipt = TxReceipt {
452 tx_hash: TxHash::zero(),
453 status: TxStatus::Pending,
454 execution_result: None,
455 error: None,
456 transaction_fee: None,
457 block_hash: None,
458 block_number: None,
459 epoch_number: None,
460 };
461
462 let json = serde_json::to_string(&receipt).expect("serialize receipt");
463 let decoded: TxReceipt = serde_json::from_str(&json).expect("deserialize receipt");
464 assert_eq!(decoded, receipt);
465 }
466
467 #[test]
468 fn payload_serializes() {
469 let payload = ExecutionPayload::default();
470 let json = serde_json::to_string(&payload).expect("serialize ExecutionPayload");
471 assert!(json.contains("\"calls\":[]"));
472 }
473
474 #[test]
475 fn merge_empty_payloads() {
476 let result = ExecutionPayload::merge(vec![]).expect("merge empty");
477 assert!(result.calls.is_empty());
478 assert!(result.auth_witnesses.is_empty());
479 assert!(result.capsules.is_empty());
480 assert!(result.extra_hashed_args.is_empty());
481 assert!(result.fee_payer.is_none());
482 }
483
484 #[test]
485 fn merge_single_payload() {
486 let payer = AztecAddress(Fr::from(1u64));
487 let payload = ExecutionPayload {
488 calls: vec![FunctionCall {
489 to: AztecAddress(Fr::from(2u64)),
490 selector: FunctionSelector::from_hex("0x11223344").expect("valid"),
491 args: vec![],
492 function_type: FunctionType::Private,
493 is_static: false,
494 }],
495 auth_witnesses: vec![AuthWitness {
496 fields: vec![Fr::from(9u64)],
497 }],
498 capsules: vec![],
499 extra_hashed_args: vec![],
500 fee_payer: Some(payer),
501 };
502
503 let merged = ExecutionPayload::merge(vec![payload.clone()]).expect("merge single");
504 assert_eq!(merged, payload);
505 }
506
507 #[test]
508 fn merge_concatenates_fields() {
509 let p1 = ExecutionPayload {
510 calls: vec![FunctionCall {
511 to: AztecAddress(Fr::from(1u64)),
512 selector: FunctionSelector::from_hex("0x11111111").expect("valid"),
513 args: vec![],
514 function_type: FunctionType::Private,
515 is_static: false,
516 }],
517 auth_witnesses: vec![AuthWitness {
518 fields: vec![Fr::from(1u64)],
519 }],
520 capsules: vec![],
521 extra_hashed_args: vec![],
522 fee_payer: None,
523 };
524
525 let p2 = ExecutionPayload {
526 calls: vec![FunctionCall {
527 to: AztecAddress(Fr::from(2u64)),
528 selector: FunctionSelector::from_hex("0x22222222").expect("valid"),
529 args: vec![],
530 function_type: FunctionType::Public,
531 is_static: false,
532 }],
533 auth_witnesses: vec![AuthWitness {
534 fields: vec![Fr::from(2u64)],
535 }],
536 capsules: vec![],
537 extra_hashed_args: vec![],
538 fee_payer: None,
539 };
540
541 let merged = ExecutionPayload::merge(vec![p1, p2]).expect("merge two");
542 assert_eq!(merged.calls.len(), 2);
543 assert_eq!(merged.auth_witnesses.len(), 2);
544 assert!(merged.fee_payer.is_none());
545 }
546
547 #[test]
548 fn merge_same_fee_payer_succeeds() {
549 let payer = AztecAddress(Fr::from(5u64));
550 let p1 = ExecutionPayload {
551 fee_payer: Some(payer),
552 ..Default::default()
553 };
554 let p2 = ExecutionPayload {
555 fee_payer: Some(payer),
556 ..Default::default()
557 };
558
559 let merged = ExecutionPayload::merge(vec![p1, p2]).expect("same payer");
560 assert_eq!(merged.fee_payer, Some(payer));
561 }
562
563 #[test]
564 fn merge_conflicting_fee_payer_errors() {
565 let p1 = ExecutionPayload {
566 fee_payer: Some(AztecAddress(Fr::from(1u64))),
567 ..Default::default()
568 };
569 let p2 = ExecutionPayload {
570 fee_payer: Some(AztecAddress(Fr::from(2u64))),
571 ..Default::default()
572 };
573
574 let result = ExecutionPayload::merge(vec![p1, p2]);
575 assert!(result.is_err());
576 }
577
578 #[test]
579 fn merge_mixed_fee_payer_takes_defined() {
580 let payer = AztecAddress(Fr::from(3u64));
581 let p1 = ExecutionPayload {
582 fee_payer: None,
583 ..Default::default()
584 };
585 let p2 = ExecutionPayload {
586 fee_payer: Some(payer),
587 ..Default::default()
588 };
589
590 let merged = ExecutionPayload::merge(vec![p1, p2]).expect("mixed payer");
591 assert_eq!(merged.fee_payer, Some(payer));
592 }
593
594 #[test]
595 fn payload_with_calls_roundtrip() {
596 let payload = ExecutionPayload {
597 calls: vec![FunctionCall {
598 to: AztecAddress(Fr::from(1u64)),
599 selector: crate::abi::FunctionSelector::from_hex("0xaabbccdd")
600 .expect("valid selector"),
601 args: vec![AbiValue::Field(Fr::from(42u64))],
602 function_type: FunctionType::Private,
603 is_static: false,
604 }],
605 auth_witnesses: vec![AuthWitness {
606 fields: vec![Fr::from(1u64)],
607 }],
608 capsules: vec![],
609 extra_hashed_args: vec![],
610 fee_payer: Some(AztecAddress(Fr::from(99u64))),
611 };
612
613 let json = serde_json::to_string(&payload).expect("serialize payload");
614 let decoded: ExecutionPayload = serde_json::from_str(&json).expect("deserialize payload");
615 assert_eq!(decoded, payload);
616 }
617}