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

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

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

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

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

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

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

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

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

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

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

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

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

            
154
40
        Ok(((ticket.encode(), message_id), fee))
155
46
    }
156

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

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

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

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

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

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

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

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

            
236
40
        Ok(result)
237
44
    }
238

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

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

            
249
        // Check if clear origin exists and skip over it.
250
41
        if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() {
251
39
            let _ = self.next();
252
39
        }
253

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

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

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

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

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

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

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

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

            
326
        // transfer amount must be greater than 0.
327
41
        ensure!(amount > 0, ZeroAssetTransfer);
328

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

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

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

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

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

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

            
381
        // use XcmConverterError::*;
382

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

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

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

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

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

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

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

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

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

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

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

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

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

            
468
        // ensure!(asset_id == expected_asset_id, InvalidAsset);
469

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

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

            
484
pub struct SnowbrigeTokenTransferRouter<Bridges, UniversalLocation>(
485
    PhantomData<(Bridges, UniversalLocation)>,
486
);
487

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

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

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

            
508
        // Channel ID is ignored by the bridge which use a different type
509
46
        let channel = 0;
510

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

            
527
22
    fn deliver(ticket: Self::Ticket) -> Result<XcmHash, SendError> {
528
22
        Bridges::deliver(ticket)
529
22
    }
530
}
531

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

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

            
550
        Ok(channel.agent_id)
551
    }
552
}