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
// TODO:
18
// Rewrite of the following code which cause issues as Tanssi is not a parachain
19
// https://github.com/moondance-labs/polkadot-sdk/blob/tanssi-polkadot-stable2412/bridges/snowbridge/primitives/router/src/outbound/mod.rs#L98
20

            
21
use crate::{match_expression, XcmConverterError};
22
use alloc::vec::Vec;
23
use core::iter::Peekable;
24
use core::marker::PhantomData;
25
use core::slice::Iter;
26
use frame_support::{ensure, traits::Get};
27
use parity_scale_codec::{Decode, Encode};
28
use snowbridge_core::{AgentId, ChannelId, TokenId};
29
use snowbridge_outbound_queue_primitives::v1::{
30
    message::{Command, Message, SendMessage},
31
    AgentExecuteCommand,
32
};
33
use sp_core::H160;
34
use sp_runtime::traits::{MaybeEquivalence, TryConvert};
35
use xcm::prelude::*;
36
use xcm::{
37
    latest::SendError::{MissingArgument, NotApplicable},
38
    VersionedLocation, VersionedXcm,
39
};
40
use xcm_builder::{ensure_is_remote, InspectMessageQueues};
41
use xcm_executor::traits::{validate_export, ExportXcm};
42

            
43
pub struct EthereumBlobExporter<
44
    UniversalLocation,
45
    EthereumNetwork,
46
    OutboundQueue,
47
    ConvertAssetId,
48
    BridgeChannelInfo,
49
>(
50
    PhantomData<(
51
        UniversalLocation,
52
        EthereumNetwork,
53
        OutboundQueue,
54
        ConvertAssetId,
55
        BridgeChannelInfo,
56
    )>,
57
);
58

            
59
impl<UniversalLocation, EthereumNetwork, OutboundQueue, ConvertAssetId, BridgeChannelInfo> ExportXcm
60
    for EthereumBlobExporter<
61
        UniversalLocation,
62
        EthereumNetwork,
63
        OutboundQueue,
64
        ConvertAssetId,
65
        BridgeChannelInfo,
66
    >
67
where
68
    UniversalLocation: Get<InteriorLocation>,
69
    EthereumNetwork: Get<NetworkId>,
70
    OutboundQueue: SendMessage<Balance = u128>,
71
    ConvertAssetId: MaybeEquivalence<TokenId, Location>,
72
    BridgeChannelInfo: Get<Option<(ChannelId, AgentId)>>,
