1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::fmt;
3
4use crate::abi::{AbiValue, FunctionSelector, FunctionType};
5use crate::fee::GasSettings;
6use crate::types::{decode_fixed_hex, encode_hex, AztecAddress, Fr};
7use crate::Error;
8
9#[derive(Clone, Copy, PartialEq, Eq, Hash)]
11pub struct TxHash(pub [u8; 32]);
12
13impl TxHash {
14 pub const fn zero() -> Self {
16 Self([0u8; 32])
17 }
18
19 pub fn from_hex(value: &str) -> Result<Self, Error> {
21 Ok(Self(decode_fixed_hex::<32>(value)?))
22 }
23}
24
25impl fmt::Display for TxHash {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 f.write_str(&encode_hex(&self.0))
28 }
29}
30
31impl fmt::Debug for TxHash {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 write!(f, "TxHash({self})")
34 }
35}
36
37impl Serialize for TxHash {
38 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
39 where
40 S: Serializer,
41 {
42 serializer.serialize_str(&self.to_string())
43 }
44}
45
46impl<'de> Deserialize<'de> for TxHash {
47 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
48 where
49 D: Deserializer<'de>,
50 {
51 let s = String::deserialize(deserializer)?;
52 Self::from_hex(&s).map_err(serde::de::Error::custom)
53 }
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum TxStatus {
60 Dropped,
62 Pending,
64 Proposed,
66 Checkpointed,
68 Proven,
70 Finalized,
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum TxExecutionResult {
78 Success,
80 AppLogicReverted,
82 TeardownReverted,
84 BothReverted,
86}
87
88#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
90pub struct TxReceipt {
91 pub tx_hash: TxHash,
93 pub status: TxStatus,
95 pub execution_result: Option<TxExecutionResult>,
97 pub error: Option<String>,
99 pub transaction_fee: Option<u128>,
101 #[serde(default, with = "option_hex_bytes_32")]
103 pub block_hash: Option<[u8; 32]>,
104 pub block_number: Option<u64>,
106 pub epoch_number: Option<u64>,
108}
109
110mod option_hex_bytes_32 {
111 use serde::{Deserialize, Deserializer, Serializer};
112
113 use crate::types::{decode_fixed_hex, encode_hex};
114
115 #[allow(clippy::ref_option)]
116 pub fn serialize<S>(value: &Option<[u8; 32]>, serializer: S) -> Result<S::Ok, S::Error>
117 where
118 S: Serializer,
119 {
120 match value {
121 Some(bytes) => serializer.serialize_some(&encode_hex(bytes)),
122 None => serializer.serialize_none(),
123 }
124 }
125
126 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<[u8; 32]>, D::Error>
127 where
128 D: Deserializer<'de>,
129 {
130 let opt: Option<String> = Option::deserialize(deserializer)?;
131 match opt {
132 Some(s) => {
133 let bytes = decode_fixed_hex::<32>(&s).map_err(serde::de::Error::custom)?;
134 Ok(Some(bytes))
135 }
136 None => Ok(None),
137 }
138 }
139}
140
141impl TxReceipt {
142 pub const fn is_mined(&self) -> bool {
144 matches!(
145 self.status,
146 TxStatus::Proposed | TxStatus::Checkpointed | TxStatus::Proven | TxStatus::Finalized
147 )
148 }
149
150 pub fn is_pending(&self) -> bool {
152 self.status == TxStatus::Pending
153 }
154
155 pub fn is_dropped(&self) -> bool {
157 self.status == TxStatus::Dropped
158 }
159
160 pub fn has_execution_succeeded(&self) -> bool {
162 self.execution_result == Some(TxExecutionResult::Success)
163 }
164
165 pub fn has_execution_reverted(&self) -> bool {
167 self.execution_result.is_some() && !self.has_execution_succeeded()
168 }
169}
170
171#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
173pub struct FunctionCall {
174 pub to: AztecAddress,
176 pub selector: FunctionSelector,
178 pub args: Vec<AbiValue>,
180 pub function_type: FunctionType,
182 pub is_static: bool,
184 #[serde(default)]
186 pub hide_msg_sender: bool,
187}
188
189impl FunctionCall {
190 pub fn empty() -> Self {
192 Self {
193 to: AztecAddress::zero(),
194 selector: FunctionSelector::empty(),
195 args: vec![],
196 function_type: FunctionType::Private,
197 is_static: false,
198 hide_msg_sender: false,
199 }
200 }
201
202 pub fn is_empty(&self) -> bool {
204 self.to == AztecAddress::zero() && self.selector == FunctionSelector::empty()
205 }
206}
207
208#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
210pub struct AuthWitness {
211 #[serde(default)]
213 pub request_hash: Fr,
214 #[serde(default)]
216 pub fields: Vec<Fr>,
217}
218
219#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
224pub struct Capsule {
225 pub contract_address: AztecAddress,
227 pub storage_slot: Fr,
229 pub data: Vec<Fr>,
231}
232
233#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct TxContext {
240 pub chain_id: Fr,
242 pub version: Fr,
244 pub gas_settings: GasSettings,
246}
247
248#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
250#[serde(rename_all = "camelCase")]
251pub struct HashedValues {
252 #[serde(default)]
254 pub values: Vec<Fr>,
255 #[serde(default)]
257 pub hash: Fr,
258}
259
260impl HashedValues {
261 pub fn from_args(args: Vec<Fr>) -> Self {
263 let hash = crate::hash::compute_var_args_hash(&args);
264 Self { values: args, hash }
265 }
266
267 pub fn from_calldata(calldata: Vec<Fr>) -> Self {
269 let hash = crate::hash::compute_calldata_hash(&calldata);
270 Self {
271 values: calldata,
272 hash,
273 }
274 }
275
276 pub fn hash(&self) -> Fr {
278 self.hash
279 }
280}
281
282#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
287pub struct ExecutionPayload {
288 #[serde(default)]
290 pub calls: Vec<FunctionCall>,
291 #[serde(default)]
293 pub auth_witnesses: Vec<AuthWitness>,
294 #[serde(default)]
296 pub capsules: Vec<Capsule>,
297 #[serde(default)]
299 pub extra_hashed_args: Vec<HashedValues>,
300 pub fee_payer: Option<AztecAddress>,
302}
303
304impl ExecutionPayload {
305 pub fn merge(payloads: Vec<ExecutionPayload>) -> Result<Self, Error> {
311 let mut calls = Vec::new();
312 let mut auth_witnesses = Vec::new();
313 let mut capsules = Vec::new();
314 let mut extra_hashed_args = Vec::new();
315 let mut fee_payer: Option<AztecAddress> = None;
316
317 for payload in payloads {
318 calls.extend(payload.calls);
319 auth_witnesses.extend(payload.auth_witnesses);
320 capsules.extend(payload.capsules);
321 extra_hashed_args.extend(payload.extra_hashed_args);
322
323 if let Some(payer) = payload.fee_payer {
324 if let Some(existing) = fee_payer {
325 if existing != payer {
326 return Err(Error::InvalidData(format!(
327 "conflicting fee payers: {existing} vs {payer}"
328 )));
329 }
330 }
331 fee_payer = Some(payer);
332 }
333 }
334
335 Ok(ExecutionPayload {
336 calls,
337 auth_witnesses,
338 capsules,
339 extra_hashed_args,
340 fee_payer,
341 })
342 }
343}
344
345#[cfg(test)]
346#[allow(clippy::expect_used, clippy::panic)]
347mod tests {
348 use super::*;
349
350 fn make_receipt(status: TxStatus, exec: Option<TxExecutionResult>) -> TxReceipt {
351 TxReceipt {
352 tx_hash: TxHash::zero(),
353 status,
354 execution_result: exec,
355 error: None,
356 transaction_fee: None,
357 block_hash: None,
358 block_number: None,
359 epoch_number: None,
360 }
361 }
362
363 #[test]
364 fn tx_hash_hex_roundtrip() {
365 let hash = TxHash([0xab; 32]);
366 let json = serde_json::to_string(&hash).expect("serialize TxHash");
367 assert!(json.contains("0xabab"), "should serialize as hex string");
368 let decoded: TxHash = serde_json::from_str(&json).expect("deserialize TxHash");
369 assert_eq!(decoded, hash);
370 }
371
372 #[test]
373 fn tx_hash_from_hex() {
374 let hash =
375 TxHash::from_hex("0x0000000000000000000000000000000000000000000000000000000000000001")
376 .expect("valid hex");
377 assert_eq!(hash.0[31], 1);
378 assert_eq!(hash.0[0], 0);
379 }
380
381 #[test]
382 fn tx_hash_display() {
383 let hash = TxHash::zero();
384 let s = hash.to_string();
385 assert_eq!(
386 s,
387 "0x0000000000000000000000000000000000000000000000000000000000000000"
388 );
389 }
390
391 #[test]
392 fn tx_status_roundtrip() {
393 let statuses = [
394 (TxStatus::Dropped, "\"dropped\""),
395 (TxStatus::Pending, "\"pending\""),
396 (TxStatus::Proposed, "\"proposed\""),
397 (TxStatus::Checkpointed, "\"checkpointed\""),
398 (TxStatus::Proven, "\"proven\""),
399 (TxStatus::Finalized, "\"finalized\""),
400 ];
401
402 for (status, expected_json) in statuses {
403 let json = serde_json::to_string(&status).expect("serialize TxStatus");
404 assert_eq!(json, expected_json);
405 let decoded: TxStatus = serde_json::from_str(&json).expect("deserialize TxStatus");
406 assert_eq!(decoded, status);
407 }
408 }
409
410 #[test]
411 fn tx_execution_result_roundtrip() {
412 let results = [
413 TxExecutionResult::Success,
414 TxExecutionResult::AppLogicReverted,
415 TxExecutionResult::TeardownReverted,
416 TxExecutionResult::BothReverted,
417 ];
418
419 for result in results {
420 let json = serde_json::to_string(&result).expect("serialize TxExecutionResult");
421 let decoded: TxExecutionResult =
422 serde_json::from_str(&json).expect("deserialize TxExecutionResult");
423 assert_eq!(decoded, result);
424 }
425 }
426
427 #[test]
428 fn receipt_mined_success() {
429 let receipt = TxReceipt {
430 tx_hash: TxHash::zero(),
431 status: TxStatus::Checkpointed,
432 execution_result: Some(TxExecutionResult::Success),
433 error: None,
434 transaction_fee: Some(1000),
435 block_hash: Some([0x11; 32]),
436 block_number: Some(42),
437 epoch_number: Some(1),
438 };
439
440 assert!(receipt.is_mined());
441 assert!(!receipt.is_pending());
442 assert!(!receipt.is_dropped());
443 assert!(receipt.has_execution_succeeded());
444 assert!(!receipt.has_execution_reverted());
445 }
446
447 #[test]
448 fn receipt_pending() {
449 let receipt = make_receipt(TxStatus::Pending, None);
450 assert!(!receipt.is_mined());
451 assert!(receipt.is_pending());
452 assert!(!receipt.is_dropped());
453 assert!(!receipt.has_execution_succeeded());
454 assert!(!receipt.has_execution_reverted());
455 }
456
457 #[test]
458 fn receipt_dropped() {
459 let receipt = make_receipt(TxStatus::Dropped, None);
460 assert!(!receipt.is_mined());
461 assert!(!receipt.is_pending());
462 assert!(receipt.is_dropped());
463 }
464
465 #[test]
466 fn receipt_reverted() {
467 let receipt = make_receipt(
468 TxStatus::Checkpointed,
469 Some(TxExecutionResult::AppLogicReverted),
470 );
471 assert!(receipt.is_mined());
472 assert!(!receipt.has_execution_succeeded());
473 assert!(receipt.has_execution_reverted());
474 }
475
476 #[test]
477 fn receipt_both_reverted() {
478 let receipt = make_receipt(
479 TxStatus::Checkpointed,
480 Some(TxExecutionResult::BothReverted),
481 );
482 assert!(receipt.has_execution_reverted());
483 }
484
485 #[test]
486 fn receipt_all_mined_statuses() {
487 for status in [
488 TxStatus::Proposed,
489 TxStatus::Checkpointed,
490 TxStatus::Proven,
491 TxStatus::Finalized,
492 ] {
493 let receipt = make_receipt(status, Some(TxExecutionResult::Success));
494 assert!(receipt.is_mined(), "{status:?} should count as mined");
495 }
496 }
497
498 #[test]
499 fn receipt_json_roundtrip() {
500 let receipt = TxReceipt {
501 tx_hash: TxHash::from_hex(
502 "0x00000000000000000000000000000000000000000000000000000000deadbeef",
503 )
504 .expect("valid hex"),
505 status: TxStatus::Finalized,
506 execution_result: Some(TxExecutionResult::Success),
507 error: None,
508 transaction_fee: Some(5000),
509 block_hash: Some([0xcc; 32]),
510 block_number: Some(100),
511 epoch_number: Some(10),
512 };
513
514 let json = serde_json::to_string(&receipt).expect("serialize receipt");
515 assert!(json.contains("deadbeef"), "tx_hash should be hex");
516 assert!(json.contains("0xcc"), "block_hash should be hex");
517
518 let decoded: TxReceipt = serde_json::from_str(&json).expect("deserialize receipt");
519 assert_eq!(decoded, receipt);
520 }
521
522 #[test]
523 fn receipt_json_roundtrip_with_nulls() {
524 let receipt = TxReceipt {
525 tx_hash: TxHash::zero(),
526 status: TxStatus::Pending,
527 execution_result: None,
528 error: None,
529 transaction_fee: None,
530 block_hash: None,
531 block_number: None,
532 epoch_number: None,
533 };
534
535 let json = serde_json::to_string(&receipt).expect("serialize receipt");
536 let decoded: TxReceipt = serde_json::from_str(&json).expect("deserialize receipt");
537 assert_eq!(decoded, receipt);
538 }
539
540 #[test]
541 fn payload_serializes() {
542 let payload = ExecutionPayload::default();
543 let json = serde_json::to_string(&payload).expect("serialize ExecutionPayload");
544 assert!(json.contains("\"calls\":[]"));
545 }
546
547 #[test]
548 fn merge_empty_payloads() {
549 let result = ExecutionPayload::merge(vec![]).expect("merge empty");
550 assert!(result.calls.is_empty());
551 assert!(result.auth_witnesses.is_empty());
552 assert!(result.capsules.is_empty());
553 assert!(result.extra_hashed_args.is_empty());
554 assert!(result.fee_payer.is_none());
555 }
556
557 #[test]
558 fn merge_single_payload() {
559 let payer = AztecAddress(Fr::from(1u64));
560 let payload = ExecutionPayload {
561 calls: vec![FunctionCall {
562 to: AztecAddress(Fr::from(2u64)),
563 selector: FunctionSelector::from_hex("0x11223344").expect("valid"),
564 args: vec![],
565 function_type: FunctionType::Private,
566 is_static: false,
567 hide_msg_sender: false,
568 }],
569 auth_witnesses: vec![AuthWitness {
570 fields: vec![Fr::from(9u64)],
571 ..Default::default()
572 }],
573 capsules: vec![],
574 extra_hashed_args: vec![],
575 fee_payer: Some(payer),
576 };
577
578 let merged = ExecutionPayload::merge(vec![payload]).expect("merge single");
579 assert_eq!(merged.calls.len(), 1);
580 assert_eq!(merged.fee_payer, Some(payer));
581 }
582
583 #[test]
584 fn merge_concatenates_fields() {
585 let p1 = ExecutionPayload {
586 calls: vec![FunctionCall {
587 to: AztecAddress(Fr::from(1u64)),
588 selector: FunctionSelector::from_hex("0x11111111").expect("valid"),
589 args: vec![],
590 function_type: FunctionType::Private,
591 is_static: false,
592 hide_msg_sender: false,
593 }],
594 auth_witnesses: vec![AuthWitness {
595 fields: vec![Fr::from(1u64)],
596 ..Default::default()
597 }],
598 capsules: vec![],
599 extra_hashed_args: vec![],
600 fee_payer: None,
601 };
602
603 let p2 = ExecutionPayload {
604 calls: vec![FunctionCall {
605 to: AztecAddress(Fr::from(2u64)),
606 selector: FunctionSelector::from_hex("0x22222222").expect("valid"),
607 args: vec![],
608 function_type: FunctionType::Public,
609 is_static: false,
610 hide_msg_sender: false,
611 }],
612 auth_witnesses: vec![AuthWitness {
613 fields: vec![Fr::from(2u64)],
614 ..Default::default()
615 }],
616 capsules: vec![],
617 extra_hashed_args: vec![],
618 fee_payer: None,
619 };
620
621 let merged = ExecutionPayload::merge(vec![p1, p2]).expect("merge two");
622 assert_eq!(merged.calls.len(), 2);
623 assert_eq!(merged.auth_witnesses.len(), 2);
624 assert!(merged.fee_payer.is_none());
625 }
626
627 #[test]
628 fn merge_same_fee_payer_succeeds() {
629 let payer = AztecAddress(Fr::from(5u64));
630 let p1 = ExecutionPayload {
631 fee_payer: Some(payer),
632 ..Default::default()
633 };
634 let p2 = ExecutionPayload {
635 fee_payer: Some(payer),
636 ..Default::default()
637 };
638
639 let merged = ExecutionPayload::merge(vec![p1, p2]).expect("same payer");
640 assert_eq!(merged.fee_payer, Some(payer));
641 }
642
643 #[test]
644 fn merge_conflicting_fee_payer_errors() {
645 let p1 = ExecutionPayload {
646 fee_payer: Some(AztecAddress(Fr::from(1u64))),
647 ..Default::default()
648 };
649 let p2 = ExecutionPayload {
650 fee_payer: Some(AztecAddress(Fr::from(2u64))),
651 ..Default::default()
652 };
653
654 let result = ExecutionPayload::merge(vec![p1, p2]);
655 assert!(result.is_err());
656 }
657
658 #[test]
659 fn merge_mixed_fee_payer_takes_defined() {
660 let payer = AztecAddress(Fr::from(3u64));
661 let p1 = ExecutionPayload {
662 fee_payer: None,
663 ..Default::default()
664 };
665 let p2 = ExecutionPayload {
666 fee_payer: Some(payer),
667 ..Default::default()
668 };
669
670 let merged = ExecutionPayload::merge(vec![p1, p2]).expect("mixed payer");
671 assert_eq!(merged.fee_payer, Some(payer));
672 }
673
674 #[test]
675 fn payload_with_calls_roundtrip() {
676 let payload = ExecutionPayload {
677 calls: vec![FunctionCall {
678 to: AztecAddress(Fr::from(1u64)),
679 selector: crate::abi::FunctionSelector::from_hex("0xaabbccdd")
680 .expect("valid selector"),
681 args: vec![AbiValue::Field(Fr::from(42u64))],
682 function_type: FunctionType::Private,
683 is_static: false,
684 hide_msg_sender: false,
685 }],
686 auth_witnesses: vec![AuthWitness {
687 fields: vec![Fr::from(1u64)],
688 ..Default::default()
689 }],
690 capsules: vec![],
691 extra_hashed_args: vec![],
692 fee_payer: Some(AztecAddress(Fr::from(99u64))),
693 };
694
695 let json = serde_json::to_string(&payload).expect("serialize payload");
696 let decoded: ExecutionPayload = serde_json::from_str(&json).expect("deserialize payload");
697 assert_eq!(decoded, payload);
698 }
699}