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 request_hash: Fr,
191 #[serde(default)]
193 pub fields: Vec<Fr>,
194}
195
196#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
201pub struct Capsule {
202 pub contract_address: AztecAddress,
204 pub storage_slot: Fr,
206 pub data: Vec<Fr>,
208}
209
210#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
212pub struct HashedValues {
213 #[serde(default)]
215 pub values: Vec<Fr>,
216}
217
218#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
223pub struct ExecutionPayload {
224 #[serde(default)]
226 pub calls: Vec<FunctionCall>,
227 #[serde(default)]
229 pub auth_witnesses: Vec<AuthWitness>,
230 #[serde(default)]
232 pub capsules: Vec<Capsule>,
233 #[serde(default)]
235 pub extra_hashed_args: Vec<HashedValues>,
236 pub fee_payer: Option<AztecAddress>,
238}
239
240impl ExecutionPayload {
241 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}