73
{
74
    type Ticket = (Vec<u8>, XcmHash);
75

            
76
90
    fn validate(
77
90
        network: NetworkId,
78
90
        _channel: u32,
79
90
        universal_source: &mut Option<InteriorLocation>,
80
90
        destination: &mut Option<InteriorLocation>,
81
90
        message: &mut Option<Xcm<()>>,
82
90
    ) -> SendResult<Self::Ticket> {
83
90
        let expected_network = EthereumNetwork::get();
84
90
        let universal_location = UniversalLocation::get();
85

            
86
90
        log::trace!(target: "xcm::ethereum_blob_exporter", "validate params: network={network:?}, _channel={_channel:?}, universal_source={universal_source:?}, destination={destination:?}, message={message:?}");
87

            
88
90
        if network != expected_network {
89
            log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched bridge network {network:?}.");
90
            return Err(SendError::NotApplicable);
91
90
        }
92

            
93
        // Cloning destination to avoid modifying the value so subsequent exporters can use it.
94
90
        let dest = destination.clone().ok_or(SendError::MissingArgument)?;
95
90
        if dest != Here {
96
            log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched remote destination {dest:?}.");
97
            return Err(SendError::NotApplicable);
98
90
        }
99

            
100
        // Cloning universal_source to avoid modifying the value so subsequent exporters can use it.
101
90
        let (local_net, local_sub) = universal_source.clone()
102
90
            .ok_or_else(|| {
103
                log::error!(target: "xcm::ethereum_blob_exporter", "universal source not provided.");
104
                SendError::MissingArgument
105
            })?
106
90
            .split_global()
107
90
            .map_err(|()| {
108
                log::error!(target: "xcm::ethereum_blob_exporter", "could not get global consensus from universal source '{universal_source:?}'.");
109
                SendError::NotApplicable
110
            })?;
111

            
112
90
        if Ok(local_net) != universal_location.global_consensus() {
113
            log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched relay network {local_net:?}.");
114
            return Err(SendError::NotApplicable);
115
90
        }
116

            
117
        // TODO: Support source being a parachain.
118
90
        if !matches!(local_sub, Junctions::Here) {
119
            log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched sub network {local_sub:?}.");
120
            return Err(SendError::NotApplicable);
121
90
        }
122

            
123
90
        let (channel_id, agent_id) = BridgeChannelInfo::get().ok_or_else(|| {
124
2
            log::error!(target: "xcm::ethereum_blob_exporter", "channel id and agent id cannot be fetched");
125
2
            SendError::Unroutable
126
2
        })?;
127

            
128
88
        let message = message.take().ok_or_else(|| {
129
            log::error!(target: "xcm::ethereum_blob_exporter", "xcm message not provided.");
130
            SendError::MissingArgument
131
        })?;
132

            
133
88
        let mut converter =
134
88
            XcmConverter::<ConvertAssetId, ()>::new(&message, expected_network, agent_id);
135
88
        let (command, message_id) = converter.convert().map_err(|err|{
136
4
            log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to pattern matching error '{err:?}'.");
137
4
            SendError::Unroutable
138
4
        })?;
139

            
140
84
        let outbound_message = Message {
141
84
            id: Some(message_id.into()),
142
84
            channel_id,
143
84
            command,
144
84
        };
145

            
146
        // validate the message
147
84
        let (ticket, fee) = OutboundQueue::validate(&outbound_message).map_err(|err| {
148
            log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue validation of message failed. {err:?}");
149
            SendError::Unroutable
150
        })?;
151

            
152
        // convert fee to Asset
153
84
        let fee = Asset::from((Location::here(), fee.total())).into();
154

            
155
84
        Ok(((ticket.encode(), message_id), fee))
156
90
    }
157

            
158
44
    fn deliver(blob: (Vec<u8>, XcmHash)) -> Result<XcmHash, SendError> {
159
44
        let ticket: OutboundQueue::Ticket = OutboundQueue::Ticket::decode(&mut blob.0.as_ref())
160
44
            .map_err(|_| {
161
                log::trace!(target: "xcm::ethereum_blob_exporter", "undeliverable due to decoding error");
162
                SendError::NotApplicable
163
            })?;
164

            
165
44
        let message_id = OutboundQueue::deliver(ticket).map_err(|_| {
166
            log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue submit of message failed");
167
            SendError::Transport("other transport error")
168
        })?;
169

            
170
44
        log::info!(target: "xcm::ethereum_blob_exporter", "message delivered {message_id:#?}.");
171
44
        Ok(message_id.into())
172
44
    }
173
}
174

            
175
pub struct XcmConverter<'a, ConvertAssetId, Call> {
176
    iter: Peekable<Iter<'a, Instruction<Call>>>,
177
    ethereum_network: NetworkId,
178
    agent_id: AgentId,
179
    _marker: PhantomData<ConvertAssetId>,
180
}
181
impl<'a, ConvertAssetId, Call> XcmConverter<'a, ConvertAssetId, Call>
182
where
183
    ConvertAssetId: MaybeEquivalence<TokenId, Location>,
