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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
156
2
        Ok(((ticket.encode(), message_id), fee))
157
5
    }
158

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

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

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

            
176
/// Errors that can be thrown to the pattern matching step.
177
#[derive(PartialEq, Debug)]
178
pub enum XcmConverterError {
179
    UnexpectedEndOfXcm,
180
    EndOfXcmMessageExpected,
181
    WithdrawAssetExpected,
182
    DepositAssetExpected,
183
    NoReserveAssets,
184
    FilterDoesNotConsumeAllAssets,
185
    TooManyAssets,
186
    ZeroAssetTransfer,
187
    BeneficiaryResolutionFailed,
188
    AssetResolutionFailed,
189
    InvalidFeeAsset,
190
    SetTopicExpected,
191
    ReserveAssetDepositedExpected,
192
    InvalidAsset,
193
    UnexpectedInstruction,
194
}
195

            
196
macro_rules! match_expression {
197
	($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )?, $value:expr $(,)?) => {
198
		match $expression {
199
			$( $pattern )|+ $( if $guard )? => Some($value),
200
			_ => None,
201
		}
202
	};
203
}
204

            
205
pub struct XcmConverter<'a, ConvertAssetId, Call> {
206
    iter: Peekable<Iter<'a, Instruction<Call>>>,
207
    ethereum_network: NetworkId,
208
    agent_id: AgentId,
209
    _marker: PhantomData<ConvertAssetId>,
210
}
211
impl<'a, ConvertAssetId, Call> XcmConverter<'a, ConvertAssetId, Call>
212
where
213
    ConvertAssetId: MaybeEquivalence<TokenId, Location>,
214
{
215
5
    pub fn new(message: &'a Xcm<Call>, ethereum_network: NetworkId, agent_id: AgentId) -> Self {
216
5
        Self {
217
5
            iter: message.inner().iter().peekable(),
218
5
            ethereum_network,
219
5
            agent_id,
220
5
            _marker: Default::default(),
221
5
        }
222
5
    }
223

            
224
4
    pub fn convert(&mut self) -> Result<(Command, [u8; 32]), XcmConverterError> {
225
4
        let result = match self.peek() {
226
            Ok(ReserveAssetDeposited { .. }) => self.make_mint_foreign_token_command(),
227
            // Get withdraw/deposit and make native tokens create message.
228
2
            Ok(WithdrawAsset { .. }) => self.make_unlock_native_token_command(),
229
            Err(e) => Err(e),
230
2
            _ => return Err(XcmConverterError::UnexpectedInstruction),
231
        }?;
232

            
233
        // All xcm instructions must be consumed before exit.
234
2
        if self.next().is_ok() {
235
            return Err(XcmConverterError::EndOfXcmMessageExpected);
236
2
        }
237
2

            
238
2
        Ok(result)
239
4
    }
240

            
241
3
    pub fn make_unlock_native_token_command(
242
3
        &mut self,
243
3
    ) -> Result<(Command, [u8; 32]), XcmConverterError> {
244
        use XcmConverterError::*;
245

            
246
        // Get the reserve assets from WithdrawAsset.
247
3
        let reserve_assets =
248
3
            match_expression!(self.next()?, WithdrawAsset(reserve_assets), reserve_assets)
249
3
                .ok_or(WithdrawAssetExpected)?;
250

            
251
        // Check if clear origin exists and skip over it.
252
3
        if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() {
253
2
            let _ = self.next();
254
2
        }
255

            
256
        // Get the fee asset item from BuyExecution or continue parsing.
257
3
        let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees);
258
3
        if fee_asset.is_some() {
259
2
            let _ = self.next();
260
2
        }
261

            
262
3
        let (deposit_assets, beneficiary) = match_expression!(
263
3
            self.next()?,
264
            DepositAsset {
265
3
                assets,
266
3
                beneficiary
267
3
            },
268
3
            (assets, beneficiary)
269
        )
270
3
        .ok_or(DepositAssetExpected)?;
271

            
272
        // assert that the beneficiary is AccountKey20.
273
3
        let recipient = match_expression!(
274
3
            beneficiary.unpack(),
275
3
            (0, [AccountKey20 { network, key }])
276
3
                if self.network_matches(network),
277
3
            H160(*key)
278
        )
279
3
        .ok_or(BeneficiaryResolutionFailed)?;
280

            
281
        // Make sure there are reserved assets.
282
3
        if reserve_assets.len() == 0 {
283
            return Err(NoReserveAssets);
284
3
        }
285
3

            
286
3
        // Check the the deposit asset filter matches what was reserved.
287
3
        if reserve_assets
288
3
            .inner()
289
3
            .iter()
290
3
            .any(|asset| !deposit_assets.matches(asset))
291
        {
292
            return Err(FilterDoesNotConsumeAllAssets);
293
3
        }
294
3

            
295
3
        // We only support a single asset at a time.
296
3
        ensure!(reserve_assets.len() == 1, TooManyAssets);
297
3
        let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?;
298

            
299
        // Fees are collected on Tanssi, up front and directly from the user, to cover the
300
        // complete cost of the transfer. Any additional fees provided in the XCM program are
301
        // refunded to the beneficiary. We only validate the fee here if its provided to make sure
302
        // the XCM program is well formed. Another way to think about this from an XCM perspective
303
        // would be that the user offered to pay X amount in fees, but we charge 0 of that X amount
304
        // (no fee) and refund X to the user.
305
3
        if let Some(fee_asset) = fee_asset {
306
            // The fee asset must be the same as the reserve asset.
307
2
            if fee_asset.id != reserve_asset.id || fee_asset.fun > reserve_asset.fun {
308
                return Err(InvalidFeeAsset);
309
2
            }
310
1
        }
311

            
312
3
        let (token, amount) = match reserve_asset {
313
            Asset {
314
3
                id: AssetId(inner_location),
315
3
                fun: Fungible(amount),
316
3
            } => match inner_location.unpack() {
317
3
                (0, [AccountKey20 { network, key }]) if self.network_matches(network) => {
318
3
                    Some((H160(*key), *amount))
319
                }
320
                // Native ETH token
321
                (0, []) => Some((H160::zero(), *amount)),
322
                _ => None,
323
            },
324
            _ => None,
325
        }
326
3
        .ok_or(AssetResolutionFailed)?;
327

            
328
        // transfer amount must be greater than 0.
329
3
        ensure!(amount > 0, ZeroAssetTransfer);
330

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

            
334
3
        Ok((
335
3
            // TODO: This should be changed to UnlockNativeToken once we migrate to Snowbridge V2.
336
3
            Command::AgentExecute {
337
3
                agent_id: self.agent_id,
338
3
                command: AgentExecuteCommand::TransferToken {
339
3
                    token,
340
3
                    recipient,
341
3
                    amount,
342
3
                },
343
3
            },
344
3
            *topic_id,
345
3
        ))
346
3
    }
