1use alloy_primitives::{Address, Bytes, FixedBytes, U256};
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14
15use super::DomainId;
16use crate::FinalityThreshold;
17
18fn push_address_word(bytes: &mut Vec<u8>, address: Address) {
19 bytes.extend_from_slice(&[0u8; 12]);
20 bytes.extend_from_slice(address.as_slice());
21}
22
23fn decode_address_word(bytes: &[u8]) -> Option<Address> {
24 (bytes.len() == 32).then(|| Address::from_slice(&bytes[12..32]))
25}
26
27fn bytes_is_empty(bytes: &Bytes) -> bool {
28 bytes.is_empty()
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Error)]
33#[error("invalid CCTP v2 message: {reason}")]
34pub struct ParseMessageError {
35 reason: String,
36}
37
38impl ParseMessageError {
39 fn new(reason: impl Into<String>) -> Self {
40 Self {
41 reason: reason.into(),
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct MessageHeader {
66 pub version: u32,
68 pub source_domain: DomainId,
70 pub destination_domain: DomainId,
72 pub nonce: FixedBytes<32>,
74 pub sender: FixedBytes<32>,
76 pub recipient: FixedBytes<32>,
78 pub destination_caller: FixedBytes<32>,
80 pub min_finality_threshold: u32,
82 pub finality_threshold_executed: u32,
84}
85
86impl MessageHeader {
87 pub const SIZE: usize = 148;
89
90 #[allow(clippy::too_many_arguments)]
92 pub fn new(
93 version: u32,
94 source_domain: DomainId,
95 destination_domain: DomainId,
96 nonce: FixedBytes<32>,
97 sender: FixedBytes<32>,
98 recipient: FixedBytes<32>,
99 destination_caller: FixedBytes<32>,
100 min_finality_threshold: u32,
101 finality_threshold_executed: u32,
102 ) -> Self {
103 Self {
104 version,
105 source_domain,
106 destination_domain,
107 nonce,
108 sender,
109 recipient,
110 destination_caller,
111 min_finality_threshold,
112 finality_threshold_executed,
113 }
114 }
115
116 pub fn encode(&self) -> Bytes {
120 let mut bytes = Vec::with_capacity(Self::SIZE);
121
122 bytes.extend_from_slice(&self.version.to_be_bytes());
124 bytes.extend_from_slice(&self.source_domain.as_u32().to_be_bytes());
126 bytes.extend_from_slice(&self.destination_domain.as_u32().to_be_bytes());
128 bytes.extend_from_slice(self.nonce.as_slice());
130 bytes.extend_from_slice(self.sender.as_slice());
132 bytes.extend_from_slice(self.recipient.as_slice());
134 bytes.extend_from_slice(self.destination_caller.as_slice());
136 bytes.extend_from_slice(&self.min_finality_threshold.to_be_bytes());
138 bytes.extend_from_slice(&self.finality_threshold_executed.to_be_bytes());
140
141 Bytes::from(bytes)
142 }
143
144 pub fn decode(bytes: &[u8]) -> Option<Self> {
149 if bytes.len() < Self::SIZE {
150 return None;
151 }
152
153 let version = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
154
155 let source_domain = u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
156 let source_domain = DomainId::from_u32(source_domain)?;
157
158 let destination_domain = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
159 let destination_domain = DomainId::from_u32(destination_domain)?;
160
161 let nonce = FixedBytes::from_slice(&bytes[12..44]);
162 let sender = FixedBytes::from_slice(&bytes[44..76]);
163 let recipient = FixedBytes::from_slice(&bytes[76..108]);
164 let destination_caller = FixedBytes::from_slice(&bytes[108..140]);
165
166 let min_finality_threshold =
167 u32::from_be_bytes([bytes[140], bytes[141], bytes[142], bytes[143]]);
168 let finality_threshold_executed =
169 u32::from_be_bytes([bytes[144], bytes[145], bytes[146], bytes[147]]);
170
171 Some(Self {
172 version,
173 source_domain,
174 destination_domain,
175 nonce,
176 sender,
177 recipient,
178 destination_caller,
179 min_finality_threshold,
180 finality_threshold_executed,
181 })
182 }
183
184 pub fn parse(bytes: &[u8]) -> std::result::Result<Self, ParseMessageError> {
186 if bytes.len() < Self::SIZE {
187 return Err(ParseMessageError::new(format!(
188 "header requires at least {} bytes, got {}",
189 Self::SIZE,
190 bytes.len()
191 )));
192 }
193
194 Self::decode(bytes).ok_or_else(|| ParseMessageError::new("failed to decode header"))
195 }
196
197 pub fn has_placeholder_nonce(&self) -> bool {
199 self.nonce.as_slice().iter().all(|byte| *byte == 0)
200 }
201
202 #[must_use]
208 pub fn sender_address(&self) -> Address {
209 Address::from_slice(&self.sender.as_slice()[12..32])
210 }
211
212 #[must_use]
218 pub fn recipient_address(&self) -> Address {
219 Address::from_slice(&self.recipient.as_slice()[12..32])
220 }
221
222 #[must_use]
228 pub fn destination_caller_address(&self) -> Option<Address> {
229 (!self.is_permissionless())
230 .then(|| Address::from_slice(&self.destination_caller.as_slice()[12..32]))
231 }
232
233 pub fn is_permissionless(&self) -> bool {
235 self.destination_caller
236 .as_slice()
237 .iter()
238 .all(|byte| *byte == 0)
239 }
240
241 #[must_use]
243 pub fn requested_finality(&self) -> Option<FinalityThreshold> {
244 FinalityThreshold::from_u32(self.min_finality_threshold)
245 }
246
247 #[must_use]
249 pub fn attested_finality(&self) -> Option<FinalityThreshold> {
250 FinalityThreshold::from_u32(self.finality_threshold_executed)
251 }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273pub struct BurnMessageV2 {
274 pub version: u32,
276 pub burn_token: Address,
278 pub mint_recipient: Address,
280 pub amount: U256,
282 pub message_sender: Address,
284 pub max_fee: U256,
286 pub fee_executed: U256,
288 pub expiration_block: U256,
290 pub hook_data: Bytes,
292}
293
294impl BurnMessageV2 {
295 pub const MIN_SIZE: usize = 228;
297
298 pub fn new(
300 burn_token: Address,
301 mint_recipient: Address,
302 amount: U256,
303 message_sender: Address,
304 ) -> Self {
305 Self {
306 version: 1,
307 burn_token,
308 mint_recipient,
309 amount,
310 message_sender,
311 max_fee: U256::ZERO,
312 fee_executed: U256::ZERO,
313 expiration_block: U256::ZERO,
314 hook_data: Bytes::new(),
315 }
316 }
317
318 pub fn new_with_fast_transfer(
320 burn_token: Address,
321 mint_recipient: Address,
322 amount: U256,
323 message_sender: Address,
324 max_fee: U256,
325 ) -> Self {
326 Self {
327 version: 1,
328 burn_token,
329 mint_recipient,
330 amount,
331 message_sender,
332 max_fee,
333 fee_executed: U256::ZERO,
334 expiration_block: U256::ZERO,
335 hook_data: Bytes::new(),
336 }
337 }
338
339 pub fn new_with_hooks(
341 burn_token: Address,
342 mint_recipient: Address,
343 amount: U256,
344 message_sender: Address,
345 hook_data: Bytes,
346 ) -> Self {
347 Self {
348 version: 1,
349 burn_token,
350 mint_recipient,
351 amount,
352 message_sender,
353 max_fee: U256::ZERO,
354 fee_executed: U256::ZERO,
355 expiration_block: U256::ZERO,
356 hook_data,
357 }
358 }
359
360 pub fn with_hook_data(mut self, hook_data: Bytes) -> Self {
362 self.hook_data = hook_data;
363 self
364 }
365
366 pub fn with_max_fee(mut self, max_fee: U256) -> Self {
368 self.max_fee = max_fee;
369 self
370 }
371
372 pub fn with_expiration_block(mut self, expiration_block: U256) -> Self {
374 self.expiration_block = expiration_block;
375 self
376 }
377
378 pub fn encode(&self) -> Bytes {
380 let mut bytes = Vec::with_capacity(Self::MIN_SIZE + self.hook_data.len());
381
382 bytes.extend_from_slice(&self.version.to_be_bytes());
383 push_address_word(&mut bytes, self.burn_token);
384 push_address_word(&mut bytes, self.mint_recipient);
385 bytes.extend_from_slice(&self.amount.to_be_bytes::<32>());
386 push_address_word(&mut bytes, self.message_sender);
387 bytes.extend_from_slice(&self.max_fee.to_be_bytes::<32>());
388 bytes.extend_from_slice(&self.fee_executed.to_be_bytes::<32>());
389 bytes.extend_from_slice(&self.expiration_block.to_be_bytes::<32>());
390 bytes.extend_from_slice(&self.hook_data);
391
392 Bytes::from(bytes)
393 }
394
395 pub fn decode(bytes: &[u8]) -> Option<Self> {
397 if bytes.len() < Self::MIN_SIZE {
398 return None;
399 }
400
401 Some(Self {
402 version: u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
403 burn_token: decode_address_word(&bytes[4..36])?,
404 mint_recipient: decode_address_word(&bytes[36..68])?,
405 amount: U256::from_be_slice(&bytes[68..100]),
406 message_sender: decode_address_word(&bytes[100..132])?,
407 max_fee: U256::from_be_slice(&bytes[132..164]),
408 fee_executed: U256::from_be_slice(&bytes[164..196]),
409 expiration_block: U256::from_be_slice(&bytes[196..228]),
410 hook_data: Bytes::copy_from_slice(&bytes[228..]),
411 })
412 }
413
414 pub fn parse(bytes: &[u8]) -> std::result::Result<Self, ParseMessageError> {
416 if bytes.len() < Self::MIN_SIZE {
417 return Err(ParseMessageError::new(format!(
418 "burn message body requires at least {} bytes, got {}",
419 Self::MIN_SIZE,
420 bytes.len()
421 )));
422 }
423
424 Self::decode(bytes)
425 .ok_or_else(|| ParseMessageError::new("failed to decode burn message body"))
426 }
427
428 pub fn has_hooks(&self) -> bool {
430 !self.hook_data.is_empty()
431 }
432
433 pub fn is_fast_transfer(&self) -> bool {
435 self.max_fee > U256::ZERO
436 }
437}
438
439#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
444pub struct ParsedV2Message {
445 pub header: MessageHeader,
446 pub body: BurnMessageV2,
447}
448
449impl ParsedV2Message {
450 pub fn encode(&self) -> Bytes {
452 let mut bytes = self.header.encode().to_vec();
453 bytes.extend_from_slice(&self.body.encode());
454 Bytes::from(bytes)
455 }
456
457 pub fn decode(bytes: &[u8]) -> Option<Self> {
459 let header = MessageHeader::decode(bytes)?;
460 let body = BurnMessageV2::decode(&bytes[MessageHeader::SIZE..])?;
461 Some(Self { header, body })
462 }
463
464 pub fn parse(bytes: &[u8]) -> std::result::Result<Self, ParseMessageError> {
466 let header = MessageHeader::parse(bytes)?;
467 let body = BurnMessageV2::parse(&bytes[MessageHeader::SIZE..])?;
468 Ok(Self { header, body })
469 }
470
471 #[must_use]
473 pub fn message_hash(&self) -> FixedBytes<32> {
474 alloy_primitives::keccak256(self.encode())
475 }
476
477 #[must_use]
483 pub fn summary(&self) -> ParsedV2MessageSummary {
484 let encoded = self.encode();
485 let message_hash = alloy_primitives::keccak256(&encoded);
486 let message_len_bytes = encoded.len();
487
488 ParsedV2MessageSummary {
489 message_hash,
490 message_len_bytes,
491 source_domain: self.header.source_domain,
492 destination_domain: self.header.destination_domain,
493 message_version: self.header.version,
494 body_version: self.body.version,
495 nonce: self.header.nonce,
496 has_placeholder_nonce: self.header.has_placeholder_nonce(),
497 sender: self.header.sender_address(),
498 recipient: self.header.recipient_address(),
499 destination_caller: self.header.destination_caller_address(),
500 permissionless_relay: self.header.is_permissionless(),
501 requested_finality: self.header.requested_finality(),
502 attested_finality: self.header.attested_finality(),
503 burn_token: self.body.burn_token,
504 mint_recipient: self.body.mint_recipient,
505 amount: self.body.amount,
506 message_sender: self.body.message_sender,
507 max_fee: self.body.max_fee,
508 fee_executed: self.body.fee_executed,
509 expiration_block: self.body.expiration_block,
510 hook_data: self.body.hook_data.clone(),
511 hook_data_len_bytes: self.body.hook_data.len(),
512 has_hooks: self.body.has_hooks(),
513 is_fast_transfer: self.body.is_fast_transfer(),
514 }
515 }
516}
517
518#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
524pub struct ParsedV2MessageSummary {
525 pub message_hash: FixedBytes<32>,
526 pub message_len_bytes: usize,
527 pub source_domain: DomainId,
528 pub destination_domain: DomainId,
529 pub message_version: u32,
530 pub body_version: u32,
531 pub nonce: FixedBytes<32>,
532 pub has_placeholder_nonce: bool,
533 pub sender: Address,
534 pub recipient: Address,
535 #[serde(default, skip_serializing_if = "Option::is_none")]
536 pub destination_caller: Option<Address>,
537 pub permissionless_relay: bool,
538 #[serde(default, skip_serializing_if = "Option::is_none")]
539 pub requested_finality: Option<FinalityThreshold>,
540 #[serde(default, skip_serializing_if = "Option::is_none")]
541 pub attested_finality: Option<FinalityThreshold>,
542 pub burn_token: Address,
543 pub mint_recipient: Address,
544 pub amount: U256,
545 pub message_sender: Address,
546 pub max_fee: U256,
547 pub fee_executed: U256,
548 pub expiration_block: U256,
549 #[serde(default, skip_serializing_if = "bytes_is_empty")]
550 pub hook_data: Bytes,
551 pub hook_data_len_bytes: usize,
552 pub has_hooks: bool,
553 pub is_fast_transfer: bool,
554}
555
556impl ParsedV2MessageSummary {
557 pub fn parse(bytes: &[u8]) -> std::result::Result<Self, ParseMessageError> {
559 ParsedV2Message::parse(bytes).map(|message| message.summary())
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566 use alloy_primitives::{address, hex};
567
568 #[test]
569 fn test_message_header_size() {
570 assert_eq!(MessageHeader::SIZE, 148);
571 }
572
573 #[test]
574 fn test_message_header_encode_decode() {
575 let header = MessageHeader::new(
576 1,
577 DomainId::Ethereum,
578 DomainId::Arbitrum,
579 FixedBytes::from([1u8; 32]),
580 FixedBytes::from([2u8; 32]),
581 FixedBytes::from([3u8; 32]),
582 FixedBytes::from([0u8; 32]),
583 1000,
584 1000,
585 );
586
587 let encoded = header.encode();
588 assert_eq!(encoded.len(), MessageHeader::SIZE);
589
590 let decoded = MessageHeader::decode(&encoded).expect("should decode");
591 assert_eq!(header, decoded);
592 }
593
594 #[test]
595 fn test_message_header_decode_too_short() {
596 let short_bytes = vec![0u8; 100];
597 assert!(MessageHeader::decode(&short_bytes).is_none());
598 }
599
600 #[test]
601 fn test_message_header_decode_invalid_domain() {
602 let mut bytes = vec![0u8; MessageHeader::SIZE];
603 bytes[4..8].copy_from_slice(&999u32.to_be_bytes());
605 assert!(MessageHeader::decode(&bytes).is_none());
606 }
607
608 #[test]
609 fn test_burn_message_v2_new() {
610 let burn_token = address!("A2d2a41577ce14e20a6c2de999A8Ec2BD9fe34aF");
611 let mint_recipient = address!("742d35Cc6634C0532925a3b844Bc9e7595f8fA0d");
612 let amount = U256::from(1000000u64);
613 let sender = address!("1234567890abcdef1234567890abcdef12345678");
614
615 let msg = BurnMessageV2::new(burn_token, mint_recipient, amount, sender);
616
617 assert_eq!(msg.version, 1);
618 assert_eq!(msg.burn_token, burn_token);
619 assert_eq!(msg.mint_recipient, mint_recipient);
620 assert_eq!(msg.amount, amount);
621 assert_eq!(msg.message_sender, sender);
622 assert_eq!(msg.max_fee, U256::ZERO);
623 assert_eq!(msg.fee_executed, U256::ZERO);
624 assert_eq!(msg.expiration_block, U256::ZERO);
625 assert!(msg.hook_data.is_empty());
626 assert!(!msg.has_hooks());
627 assert!(!msg.is_fast_transfer());
628 }
629
630 #[test]
631 fn test_burn_message_v2_fast_transfer() {
632 let burn_token = address!("A2d2a41577ce14e20a6c2de999A8Ec2BD9fe34aF");
633 let mint_recipient = address!("742d35Cc6634C0532925a3b844Bc9e7595f8fA0d");
634 let amount = U256::from(1000000u64);
635 let sender = address!("1234567890abcdef1234567890abcdef12345678");
636 let max_fee = U256::from(100u64);
637
638 let msg = BurnMessageV2::new_with_fast_transfer(
639 burn_token,
640 mint_recipient,
641 amount,
642 sender,
643 max_fee,
644 );
645
646 assert_eq!(msg.max_fee, max_fee);
647 assert!(msg.is_fast_transfer());
648 assert!(!msg.has_hooks());
649 }
650
651 #[test]
652 fn test_burn_message_v2_with_hooks() {
653 let burn_token = address!("A2d2a41577ce14e20a6c2de999A8Ec2BD9fe34aF");
654 let mint_recipient = address!("742d35Cc6634C0532925a3b844Bc9e7595f8fA0d");
655 let amount = U256::from(1000000u64);
656 let sender = address!("1234567890abcdef1234567890abcdef12345678");
657 let hook_data = Bytes::from(vec![1, 2, 3, 4]);
658
659 let msg = BurnMessageV2::new_with_hooks(
660 burn_token,
661 mint_recipient,
662 amount,
663 sender,
664 hook_data.clone(),
665 );
666
667 assert_eq!(msg.hook_data, hook_data);
668 assert!(msg.has_hooks());
669 assert!(!msg.is_fast_transfer());
670 }
671
672 #[test]
673 fn test_burn_message_v2_builder() {
674 let burn_token = address!("A2d2a41577ce14e20a6c2de999A8Ec2BD9fe34aF");
675 let mint_recipient = address!("742d35Cc6634C0532925a3b844Bc9e7595f8fA0d");
676 let amount = U256::from(1000000u64);
677 let sender = address!("1234567890abcdef1234567890abcdef12345678");
678
679 let msg = BurnMessageV2::new(burn_token, mint_recipient, amount, sender)
680 .with_max_fee(U256::from(100u64))
681 .with_hook_data(Bytes::from(vec![1, 2, 3]))
682 .with_expiration_block(U256::from(1000u64));
683
684 assert!(msg.is_fast_transfer());
685 assert!(msg.has_hooks());
686 assert_eq!(msg.expiration_block, U256::from(1000u64));
687 }
688
689 #[test]
690 fn test_burn_message_v2_encode_decode_roundtrip() {
691 let message = BurnMessageV2::new_with_fast_transfer(
692 address!("75FaF114EAFb1bdbE2f0316Df893Fd58ce46AA4D"),
693 address!("7F7D081724F0240c64C9E01CDe4626602f9a0192"),
694 U256::from(1_000_000u64),
695 address!("1234567890abcdef1234567890abcdef12345678"),
696 U256::from(100u64),
697 )
698 .with_hook_data(Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]))
699 .with_expiration_block(U256::from(12345u64));
700
701 let encoded = message.encode();
702 let decoded = BurnMessageV2::decode(&encoded).expect("burn message should decode");
703
704 assert_eq!(decoded, message);
705 }
706
707 #[test]
708 fn test_message_header_permissionless_helpers() {
709 let header = MessageHeader::new(
710 1,
711 DomainId::Ethereum,
712 DomainId::Base,
713 FixedBytes::from([0u8; 32]),
714 address!("75FaF114EAFb1bdbE2f0316Df893Fd58ce46AA4D").into_word(),
715 address!("7F7D081724F0240c64C9E01CDe4626602f9a0192").into_word(),
716 FixedBytes::ZERO,
717 FinalityThreshold::Fast.as_u32(),
718 FinalityThreshold::Standard.as_u32(),
719 );
720
721 assert!(header.has_placeholder_nonce());
722 assert!(header.is_permissionless());
723 assert_eq!(
724 header.sender_address(),
725 address!("75FaF114EAFb1bdbE2f0316Df893Fd58ce46AA4D")
726 );
727 assert_eq!(
728 header.recipient_address(),
729 address!("7F7D081724F0240c64C9E01CDe4626602f9a0192")
730 );
731 assert_eq!(header.requested_finality(), Some(FinalityThreshold::Fast));
732 assert_eq!(
733 header.attested_finality(),
734 Some(FinalityThreshold::Standard)
735 );
736 assert_eq!(header.destination_caller_address(), None);
737 }
738
739 #[test]
740 fn test_parsed_v2_message_from_real_circle_message() {
741 let raw_message = hex::decode("0000000100000003000000062f3cb13cf4a6103f9e3b256495b08c4e05630fcba639565d199ed420a5f2be010000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000000000000000000000000000000000000000000000000007d0000007d00000000100000000000000000000000075faf114eafb1bdbe2f0316df893fd58ce46aa4d0000000000000000000000007f7d081724f0240c64c9e01cde4626602f9a019200000000000000000000000000000000000000000000000000000000000f42400000000000000000000000007f7d081724f0240c64c9e01cde4626602f9a0192000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
742
743 let parsed = ParsedV2Message::parse(&raw_message).expect("message should parse");
744 let summary = parsed.summary();
745
746 assert_eq!(parsed.header.source_domain, DomainId::Arbitrum);
747 assert_eq!(parsed.header.destination_domain, DomainId::Base);
748 assert!(!parsed.header.has_placeholder_nonce());
749 assert_eq!(
750 parsed.header.requested_finality(),
751 Some(FinalityThreshold::Standard)
752 );
753 assert_eq!(
754 parsed.header.attested_finality(),
755 Some(FinalityThreshold::Standard)
756 );
757 assert_eq!(
758 parsed.body.burn_token,
759 address!("75FaF114EAFb1bdbE2f0316Df893Fd58ce46AA4D")
760 );
761 assert_eq!(
762 parsed.body.mint_recipient,
763 address!("7F7D081724F0240c64C9E01CDe4626602f9a0192")
764 );
765 assert_eq!(parsed.body.amount, U256::from(1_000_000u64));
766 assert_eq!(
767 parsed.body.message_sender,
768 address!("7F7D081724F0240c64C9E01CDe4626602f9a0192")
769 );
770 assert_eq!(parsed.body.max_fee, U256::ZERO);
771 assert_eq!(parsed.body.fee_executed, U256::ZERO);
772 assert_eq!(parsed.body.expiration_block, U256::ZERO);
773 assert!(parsed.body.hook_data.is_empty());
774 assert_eq!(parsed.encode().as_ref(), raw_message.as_slice());
775 assert_eq!(
776 parsed.message_hash(),
777 alloy_primitives::keccak256(&raw_message)
778 );
779 assert_eq!(
780 summary.message_hash,
781 alloy_primitives::keccak256(&raw_message)
782 );
783 assert!(summary.permissionless_relay);
784 assert!(!summary.has_hooks);
785 assert!(!summary.is_fast_transfer);
786 }
787
788 #[test]
789 fn test_parsed_v2_message_summary_omits_empty_optionals() {
790 let summary = ParsedV2MessageSummary {
791 message_hash: FixedBytes::from([0x11; 32]),
792 message_len_bytes: 376,
793 source_domain: DomainId::Ethereum,
794 destination_domain: DomainId::Base,
795 message_version: 1,
796 body_version: 1,
797 nonce: FixedBytes::from([0x22; 32]),
798 has_placeholder_nonce: false,
799 sender: address!("75FaF114EAFb1bdbE2f0316Df893Fd58ce46AA4D"),
800 recipient: address!("7F7D081724F0240c64C9E01CDe4626602f9a0192"),
801 destination_caller: None,
802 permissionless_relay: true,
803 requested_finality: Some(FinalityThreshold::Standard),
804 attested_finality: Some(FinalityThreshold::Standard),
805 burn_token: address!("75FaF114EAFb1bdbE2f0316Df893Fd58ce46AA4D"),
806 mint_recipient: address!("7F7D081724F0240c64C9E01CDe4626602f9a0192"),
807 amount: U256::from(1_000_000u64),
808 message_sender: address!("7F7D081724F0240c64C9E01CDe4626602f9a0192"),
809 max_fee: U256::ZERO,
810 fee_executed: U256::ZERO,
811 expiration_block: U256::ZERO,
812 hook_data: Bytes::new(),
813 hook_data_len_bytes: 0,
814 has_hooks: false,
815 is_fast_transfer: false,
816 };
817
818 let json = serde_json::to_value(summary).expect("summary should serialize");
819 assert!(json.get("destination_caller").is_none());
820 assert!(json.get("hook_data").is_none());
821 }
822}