184
{
185
89
    pub fn new(message: &'a Xcm<Call>, ethereum_network: NetworkId, agent_id: AgentId) -> Self {
186
89
        Self {
187
89
            iter: message.inner().iter().peekable(),
188
89
            ethereum_network,
189
89
            agent_id,
190
89
            _marker: Default::default(),
191
89
        }
192
89
    }
193

            
194
88
    pub fn convert(&mut self) -> Result<(Command, [u8; 32]), XcmConverterError> {
195
88
        let result = match self.peek() {
196
            Ok(ReserveAssetDeposited { .. }) => self.make_mint_foreign_token_command(),
197
            // Get withdraw/deposit and make native tokens create message.
198
84
            Ok(WithdrawAsset { .. }) => self.make_unlock_native_token_command(),
199
            Err(e) => Err(e),
200
4
            _ => return Err(XcmConverterError::UnexpectedInstruction),
201
        }?;
202

            
203
        // All xcm instructions must be consumed before exit.
204
84
        if self.next().is_ok() {
205
            return Err(XcmConverterError::EndOfXcmMessageExpected);
206
84
        }
207

            
208
84
        Ok(result)
209
88
    }
210

            
211
85
    pub fn make_unlock_native_token_command(
212
85
        &mut self,
213
85
    ) -> Result<(Command, [u8; 32]), XcmConverterError> {
214
        use XcmConverterError::*;
215

            
216
        // Get the reserve assets from WithdrawAsset.
217
85
        let reserve_assets =
218
85
            match_expression!(self.next()?, WithdrawAsset(reserve_assets), reserve_assets)
219
85
                .ok_or(WithdrawAssetExpected)?;
220

            
221
        // Check if clear origin exists and skip over it.
222
85
        if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() {
223
83
            let _ = self.next();
224
83
        }
225

            
226
        // Get the fee asset item from BuyExecution or continue parsing.
227
85
        let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees);
228
85
        if fee_asset.is_some() {
229
3
            let _ = self.next();
230
83
        }
231

            
232
85
        let (deposit_assets, beneficiary) = match_expression!(
233
85
            self.next()?,
234
            DepositAsset {
235
85
                assets,
236
85
                beneficiary
237
            },
238
85
            (assets, beneficiary)
239
        )
240
85
        .ok_or(DepositAssetExpected)?;
241

            
242
        // assert that the beneficiary is AccountKey20.
243
85
        let recipient = match_expression!(
244
85
            beneficiary.unpack(),
245
85
            (0, [AccountKey20 { network, key }])
246
85
                if self.network_matches(network),
247
85
            H160(*key)
248
        )
249
85
        .ok_or(BeneficiaryResolutionFailed)?;
250

            
251
        // Make sure there are reserved assets.
252
85
        if reserve_assets.len() == 0 {
253
            return Err(NoReserveAssets);
254
85
        }
255

            
256
        // Check the the deposit asset filter matches what was reserved.
257
85
        if reserve_assets
258
85
            .inner()
259
85
            .iter()
260
85
            .any(|asset| !deposit_assets.matches(asset))
261
        {
262
            return Err(FilterDoesNotConsumeAllAssets);
263
85
        }
264

            
265
        // We only support a single asset at a time.
266
85
        ensure!(reserve_assets.len() == 1, TooManyAssets);
267
85
        let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?;
268

            
269
        // Fees are collected on Tanssi, up front and directly from the user, to cover the
270
        // complete cost of the transfer. Any additional fees provided in the XCM program are
271
        // refunded to the beneficiary. We only validate the fee here if its provided to make sure
272
        // the XCM program is well formed. Another way to think about this from an XCM perspective
273
        // would be that the user offered to pay X amount in fees, but we charge 0 of that X amount
274
        // (no fee) and refund X to the user.
275
85
        if let Some(fee_asset) = fee_asset {
276
            // The fee asset must be the same as the reserve asset.
277
3
            if fee_asset.id != reserve_asset.id || fee_asset.fun > reserve_asset.fun {
278
                return Err(InvalidFeeAsset);
279
3
            }
280
82
        }
281

            
282
85
        let (token, amount) = match reserve_asset {
283
            Asset {
284
85
                id: AssetId(inner_location),
285
85
                fun: Fungible(amount),
286
85
            } => match inner_location.unpack() {
287
85
                (0, [AccountKey20 { network, key }]) if self.network_matches(network) => {
288
85
                    Some((H160(*key), *amount))
289
                }
290
                // Native ETH token
291
                (0, []) => Some((H160::zero(), *amount)),
292
                _ => None,
293
            },
294
            _ => None,
295
        }
296
85
        .ok_or(AssetResolutionFailed)?;
297

            
298
        // transfer amount must be greater than 0.
299
85
        ensure!(amount > 0, ZeroAssetTransfer);
300

            
301
        // Check if there is a SetTopic and skip over it if found.
302
85
        let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?;
303

            
304
85
        Ok((
305
85
            // TODO: This should be changed to UnlockNativeToken once we migrate to Snowbridge V2.
306
85
            Command::AgentExecute {
307
85
                agent_id: self.agent_id,
308
85
                command: AgentExecuteCommand::TransferToken {
309
85
                    token,
310
85
                    recipient,
311
85
                    amount,
312
85
                },
313
85
            },
314
85
            *topic_id,
315
85
        ))
316
85
    }