347

            
348
15
    fn next(&mut self) -> Result<&'a Instruction<Call>, XcmConverterError> {
349
15
        self.iter
350
15
            .next()
351
15
            .ok_or(XcmConverterError::UnexpectedEndOfXcm)
352
15
    }
353

            
354
10
    fn peek(&mut self) -> Result<&&'a Instruction<Call>, XcmConverterError> {
355
10
        self.iter
356
10
            .peek()
357
10
            .ok_or(XcmConverterError::UnexpectedEndOfXcm)
358
10
    }
359

            
360
6
    fn network_matches(&self, network: &Option<NetworkId>) -> bool {
361
6
        if let Some(network) = network {
362
6
            *network == self.ethereum_network
363
        } else {
364
            true
365
        }
366
6
    }
367

            
368
    /// Convert the xcm for Polkadot-native token from the origin chain (container chain) into the Command
369
    /// To match transfers of Polkadot-native tokens, we expect an input of the form:
370
    /// # ReserveAssetDeposited
371
    /// # ClearOrigin
372
    /// # BuyExecution
373
    /// # DepositAsset
374
    /// # SetTopic
375
    fn make_mint_foreign_token_command(
376
        &mut self,
377
    ) -> Result<(Command, [u8; 32]), XcmConverterError> {
378
        // TODO: This function will be used only when we start receiving tokens from containers.
379
        // The whole struct is copied from Snowbridge and modified for our needs, and thus function
380
        // will be modified in a latter PR.
381
        todo!("make_mint_foreign_token_command");
382

            
383
        // use XcmConverterError::*;
384

            
385
        // // Get the reserve assets.
386
        // let reserve_assets = match_expression!(
387
        //     self.next()?,
388
        //     ReserveAssetDeposited(reserve_assets),
389
        //     reserve_assets
390
        // )
391
        // .ok_or(ReserveAssetDepositedExpected)?;
392

            
393
        // // Check if clear origin exists and skip over it.
394
        // if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() {
395
        //     let _ = self.next();
396
        // }
397

            
398
        // // Get the fee asset item from BuyExecution or continue parsing.
399
        // let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees);
400
        // if fee_asset.is_some() {
401
        //     let _ = self.next();
402
        // }
403

            
404
        // let (deposit_assets, beneficiary) = match_expression!(
405
        //     self.next()?,
406
        //     DepositAsset {
407
        //         assets,
408
        //         beneficiary
409
        //     },
410
        //     (assets, beneficiary)
411
        // )
412
        // .ok_or(DepositAssetExpected)?;
413

            
414
        // // assert that the beneficiary is AccountKey20.
415
        // let recipient = match_expression!(
416
        //     beneficiary.unpack(),
417
        //     (0, [AccountKey20 { network, key }])
418
        //         if self.network_matches(network),
419
        //     H160(*key)
420
        // )
421
        // .ok_or(BeneficiaryResolutionFailed)?;
422

            
423
        // // Make sure there are reserved assets.
424
        // if reserve_assets.len() == 0 {
425
        //     return Err(NoReserveAssets);
426
        // }
427

            
428
        // // Check the the deposit asset filter matches what was reserved.
429
        // if reserve_assets
430
        //     .inner()
431
        //     .iter()
432
        //     .any(|asset| !deposit_assets.matches(asset))
433
        // {
434
        //     return Err(FilterDoesNotConsumeAllAssets);
435
        // }
436

            
437
        // // We only support a single asset at a time.
438
        // ensure!(reserve_assets.len() == 1, TooManyAssets);
439
        // let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?;
440

            
441
        // // Fees are collected on the origin chain (container chain), up front and directly from the
442
        // // user, to cover the complete cost of the transfer. Any additional fees provided in the XCM
443
        // // program are refunded to the beneficiary. We only validate the fee here if its provided to
444
        // // make sure the XCM program is well formed. Another way to think about this from an XCM
445
        // // perspective would be that the user offered to pay X amount in fees, but we charge 0 of
446
        // // that X amount (no fee) and refund X to the user.
447
        // if let Some(fee_asset) = fee_asset {
448
        //     // The fee asset must be the same as the reserve asset.
449
        //     if fee_asset.id != reserve_asset.id || fee_asset.fun > reserve_asset.fun {
450
        //         return Err(InvalidFeeAsset);
451
        //     }
452
        // }
453

            
454
        // let (asset_id, amount) = match reserve_asset {
455
        //     Asset {
456
        //         id: AssetId(inner_location),
457
        //         fun: Fungible(amount),
458
        //     } => Some((inner_location.clone(), *amount)),
459
        //     _ => None,
460
        // }
461
        // .ok_or(AssetResolutionFailed)?;
462

            
463
        // // transfer amount must be greater than 0.
464
        // ensure!(amount > 0, ZeroAssetTransfer);
465

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

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

            
470
        // ensure!(asset_id == expected_asset_id, InvalidAsset);
471

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

            
475
        // Ok((
476
        //     Command::MintForeignToken {
477
        //         token_id,
478
        //         recipient,
479
        //         amount,
480
        //     },
481
        //     *topic_id,
482
        // ))
483
    }
