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