1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::fmt;
3
4use crate::abi::{AbiValue, FunctionSelector, FunctionType};
5use crate::constants::domain_separator;
6use crate::fee::GasSettings;
7use crate::hash::poseidon2_hash_with_separator;
8#[allow(unused_imports)]
9use crate::kernel_types::PrivateKernelTailPublicInputs;
11use crate::types::{decode_fixed_hex, encode_hex, AztecAddress, Fr};
12use crate::Error;
13
14#[derive(Clone, Copy, PartialEq, Eq, Hash)]
16pub struct TxHash(pub [u8; 32]);
17
18impl TxHash {
19 pub const fn zero() -> Self {
21 Self([0u8; 32])
22 }
23
24 pub fn from_hex(value: &str) -> Result<Self, Error> {
26 Ok(Self(decode_fixed_hex::<32>(value)?))
27 }
28}
29
30impl fmt::Display for TxHash {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 f.write_str(&encode_hex(&self.0))
33 }
34}
35
36impl fmt::Debug for TxHash {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 write!(f, "TxHash({self})")
39 }
40}
41
42impl Serialize for TxHash {
43 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
44 where
45 S: Serializer,
46 {
47 serializer.serialize_str(&self.to_string())
48 }
49}
50
51impl<'de> Deserialize<'de> for TxHash {
52 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
53 where
54 D: Deserializer<'de>,
55 {
56 let s = String::deserialize(deserializer)?;
57 Self::from_hex(&s).map_err(serde::de::Error::custom)
58 }
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "lowercase")]
64pub enum TxStatus {
65 Dropped,
67 Pending,
69 Proposed,
71 Checkpointed,
73 Proven,
75 Finalized,
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(rename_all = "snake_case")]
82pub enum TxExecutionResult {
83 Success,
85 AppLogicReverted,
87 TeardownReverted,
89 BothReverted,
91}
92
93#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct TxReceipt {
97 pub tx_hash: TxHash,
99 pub status: TxStatus,
101 pub execution_result: Option<TxExecutionResult>,
103 pub error: Option<String>,
105 #[serde(default, deserialize_with = "option_u128_from_string_or_number")]
107 pub transaction_fee: Option<u128>,
108 #[serde(default, with = "option_hex_bytes_32")]
110 pub block_hash: Option<[u8; 32]>,
111 pub block_number: Option<u64>,
113 pub epoch_number: Option<u64>,
115}
116
117mod option_hex_bytes_32 {
118 use serde::{Deserialize, Deserializer, Serializer};
119
120 use crate::types::{decode_fixed_hex, encode_hex};
121
122 #[allow(clippy::ref_option)]
123 pub fn serialize<S>(value: &Option<[u8; 32]>, serializer: S) -> Result<S::Ok, S::Error>
124 where
125 S: Serializer,
126 {
127 match value {
128 Some(bytes) => serializer.serialize_some(&encode_hex(bytes)),
129 None => serializer.serialize_none(),
130 }
131 }
132
133 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<[u8; 32]>, D::Error>
134 where
135 D: Deserializer<'de>,
136 {
137 let opt: Option<String> = Option::deserialize(deserializer)?;
138 match opt {
139 Some(s) => {
140 let bytes = decode_fixed_hex::<32>(&s).map_err(serde::de::Error::custom)?;
141 Ok(Some(bytes))
142 }
143 None => Ok(None),
144 }
145 }
146}
147
148fn option_u128_from_string_or_number<'de, D>(deserializer: D) -> Result<Option<u128>, D::Error>
149where
150 D: serde::Deserializer<'de>,
151{
152 let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
153 match value {
154 None | Some(serde_json::Value::Null) => Ok(None),
155 Some(serde_json::Value::Number(n)) => n
156 .as_u64()
157 .map(|v| Some(v as u128))
158 .ok_or_else(|| serde::de::Error::custom("invalid numeric transactionFee")),
159 Some(serde_json::Value::String(s)) => s
160 .parse::<u128>()
161 .map(Some)
162 .map_err(serde::de::Error::custom),
163 Some(other) => Err(serde::de::Error::custom(format!(
164 "invalid transactionFee value: {other}"
165 ))),
166 }
167}
168
169mod base64_buffer {
170 use base64::Engine as _;
171 use serde::{Deserialize, Deserializer, Serializer};
172
173 pub fn serialize<S>(value: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
174 where
175 S: Serializer,
176 {
177 serializer.serialize_str(&base64::engine::general_purpose::STANDARD.encode(value))
178 }
179
180 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
181 where
182 D: Deserializer<'de>,
183 {
184 let encoded = String::deserialize(deserializer)?;
185 base64::engine::general_purpose::STANDARD
186 .decode(encoded)
187 .map_err(serde::de::Error::custom)
188 }
189}
190
191impl TxReceipt {
192 pub const fn is_mined(&self) -> bool {
194 matches!(
195 self.status,
196 TxStatus::Proposed | TxStatus::Checkpointed | TxStatus::Proven | TxStatus::Finalized
197 )
198 }
199
200 pub fn is_pending(&self) -> bool {
202 self.status == TxStatus::Pending
203 }
204
205 pub fn is_dropped(&self) -> bool {
207 self.status == TxStatus::Dropped
208 }
209
210 pub fn has_execution_succeeded(&self) -> bool {
212 self.execution_result == Some(TxExecutionResult::Success)
213 }
214
215 pub fn has_execution_reverted(&self) -> bool {
217 self.execution_result.is_some() && !self.has_execution_succeeded()
218 }
219}
220
221#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
223pub struct FunctionCall {
224 pub to: AztecAddress,
226 pub selector: FunctionSelector,
228 pub args: Vec<AbiValue>,
230 pub function_type: FunctionType,
232 pub is_static: bool,
234 #[serde(default)]
236 pub hide_msg_sender: bool,
237}
238
239impl FunctionCall {
240 pub fn empty() -> Self {
242 Self {
243 to: AztecAddress::zero(),
244 selector: FunctionSelector::empty(),
245 args: vec![],
246 function_type: FunctionType::Private,
247 is_static: false,
248 hide_msg_sender: false,
249 }
250 }
251
252 pub fn is_empty(&self) -> bool {
254 self.to == AztecAddress::zero() && self.selector == FunctionSelector::empty()
255 }
256}
257
258#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
260pub struct AuthWitness {
261 #[serde(default)]
263 pub request_hash: Fr,
264 #[serde(default)]
266 pub fields: Vec<Fr>,
267}
268
269#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
274pub struct Capsule {
275 pub contract_address: AztecAddress,
277 pub storage_slot: Fr,
279 pub data: Vec<Fr>,
281}
282
283#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct TxContext {
290 pub chain_id: Fr,
292 pub version: Fr,
294 pub gas_settings: GasSettings,
296}
297
298impl TxContext {
299 pub fn to_fields(&self) -> Vec<Fr> {
301 let mut fields = Vec::with_capacity(10);
302 fields.push(self.chain_id);
303 fields.push(self.version);
304
305 let gas_limits = self.gas_settings.gas_limits.clone().unwrap_or_default();
306 fields.push(Fr::from(gas_limits.da_gas));
307 fields.push(Fr::from(gas_limits.l2_gas));
308
309 let teardown = self
310 .gas_settings
311 .teardown_gas_limits
312 .clone()
313 .unwrap_or_default();
314 fields.push(Fr::from(teardown.da_gas));
315 fields.push(Fr::from(teardown.l2_gas));
316
317 let max_fee = self
318 .gas_settings
319 .max_fee_per_gas
320 .clone()
321 .unwrap_or_default();
322 fields.push(Fr::from(max_fee.fee_per_da_gas));
323 fields.push(Fr::from(max_fee.fee_per_l2_gas));
324
325 let max_priority = self
326 .gas_settings
327 .max_priority_fee_per_gas
328 .clone()
329 .unwrap_or_default();
330 fields.push(Fr::from(max_priority.fee_per_da_gas));
331 fields.push(Fr::from(max_priority.fee_per_l2_gas));
332
333 fields
334 }
335}
336
337pub fn compute_tx_request_hash(
339 origin: AztecAddress,
340 args_hash: Fr,
341 tx_context: &TxContext,
342 function_selector: FunctionSelector,
343 is_private: bool,
344 salt: Fr,
345) -> Fr {
346 let mut fields = Vec::with_capacity(15);
348 fields.push(origin.0);
349 fields.push(args_hash);
350 fields.extend(tx_context.to_fields());
351 fields.push(function_selector.to_field());
352 fields.push(Fr::from(is_private));
353 fields.push(salt);
354 poseidon2_hash_with_separator(&fields, domain_separator::TX_REQUEST)
355}
356
357#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
359#[serde(rename_all = "camelCase")]
360pub struct HashedValues {
361 #[serde(default)]
363 pub values: Vec<Fr>,
364 #[serde(default)]
366 pub hash: Fr,
367}
368
369impl HashedValues {
370 pub fn from_args(args: Vec<Fr>) -> Self {
372 let hash = crate::hash::compute_var_args_hash(&args);
373 Self { values: args, hash }
374 }
375
376 pub fn from_calldata(calldata: Vec<Fr>) -> Self {
378 let hash = crate::hash::compute_calldata_hash(&calldata);
379 Self {
380 values: calldata,
381 hash,
382 }
383 }
384
385 pub fn hash(&self) -> Fr {
387 self.hash
388 }
389}
390
391#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
393pub struct ContractClassLogFields {
394 #[serde(default)]
396 pub fields: Vec<Fr>,
397}
398
399impl ContractClassLogFields {
400 pub fn from_emitted_fields(mut emitted_fields: Vec<Fr>) -> Self {
402 const CONTRACT_CLASS_LOG_SIZE_IN_FIELDS: usize = 3023;
403 if emitted_fields.len() < CONTRACT_CLASS_LOG_SIZE_IN_FIELDS {
404 emitted_fields.resize(CONTRACT_CLASS_LOG_SIZE_IN_FIELDS, Fr::zero());
405 }
406 Self {
407 fields: emitted_fields,
408 }
409 }
410
411 pub fn emitted_fields(&self) -> &[Fr] {
413 let last = self.fields.iter().rposition(|field| *field != Fr::zero());
414 match last {
415 Some(index) => &self.fields[..=index],
416 None => &[],
417 }
418 }
419}
420
421#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
426#[serde(transparent)]
427pub struct PrivateKernelTailCircuitPublicInputs {
428 #[serde(with = "base64_buffer")]
430 pub bytes: Vec<u8>,
431}
432
433impl PrivateKernelTailCircuitPublicInputs {
434 pub fn from_bytes(bytes: Vec<u8>) -> Self {
436 Self { bytes }
437 }
438}
439
440#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
444#[serde(transparent)]
445pub struct ChonkProof {
446 #[serde(with = "base64_buffer")]
448 pub bytes: Vec<u8>,
449}
450
451impl ChonkProof {
452 pub fn from_bytes(bytes: Vec<u8>) -> Self {
454 Self { bytes }
455 }
456}
457
458#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
460#[serde(rename_all = "camelCase")]
461pub struct Tx {
462 pub data: PrivateKernelTailCircuitPublicInputs,
464 pub chonk_proof: ChonkProof,
466 #[serde(default)]
468 pub contract_class_log_fields: Vec<ContractClassLogFields>,
469 #[serde(default)]
471 pub public_function_calldata: Vec<HashedValues>,
472}
473
474impl Tx {
475 pub fn to_json_value(&self) -> Result<serde_json::Value, Error> {
477 serde_json::to_value(self).map_err(Error::from)
478 }
479}
480
481#[derive(Clone, Debug, PartialEq, Eq)]
486pub struct TypedTx {
487 pub tx_hash: TxHash,
489 pub data: PrivateKernelTailPublicInputs,
491 pub chonk_proof: ChonkProof,
493 pub contract_class_log_fields: Vec<ContractClassLogFields>,
495 pub public_function_calldata: Vec<HashedValues>,
497}
498
499impl TypedTx {
500 pub fn number_of_public_calls(&self) -> usize {
502 self.data.number_of_public_calls()
503 }
504
505 pub fn get_total_public_calldata_count(&self) -> usize {
507 self.public_function_calldata
508 .iter()
509 .map(|hv| hv.values.len())
510 .sum()
511 }
512
513 pub fn get_public_call_requests_with_calldata(
515 &self,
516 ) -> Vec<(&crate::kernel_types::PublicCallRequest, &HashedValues)> {
517 let requests = self.data.get_all_public_call_requests();
518 requests
519 .into_iter()
520 .zip(self.public_function_calldata.iter())
521 .collect()
522 }
523}
524
525#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
530pub struct ExecutionPayload {
531 #[serde(default)]
533 pub calls: Vec<FunctionCall>,
534 #[serde(default)]
536 pub auth_witnesses: Vec<AuthWitness>,
537 #[serde(default)]
539 pub capsules: Vec<Capsule>,
540 #[serde(default)]
542 pub extra_hashed_args: Vec<HashedValues>,
543 pub fee_payer: Option<AztecAddress>,
545}
546
547impl ExecutionPayload {
548 pub fn merge(payloads: Vec<ExecutionPayload>) -> Result<Self, Error> {
554 let mut calls = Vec::new();
555 let mut auth_witnesses = Vec::new();
556 let mut capsules = Vec::new();
557 let mut extra_hashed_args = Vec::new();
558 let mut fee_payer: Option<AztecAddress> = None;
559
560 for payload in payloads {
561 calls.extend(payload.calls);
562 auth_witnesses.extend(payload.auth_witnesses);
563 capsules.extend(payload.capsules);
564 extra_hashed_args.extend(payload.extra_hashed_args);
565
566 if let Some(payer) = payload.fee_payer {
567 if let Some(existing) = fee_payer {
568 if existing != payer {
569 return Err(Error::InvalidData(format!(
570 "conflicting fee payers: {existing} vs {payer}"
571 )));
572 }
573 }
574 fee_payer = Some(payer);
575 }
576 }
577
578 Ok(ExecutionPayload {
579 calls,
580 auth_witnesses,
581 capsules,
582 extra_hashed_args,
583 fee_payer,
584 })
585 }
586}
587
588#[cfg(test)]
589#[allow(clippy::expect_used, clippy::panic)]
590mod tests {
591 use super::*;
592
593 fn make_receipt(status: TxStatus, exec: Option<TxExecutionResult>) -> TxReceipt {
594 TxReceipt {
595 tx_hash: TxHash::zero(),
596 status,
597 execution_result: exec,
598 error: None,
599 transaction_fee: None,
600 block_hash: None,
601 block_number: None,
602 epoch_number: None,
603 }
604 }
605
606 #[test]
607 fn tx_hash_hex_roundtrip() {
608 let hash = TxHash([0xab; 32]);
609 let json = serde_json::to_string(&hash).expect("serialize TxHash");
610 assert!(json.contains("0xabab"), "should serialize as hex string");
611 let decoded: TxHash = serde_json::from_str(&json).expect("deserialize TxHash");
612 assert_eq!(decoded, hash);
613 }
614
615 #[test]
616 fn tx_hash_from_hex() {
617 let hash =
618 TxHash::from_hex("0x0000000000000000000000000000000000000000000000000000000000000001")
619 .expect("valid hex");
620 assert_eq!(hash.0[31], 1);
621 assert_eq!(hash.0[0], 0);
622 }
623
624 #[test]
625 fn tx_hash_display() {
626 let hash = TxHash::zero();
627 let s = hash.to_string();
628 assert_eq!(
629 s,
630 "0x0000000000000000000000000000000000000000000000000000000000000000"
631 );
632 }
633
634 #[test]
635 fn tx_status_roundtrip() {
636 let statuses = [
637 (TxStatus::Dropped, "\"dropped\""),
638 (TxStatus::Pending, "\"pending\""),
639 (TxStatus::Proposed, "\"proposed\""),
640 (TxStatus::Checkpointed, "\"checkpointed\""),
641 (TxStatus::Proven, "\"proven\""),
642 (TxStatus::Finalized, "\"finalized\""),
643 ];
644
645 for (status, expected_json) in statuses {
646 let json = serde_json::to_string(&status).expect("serialize TxStatus");
647 assert_eq!(json, expected_json);
648 let decoded: TxStatus = serde_json::from_str(&json).expect("deserialize TxStatus");
649 assert_eq!(decoded, status);
650 }
651 }
652
653 #[test]
654 fn tx_execution_result_roundtrip() {
655 let results = [
656 TxExecutionResult::Success,
657 TxExecutionResult::AppLogicReverted,
658 TxExecutionResult::TeardownReverted,
659 TxExecutionResult::BothReverted,
660 ];
661
662 for result in results {
663 let json = serde_json::to_string(&result).expect("serialize TxExecutionResult");
664 let decoded: TxExecutionResult =
665 serde_json::from_str(&json).expect("deserialize TxExecutionResult");
666 assert_eq!(decoded, result);
667 }
668 }
669
670 #[test]
671 fn receipt_mined_success() {
672 let receipt = TxReceipt {
673 tx_hash: TxHash::zero(),
674 status: TxStatus::Checkpointed,
675 execution_result: Some(TxExecutionResult::Success),
676 error: None,
677 transaction_fee: Some(1000),
678 block_hash: Some([0x11; 32]),
679 block_number: Some(42),
680 epoch_number: Some(1),
681 };
682
683 assert!(receipt.is_mined());
684 assert!(!receipt.is_pending());
685 assert!(!receipt.is_dropped());
686 assert!(receipt.has_execution_succeeded());
687 assert!(!receipt.has_execution_reverted());
688 }
689
690 #[test]
691 fn receipt_pending() {
692 let receipt = make_receipt(TxStatus::Pending, None);
693 assert!(!receipt.is_mined());
694 assert!(receipt.is_pending());
695 assert!(!receipt.is_dropped());
696 assert!(!receipt.has_execution_succeeded());
697 assert!(!receipt.has_execution_reverted());
698 }
699
700 #[test]
701 fn receipt_dropped() {
702 let receipt = make_receipt(TxStatus::Dropped, None);
703 assert!(!receipt.is_mined());
704 assert!(!receipt.is_pending());
705 assert!(receipt.is_dropped());
706 }
707
708 #[test]
709 fn receipt_reverted() {
710 let receipt = make_receipt(
711 TxStatus::Checkpointed,
712 Some(TxExecutionResult::AppLogicReverted),
713 );
714 assert!(receipt.is_mined());
715 assert!(!receipt.has_execution_succeeded());
716 assert!(receipt.has_execution_reverted());
717 }
718
719 #[test]
720 fn receipt_both_reverted() {
721 let receipt = make_receipt(
722 TxStatus::Checkpointed,
723 Some(TxExecutionResult::BothReverted),
724 );
725 assert!(receipt.has_execution_reverted());
726 }
727
728 #[test]
729 fn receipt_all_mined_statuses() {
730 for status in [
731 TxStatus::Proposed,
732 TxStatus::Checkpointed,
733 TxStatus::Proven,
734 TxStatus::Finalized,
735 ] {
736 let receipt = make_receipt(status, Some(TxExecutionResult::Success));
737 assert!(receipt.is_mined(), "{status:?} should count as mined");
738 }
739 }
740
741 #[test]
742 fn receipt_json_roundtrip() {
743 let receipt = TxReceipt {
744 tx_hash: TxHash::from_hex(
745 "0x00000000000000000000000000000000000000000000000000000000deadbeef",
746 )
747 .expect("valid hex"),
748 status: TxStatus::Finalized,
749 execution_result: Some(TxExecutionResult::Success),
750 error: None,
751 transaction_fee: Some(5000),
752 block_hash: Some([0xcc; 32]),
753 block_number: Some(100),
754 epoch_number: Some(10),
755 };
756
757 let json = serde_json::to_string(&receipt).expect("serialize receipt");
758 assert!(json.contains("deadbeef"), "tx_hash should be hex");
759 assert!(json.contains("0xcc"), "block_hash should be hex");
760
761 let decoded: TxReceipt = serde_json::from_str(&json).expect("deserialize receipt");
762 assert_eq!(decoded, receipt);
763 }
764
765 #[test]
766 fn receipt_json_roundtrip_with_nulls() {
767 let receipt = TxReceipt {
768 tx_hash: TxHash::zero(),
769 status: TxStatus::Pending,
770 execution_result: None,
771 error: None,
772 transaction_fee: None,
773 block_hash: None,
774 block_number: None,
775 epoch_number: None,
776 };
777
778 let json = serde_json::to_string(&receipt).expect("serialize receipt");
779 let decoded: TxReceipt = serde_json::from_str(&json).expect("deserialize receipt");
780 assert_eq!(decoded, receipt);
781 }
782
783 #[test]
784 fn payload_serializes() {
785 let payload = ExecutionPayload::default();
786 let json = serde_json::to_string(&payload).expect("serialize ExecutionPayload");
787 assert!(json.contains("\"calls\":[]"));
788 }
789
790 #[test]
791 fn merge_empty_payloads() {
792 let result = ExecutionPayload::merge(vec![]).expect("merge empty");
793 assert!(result.calls.is_empty());
794 assert!(result.auth_witnesses.is_empty());
795 assert!(result.capsules.is_empty());
796 assert!(result.extra_hashed_args.is_empty());
797 assert!(result.fee_payer.is_none());
798 }
799
800 #[test]
801 fn merge_single_payload() {
802 let payer = AztecAddress(Fr::from(1u64));
803 let payload = ExecutionPayload {
804 calls: vec![FunctionCall {
805 to: AztecAddress(Fr::from(2u64)),
806 selector: FunctionSelector::from_hex("0x11223344").expect("valid"),
807 args: vec![],
808 function_type: FunctionType::Private,
809 is_static: false,
810 hide_msg_sender: false,
811 }],
812 auth_witnesses: vec![AuthWitness {
813 fields: vec![Fr::from(9u64)],
814 ..Default::default()
815 }],
816 capsules: vec![],
817 extra_hashed_args: vec![],
818 fee_payer: Some(payer),
819 };
820
821 let merged = ExecutionPayload::merge(vec![payload]).expect("merge single");
822 assert_eq!(merged.calls.len(), 1);
823 assert_eq!(merged.fee_payer, Some(payer));
824 }
825
826 #[test]
827 fn merge_concatenates_fields() {
828 let p1 = ExecutionPayload {
829 calls: vec![FunctionCall {
830 to: AztecAddress(Fr::from(1u64)),
831 selector: FunctionSelector::from_hex("0x11111111").expect("valid"),
832 args: vec![],
833 function_type: FunctionType::Private,
834 is_static: false,
835 hide_msg_sender: false,
836 }],
837 auth_witnesses: vec![AuthWitness {
838 fields: vec![Fr::from(1u64)],
839 ..Default::default()
840 }],
841 capsules: vec![],
842 extra_hashed_args: vec![],
843 fee_payer: None,
844 };
845
846 let p2 = ExecutionPayload {
847 calls: vec![FunctionCall {
848 to: AztecAddress(Fr::from(2u64)),
849 selector: FunctionSelector::from_hex("0x22222222").expect("valid"),
850 args: vec![],
851 function_type: FunctionType::Public,
852 is_static: false,
853 hide_msg_sender: false,
854 }],
855 auth_witnesses: vec![AuthWitness {
856 fields: vec![Fr::from(2u64)],
857 ..Default::default()
858 }],
859 capsules: vec![],
860 extra_hashed_args: vec![],
861 fee_payer: None,
862 };
863
864 let merged = ExecutionPayload::merge(vec![p1, p2]).expect("merge two");
865 assert_eq!(merged.calls.len(), 2);
866 assert_eq!(merged.auth_witnesses.len(), 2);
867 assert!(merged.fee_payer.is_none());
868 }
869
870 #[test]
871 fn merge_same_fee_payer_succeeds() {
872 let payer = AztecAddress(Fr::from(5u64));
873 let p1 = ExecutionPayload {
874 fee_payer: Some(payer),
875 ..Default::default()
876 };
877 let p2 = ExecutionPayload {
878 fee_payer: Some(payer),
879 ..Default::default()
880 };
881
882 let merged = ExecutionPayload::merge(vec![p1, p2]).expect("same payer");
883 assert_eq!(merged.fee_payer, Some(payer));
884 }
885
886 #[test]
887 fn merge_conflicting_fee_payer_errors() {
888 let p1 = ExecutionPayload {
889 fee_payer: Some(AztecAddress(Fr::from(1u64))),
890 ..Default::default()
891 };
892 let p2 = ExecutionPayload {
893 fee_payer: Some(AztecAddress(Fr::from(2u64))),
894 ..Default::default()
895 };
896
897 let result = ExecutionPayload::merge(vec![p1, p2]);
898 assert!(result.is_err());
899 }
900
901 #[test]
902 fn merge_mixed_fee_payer_takes_defined() {
903 let payer = AztecAddress(Fr::from(3u64));
904 let p1 = ExecutionPayload {
905 fee_payer: None,
906 ..Default::default()
907 };
908 let p2 = ExecutionPayload {
909 fee_payer: Some(payer),
910 ..Default::default()
911 };
912
913 let merged = ExecutionPayload::merge(vec![p1, p2]).expect("mixed payer");
914 assert_eq!(merged.fee_payer, Some(payer));
915 }
916
917 #[test]
918 fn payload_with_calls_roundtrip() {
919 let payload = ExecutionPayload {
920 calls: vec![FunctionCall {
921 to: AztecAddress(Fr::from(1u64)),
922 selector: crate::abi::FunctionSelector::from_hex("0xaabbccdd")
923 .expect("valid selector"),
924 args: vec![AbiValue::Field(Fr::from(42u64))],
925 function_type: FunctionType::Private,
926 is_static: false,
927 hide_msg_sender: false,
928 }],
929 auth_witnesses: vec![AuthWitness {
930 fields: vec![Fr::from(1u64)],
931 ..Default::default()
932 }],
933 capsules: vec![],
934 extra_hashed_args: vec![],
935 fee_payer: Some(AztecAddress(Fr::from(99u64))),
936 };
937
938 let json = serde_json::to_string(&payload).expect("serialize payload");
939 let decoded: ExecutionPayload = serde_json::from_str(&json).expect("deserialize payload");
940 assert_eq!(decoded, payload);
941 }
942}