317

            
318
425
    fn next(&mut self) -> Result<&'a Instruction<Call>, XcmConverterError> {
319
425
        self.iter
320
425
            .next()
321
425
            .ok_or(XcmConverterError::UnexpectedEndOfXcm)
322
425
    }
323

            
324
258
    fn peek(&mut self) -> Result<&&'a Instruction<Call>, XcmConverterError> {
325
258
        self.iter
326
258
            .peek()
327
258
            .ok_or(XcmConverterError::UnexpectedEndOfXcm)
328
258
    }
329

            
330
170
    fn network_matches(&self, network: &Option<NetworkId>) -> bool {
331
170
        if let Some(network) = network {
332
170
            *network == self.ethereum_network
333
        } else {
334
            true
335
        }
336
170
    }
337

            
338
    /// Convert the xcm for Polkadot-native token from the origin chain (container chain) into the Command
339
    /// To match transfers of Polkadot-native tokens, we expect an input of the form:
340
    /// # ReserveAssetDeposited
341
    /// # ClearOrigin
342
    /// # BuyExecution
343
    /// # DepositAsset
344
    /// # SetTopic
345
    fn make_mint_foreign_token_command(
346
        &mut self,
347
    ) -> Result<(Command, [u8; 32]), XcmConverterError> {
348
        // TODO: This function will be used only when we start receiving tokens from containers.
349
        // The whole struct is copied from Snowbridge and modified for our needs, and thus function
350
        // will be modified in a latter PR.
351
        todo!("make_mint_foreign_token_command");
352

            
353
        // use XcmConverterError::*;
354

            
355
        // // Get the reserve assets.
356
        // let reserve_assets = match_expression!(
357
        //     self.next()?,
358
        //     ReserveAssetDeposited(reserve_assets),
359
        //     reserve_assets
360
        // )
361
        // .ok_or(ReserveAssetDepositedExpected)?;
362

            
363
        // // Check if clear origin exists and skip over it.
364
        // if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() {
365
        //     let _ = self.next();
366
        // }
367

            
368
        // // Get the fee asset item from BuyExecution or continue parsing.
369
        // let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees);
370
        // if fee_asset.is_some() {
371
        //     let _ = self.next();
372
        // }
373

            
374
        // let (deposit_assets, beneficiary) = match_expression!(
375
        //     self.next()?,
376
        //     DepositAsset {
377
        //         assets,
378
        //         beneficiary
379
        //     },
380
        //     (assets, beneficiary)
381
        // )
382
        // .ok_or(DepositAssetExpected)?;
383

            
384
        // // assert that the beneficiary is AccountKey20.
385
        // let recipient = match_expression!(
386
        //     beneficiary.unpack(),
387
        //     (0, [AccountKey20 { network, key }])
388
        //         if self.network_matches(network),
389
        //     H160(*key)
390
        // )
391
        // .ok_or(BeneficiaryResolutionFailed)?;
392

            
393
        // // Make sure there are reserved assets.
394
        // if reserve_assets.len() == 0 {
395
        //     return Err(NoReserveAssets);
396
        // }
397

            
398
        // // Check the the deposit asset filter matches what was reserved.
399
        // if reserve_assets
400
        //     .inner()
401
        //     .iter()
402
        //     .any(|asset| !deposit_assets.matches(asset))
403
        // {
404
        //     return Err(FilterDoesNotConsumeAllAssets);
405
        // }
406

            
407
        // // We only support a single asset at a time.
408
        // ensure!(reserve_assets.len() == 1, TooManyAssets);
409
        // let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?;
410

            
411
        // // Fees are collected on the origin chain (container chain), up front and directly from the
412
        // // user, to cover the complete cost of the transfer. Any additional fees provided in the XCM
413
        // // program are refunded to the beneficiary. We only validate the fee here if its provided to
414
        // // make sure the XCM program is well formed. Another way to think about this from an XCM
415
        // // perspective would be that the user offered to pay X amount in fees, but we charge 0 of
416
        // // that X amount (no fee) and refund X to the user.
417
        // if let Some(fee_asset) = fee_asset {
418
        //     // The fee asset must be the same as the reserve asset.
419
        //     if fee_asset.id != reserve_asset.id || fee_asset.fun > reserve_asset.fun {
420
        //         return Err(InvalidFeeAsset);
421
        //     }
422
        // }
423

            
424
        // let (asset_id, amount) = match reserve_asset {
425
        //     Asset {
426
        //         id: AssetId(inner_location),
427
        //         fun: Fungible(amount),
428
        //     } => Some((inner_location.clone(), *amount)),
429
        //     _ => None,
430
        // }
431
        // .ok_or(AssetResolutionFailed)?;
432

            
433
        // // transfer amount must be greater than 0.
434
        // ensure!(amount > 0, ZeroAssetTransfer);
435

            
436
        // let token_id = TokenIdOf::convert_location(&asset_id).ok_or(InvalidAsset)?;
437

            
438
        // let expected_asset_id = ConvertAssetId::convert(&token_id).ok_or(InvalidAsset)?;
439

            
440
        // ensure!(asset_id == expected_asset_id, InvalidAsset);
441

            
442
        // // Check if there is a SetTopic and skip over it if found.
443
        // let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?;
444

            
445
        // Ok((
446
        //     Command::MintForeignToken {
447
        //         token_id,
448
        //         recipient,
449
        //         amount,
450
        //     },
451
        //     *topic_id,
452
        // ))
453
    }
