cctp_rs/contracts/v2/message_transmitter_v2.rs
1// SPDX-FileCopyrightText: 2025 Semiotic AI, Inc.
2//
3// SPDX-License-Identifier: Apache-2.0
4//! MessageTransmitterV2 contract bindings and wrapper
5//!
6//! This module contains the Alloy-generated contract bindings for the CCTP v2
7//! MessageTransmitter contract, which handles cross-chain message verification
8//! and reception with finality-aware processing.
9
10use alloy_network::Ethereum;
11use alloy_primitives::{Address, Bytes};
12use alloy_provider::Provider;
13use alloy_rpc_types::TransactionRequest;
14use alloy_sol_types::sol;
15use tracing::{debug, info};
16
17use crate::protocol::DomainId;
18use MessageTransmitterV2::MessageTransmitterV2Instance;
19
20/// The CCTP v2 Message Transmitter contract wrapper
21///
22/// Handles message verification and reception with support for different
23/// finality levels (Fast Transfer vs Standard).
24pub struct MessageTransmitterV2Contract<P: Provider<Ethereum>> {
25 instance: MessageTransmitterV2Instance<P>,
26}
27
28impl<P: Provider<Ethereum>> MessageTransmitterV2Contract<P> {
29 /// Create a new MessageTransmitterV2Contract
30 pub fn new(address: Address, provider: P) -> Self {
31 debug!(
32 contract_address = %address,
33 event = "message_transmitter_v2_contract_initialized"
34 );
35 Self {
36 instance: MessageTransmitterV2Instance::<P>::new(address, provider),
37 }
38 }
39
40 /// Create transaction request for receiving a cross-chain message with attestation
41 ///
42 /// # Arguments
43 ///
44 /// * `message` - The message bytes from the source chain
45 /// * `attestation` - Circle's attestation signature for the message
46 /// * `from_address` - Address that will submit the transaction
47 ///
48 /// # Finality Handling
49 ///
50 /// v2 contracts handle different finality levels:
51 /// - Fast Transfer messages (threshold 1000) trigger `handleReceiveUnfinalizedMessage`
52 /// - Standard messages (threshold 2000) trigger `handleReceiveFinalizedMessage`
53 ///
54 /// The receiving contract must implement the appropriate handler interface.
55 pub fn receive_message_transaction(
56 &self,
57 message: Bytes,
58 attestation: Bytes,
59 from_address: Address,
60 ) -> TransactionRequest {
61 info!(
62 message_len = message.len(),
63 attestation_len = attestation.len(),
64 from_address = %from_address,
65 contract_address = %self.instance.address(),
66 version = "v2",
67 event = "receive_message_v2_transaction_created"
68 );
69
70 self.instance
71 .receiveMessage(message, attestation)
72 .from(from_address)
73 .into_transaction_request()
74 }
75
76 /// Create transaction request for sending a generic cross-chain message
77 ///
78 /// v2 adds support for sending arbitrary messages, not just token burns.
79 ///
80 /// # Arguments
81 ///
82 /// * `from_address` - Address initiating the message send
83 /// * `destination_domain` - CCTP domain ID for destination chain
84 /// * `recipient` - Recipient address on destination chain
85 /// * `message_body` - Arbitrary message data
86 /// * `destination_caller` - Optional authorized caller on destination (0x0 = anyone)
87 /// * `min_finality_threshold` - 1000 (fast) or 2000 (standard)
88 pub fn send_message_transaction(
89 &self,
90 from_address: Address,
91 destination_domain: DomainId,
92 recipient: Address,
93 message_body: Bytes,
94 destination_caller: Address,
95 min_finality_threshold: u32,
96 ) -> TransactionRequest {
97 info!(
98 from_address = %from_address,
99 destination_domain = %destination_domain,
100 recipient = %recipient,
101 message_len = message_body.len(),
102 destination_caller = %destination_caller,
103 finality_threshold = min_finality_threshold,
104 contract_address = %self.instance.address(),
105 version = "v2",
106 event = "send_message_v2_transaction_created"
107 );
108
109 self.instance
110 .sendMessage(
111 destination_domain.as_u32(),
112 recipient.into_word(),
113 destination_caller.into_word(),
114 min_finality_threshold,
115 message_body,
116 )
117 .from(from_address)
118 .into_transaction_request()
119 }
120
121 /// Check if a message has been received (anti-replay protection)
122 ///
123 /// Queries the `usedNonces` mapping to determine if a message has already
124 /// been processed. A non-zero value indicates the message was received.
125 ///
126 /// This is useful for checking replay protection before attempting to
127 /// receive a message on the destination chain.
128 pub async fn is_message_received(
129 &self,
130 message_hash: [u8; 32],
131 ) -> Result<bool, alloy_contract::Error> {
132 let nonce_status = self.instance.usedNonces(message_hash.into()).call().await?;
133
134 debug!(
135 message_hash = ?message_hash,
136 nonce_status = %nonce_status,
137 is_received = !nonce_status.is_zero(),
138 event = "is_message_received_checked"
139 );
140
141 // NONCE_USED constant is non-zero, so any non-zero value means received
142 Ok(!nonce_status.is_zero())
143 }
144
145 /// Returns the contract address
146 pub fn address(&self) -> Address {
147 *self.instance.address()
148 }
149}
150
151sol!(
152 #[allow(clippy::too_many_arguments)]
153 #[allow(missing_docs)]
154 #[sol(rpc)]
155 MessageTransmitterV2,
156 "abis/v2/message_transmitter_v2.json"
157);