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);