1
// Copyright (C) Moondance Labs Ltd.
2
// This file is part of Tanssi.
3

            
4
// Tanssi is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Tanssi is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Tanssi.  If not, see <http://www.gnu.org/licenses/>
16

            
17
extern crate alloc;
18

            
19
use crate::processors::v2::{
20
    execute_xcm, fallback_message_processor::AssetTrapFallbackProcessor,
21
    prepare_raw_message_xcm_instructions, CodecError, ExtractedXcmConstructionInfo,
22
    FallbackMessageProcessor, MessageExtractionError, MessageProcessorWithFallback, RawPayload,
23
};
24
use crate::processors::v2::{reanchor_location_to_tanssi, RAW_MESSAGE_PROCESSOR_TOPIC_PREFIX};
25
use alloc::boxed::Box;
26
use alloc::string::ToString;
27
use core::marker::PhantomData;
28
use parity_scale_codec::{Decode, DecodeLimit};
29
use snowbridge_inbound_queue_primitives::v2::{message::Message, MessageProcessorError, Payload};
30
use sp_core::{Get, H160};
31
use sp_runtime::DispatchError;
32
use sp_runtime::Weight;
33
use thiserror::Error;
34
use v2_processor_proc_macro::MessageProcessor;
35
use xcm::latest::ExecuteXcm;
36
use xcm::prelude::{InteriorLocation, NetworkId, VersionedXcm, Xcm};
37
use xcm::{IdentifyVersion, Version, MAX_XCM_DECODE_DEPTH};
38
use xcm_executor::traits::WeightBounds;
39

            
40
#[derive(Error, Debug)]
41
pub enum XcmDecodeError {
42
    #[error("Failed to decode versioned xcm message: {0}")]
43
    VersionedXcmDecodeError(#[from] CodecError),
44
    #[error("Xcm version {version} is not supported")]
45
    UnsupportedXcmVersion { version: Version },
46
}
47

            
48
/// Parse and strictly decode `raw` XCM bytes into a `Xcm<()>`.
49
17
fn decode_raw_xcm<T>(
50
17
    mut data: &[u8],
51
17
) -> Result<Xcm<<T as pallet_xcm::Config>::RuntimeCall>, XcmDecodeError>
52
17
where
53
17
    T: pallet_xcm::Config,
54
{
55
17
    VersionedXcm::<<T as pallet_xcm::Config>::RuntimeCall>::decode_with_depth_limit(
56
        MAX_XCM_DECODE_DEPTH,
57
17
        &mut data,
58
    )
59
17
    .map_err(|e| XcmDecodeError::VersionedXcmDecodeError(e.into()))
60
17
    .and_then(|xcm| {
61
15
        let version = xcm.identify_version();
62
15
        xcm.try_into()
63
15
            .map_err(|_| XcmDecodeError::UnsupportedXcmVersion { version })
64
15
    })
65
17
}
66

            
67
23
pub fn try_extract_message<T>(
68
23
    message: &Message,
69
23
) -> Result<
70
23
    ExtractedXcmConstructionInfo<<T as pallet_xcm::Config>::RuntimeCall>,
71
23
    MessageExtractionError,
72
23
>
73
23
where
74
23
    T: pallet_xcm::Config,
75
{
76
23
    match message.payload {
77
23
        Payload::Raw(ref payload) => {
78
23
            let raw_payload =
79
23
                RawPayload::decode(&mut payload.as_slice()).map_err(|decode_error| {
80
                    MessageExtractionError::InvalidMessage {
81
                        context: "Unable to decode RawPayload".to_string(),
82
                        source: Some(Box::new(CodecError(decode_error))),
83
                    }
84
                })?;
85
23
            match raw_payload {
86
17
                RawPayload::Xcm(payload) => Ok(decode_raw_xcm::<T>(&payload)
87
17
                    .map(|xcm| ExtractedXcmConstructionInfo {
88
15
                        origin: message.origin,
89
15
                        maybe_claimer: message.claimer.clone(),
90
15
                        assets: message.assets.clone(),
91
15
                        eth_value: message.value,
92
15
                        execution_fee_in_eth: message.execution_fee,
93
15
                        nonce: message.nonce,
94
15
                        user_xcm: xcm,
95
15
                    })
96
17
                    .map_err(|error| MessageExtractionError::InvalidMessage {
97
2
                        context: "Unable to decode Xcm".to_string(),
98
2
                        source: Some(Box::new(error)),
99
2
                    })?),
100
6
                _ => Err(MessageExtractionError::UnsupportedMessage {
101
6
                    context: "Message is unsupported".to_string(),
102
6
                    source: None,
103
6
                }),
104
            }
105
        }
106
        _ => Err(MessageExtractionError::UnsupportedMessage {
107
            context: "Message is unsupported".to_string(),
108
            source: None,
109
        }),
110
    }
111
23
}
112

            
113
/// Processes raw XCM messages from Snowbridge V2 with automatic asset trapping fallback.
114
///
115
/// This processor handles messages containing user-provided XCM instructions encoded in the
116
/// `Payload::Raw(RawPayload::Xcm)` format. The workflow is:
117
///
118
/// 1. **Extract**: Decode the `VersionedXcm` from the message payload
119
/// 2. **Prepare**: Wrap user XCM
120
/// 3. **Execute**: Run the prepared XCM on Tanssi relay chain
121
///
122
/// # Fallback Behavior
123
///
124
/// Uses `AssetTrapFallbackProcessor` as fallback for unsupported message types (e.g., Symbiotic).
125
/// This ensures assets are never lost:
126
/// - If message extraction fails → fallback traps assets for claimer recovery
127
/// - If XCM execution fails → still returns success to prevent Ethereum revert, assets become
128
///   trapped and claimable
129
///
130
/// # Error Handling
131
///
132
/// Always returns success even on XCM execution errors to avoid leaving assets in limbo on
133
/// Ethereum. Execution errors are logged but don't cause transaction reversion. This is
134
/// necessary because user XCM may contain non-revertible operations (e.g., cross-chain messages).
135
#[derive(MessageProcessor)]
136
pub struct RawMessageProcessor<
137
    T,
138
    GatewayAddress,
139
    DefaultClaimer,
140
    EthereumNetwork,
141
    EthereumUniversalLocation,
142
    TanssiUniversalLocation,
143
    XcmProcessor,
144
    XcmWeigher,
145
>(
146
    PhantomData<(
147
        T,
148
        GatewayAddress,
149
        DefaultClaimer,
150
        EthereumNetwork,
151
        EthereumUniversalLocation,
152
        TanssiUniversalLocation,
153
        XcmProcessor,
154
        XcmWeigher,
155
    )>,
156
);
157

            
158
impl<
159
        T,
160
        AccountId,
161
        GatewayAddress,
162
        DefaultClaimer,
163
        EthereumNetwork,
164
        EthereumUniversalLocation,
165
        TanssiUniversalLocation,
166
        XcmProcessor,
167
        XcmWeigher,
168
    > MessageProcessorWithFallback<AccountId>
169
    for RawMessageProcessor<
170
        T,
171
        GatewayAddress,
172
        DefaultClaimer,
173
        EthereumNetwork,
174
        EthereumUniversalLocation,
175
        TanssiUniversalLocation,
176
        XcmProcessor,
177
        XcmWeigher,
178
    >
179
where
180
    T: snowbridge_pallet_inbound_queue::Config
181
        + pallet_xcm::Config
182
        + snowbridge_pallet_system::Config,
183
    [u8; 32]: From<<T as frame_system::Config>::AccountId>,
184
    GatewayAddress: Get<H160>,
185
    DefaultClaimer: Get<<T as frame_system::Config>::AccountId>,
186
    EthereumNetwork: Get<NetworkId>,
187
    EthereumUniversalLocation: Get<InteriorLocation>,
188
    TanssiUniversalLocation: Get<InteriorLocation>,
189
    XcmProcessor: ExecuteXcm<<T as pallet_xcm::Config>::RuntimeCall>,
190
    XcmWeigher: WeightBounds<<T as pallet_xcm::Config>::RuntimeCall>,
191
{
192
    type Fallback = AssetTrapFallbackProcessor<
193
        T,
194
        GatewayAddress,
195
        DefaultClaimer,
196
        EthereumNetwork,
197
        EthereumUniversalLocation,
198
        TanssiUniversalLocation,
199
        XcmProcessor,
200
        XcmWeigher,
201
    >;
202
    type ExtractedMessage = ExtractedXcmConstructionInfo<<T as pallet_xcm::Config>::RuntimeCall>;
203

            
204
23
    fn try_extract_message(
205
23
        _sender: &AccountId,
206
23
        message: &Message,
207
23
    ) -> Result<Self::ExtractedMessage, MessageExtractionError> {
208
23
        try_extract_message::<T>(message)
209
23
    }
210

            
211
7
    fn process_extracted_message(
212
7
        _sender: AccountId,
213
7
        extracted_message: Self::ExtractedMessage,
214
7
    ) -> Result<Option<Weight>, MessageProcessorError> {
215
7
        let prepared_xcm = prepare_raw_message_xcm_instructions::<T>(
216
7
            EthereumNetwork::get(),
217
7
            &EthereumUniversalLocation::get(),
218
7
            &TanssiUniversalLocation::get(),
219
7
            GatewayAddress::get(),
220
7
            DefaultClaimer::get(),
221
7
            RAW_MESSAGE_PROCESSOR_TOPIC_PREFIX,
222
7
            extracted_message,
223
        )
224
7
        .map_err(|location_conversion_error| {
225
            log::error!(
226
                "Error while preparing xcm instructions: {:?}",
227
                location_conversion_error
228
            );
229
            MessageProcessorError::ProcessMessage(DispatchError::Other(
230
                "Error while preparing xcm instructions",
231
            ))
232
        })?
233
7
        .into();
234

            
235
7
        let eth_location_reanchored_to_tanssi = reanchor_location_to_tanssi(
236
7
            &EthereumUniversalLocation::get(),
237
7
            &TanssiUniversalLocation::get(),
238
7
            ().into(),
239
        )
240
7
        .map_err(|location_conversion_error| {
241
            log::error!(
242
                "Unable to reanchor eth location to tanssi: {:?}",
243
                location_conversion_error
244
            );
245
            MessageProcessorError::ProcessMessage(DispatchError::Other(
246
                "Unable to reanchor eth location to tanssi",
247
            ))
248
        })?;
249

            
250
        // Depending upon the content of raw xcm, it might be the case that it is not fully revertible
251
        // (i.e xcm that sends a message in another container chain and then return an error).
252
        // Another reason we are not returning error here as otherwise the tx will be reverted and assets will be in limbo in ethereum.
253
        // By returning success here, the assets will be trapped here and claimable by the claimer.
254
7
        if let Err(instruction_error) = execute_xcm::<T, XcmProcessor, XcmWeigher>(
255
7
            eth_location_reanchored_to_tanssi,
256
7
            prepared_xcm,
257
7
        ) {
258
            log::error!(
259
                "Error while executing xcm in raw message processor: {:?}",
260
                instruction_error
261
            );
262
7
        }
263

            
264
        // TODO: Add proper consumed weight
265
7
        Ok(None)
266
7
    }
267
}