454
}
455

            
456
pub struct SnowbrigeTokenTransferRouter<Bridges, UniversalLocation>(
457
    PhantomData<(Bridges, UniversalLocation)>,
458
);
459

            
460
impl<Bridges, UniversalLocation> SendXcm
461
    for SnowbrigeTokenTransferRouter<Bridges, UniversalLocation>
462
where
463
    Bridges: ExportXcm,
464
    UniversalLocation: Get<InteriorLocation>,
465
{
466
    type Ticket = Bridges::Ticket;
467

            
468
137
    fn validate(
469
137
        dest: &mut Option<Location>,
470
137
        msg: &mut Option<Xcm<()>>,
471
137
    ) -> SendResult<Self::Ticket> {
472
137
        let universal_source = UniversalLocation::get();
473

            
474
        // This `clone` ensures that `dest` is not consumed in any case.
475
137
        let dest = dest.clone().ok_or(MissingArgument)?;
476
137
        let (remote_network, remote_location) =
477
137
            ensure_is_remote(universal_source.clone(), dest).map_err(|_| NotApplicable)?;
478
137
        let xcm = msg.take().ok_or(MissingArgument)?;
479

            
480
        // Channel ID is ignored by the bridge which use a different type
481
137
        let channel = 0;
482

            
483
        // validate export message
484
137
        validate_export::<Bridges>(
485
137
            remote_network,
486
137
            channel,
487
137
            universal_source,
488
137
            remote_location,
489
137
            xcm.clone(),
490
        )
491
137
        .inspect_err(|err| {
492
51
            if let NotApplicable = err {
493
45
                // We need to make sure that msg is not consumed in case of `NotApplicable`.
494
45
                *msg = Some(xcm);
495
48
            }
496
51
        })
497
137
    }
498

            
499
46
    fn deliver(ticket: Self::Ticket) -> Result<XcmHash, SendError> {
500
46
        Bridges::deliver(ticket)
501
46
    }
502
}
503

            
504
impl<Bridge, UniversalLocation> InspectMessageQueues
505
    for SnowbrigeTokenTransferRouter<Bridge, UniversalLocation>
506
{
507
    fn clear_messages() {}
508
    fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
509
        Vec::new()
510
    }
511
}
512

            
513
pub struct SnowbridgeChannelToAgentId<T>(PhantomData<T>);
514
impl<T: snowbridge_pallet_system::Config> TryConvert<ChannelId, AgentId>
515
    for SnowbridgeChannelToAgentId<T>
516
{
517
    fn try_convert(channel_id: ChannelId) -> Result<AgentId, ChannelId> {
518
        let Some(channel) = snowbridge_pallet_system::Channels::<T>::get(channel_id) else {
519
            return Err(channel_id);
520
        };
521

            
522
        Ok(channel.agent_id)
523
    }
524
}