484
}
485

            
486
pub struct SnowbrigeTokenTransferRouter<Bridges, UniversalLocation>(
487
    PhantomData<(Bridges, UniversalLocation)>,
488
);
489

            
490
impl<Bridges, UniversalLocation> SendXcm
491
    for SnowbrigeTokenTransferRouter<Bridges, UniversalLocation>
492
where
493
    Bridges: ExportXcm,
494
    UniversalLocation: Get<InteriorLocation>,
495
{
496
    type Ticket = Bridges::Ticket;
497

            
498
5
    fn validate(
499
5
        dest: &mut Option<Location>,
500
5
        msg: &mut Option<Xcm<()>>,
501
5
    ) -> SendResult<Self::Ticket> {
502
5
        let universal_source = UniversalLocation::get();
503

            
504
        // This `clone` ensures that `dest` is not consumed in any case.
505
5
        let dest = dest.clone().ok_or(MissingArgument)?;
506
5
        let (remote_network, remote_location) =
507
5
            ensure_is_remote(universal_source.clone(), dest).map_err(|_| NotApplicable)?;
508
5
        let xcm = msg.take().ok_or(MissingArgument)?;
509

            
510
        // Channel ID is ignored by the bridge which use a different type
511
5
        let channel = 0;
512
5

            
513
5
        // validate export message
514
5
        validate_export::<Bridges>(
515
5
            remote_network,
516
5
            channel,
517
5
            universal_source,
518
5
            remote_location,
519
5
            xcm.clone(),
520
5
        )
521
5
        .inspect_err(|err| {
522
3
            if let NotApplicable = err {
523
                // We need to make sure that msg is not consumed in case of `NotApplicable`.
524
                *msg = Some(xcm);
525
3
            }
526
5
        })
527
5
    }
528

            
529
2
    fn deliver(ticket: Self::Ticket) -> Result<XcmHash, SendError> {
530
2
        Bridges::deliver(ticket)
531
2
    }
532
}
533

            
534
impl<Bridge, UniversalLocation> InspectMessageQueues
535
    for SnowbrigeTokenTransferRouter<Bridge, UniversalLocation>
536
{
537
    fn clear_messages() {}
538
    fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
539
        Vec::new()
540
    }
541
}
542

            
543
pub struct SnowbridgeChannelToAgentId<T>(PhantomData<T>);
544
impl<T: snowbridge_pallet_system::Config> TryConvert<ChannelId, AgentId>
545
    for SnowbridgeChannelToAgentId<T>
546
{
547
    fn try_convert(channel_id: ChannelId) -> Result<AgentId, ChannelId> {
548
        let Some(channel) = snowbridge_pallet_system::Channels::<T>::get(channel_id) else {
549
            return Err(channel_id);
550
        };
551

            
552
        Ok(channel.agent_id)
553
    }
554
}