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
                _ => None,
321
            },
322
            _ => None,
323
        }
324
3
        .ok_or(AssetResolutionFailed)?;
325

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

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

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

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

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

            
358
6
    fn network_matches(&self, network: &Option<NetworkId>) -> bool {
359
6
        if let Some(network) = network {
360
6
            *network == self.ethereum_network
361
        } else {
362
            true
363
        }
364
6
    }
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
5
    fn validate(
497
5
        dest: &mut Option<Location>,
498
5
        msg: &mut Option<Xcm<()>>,
499
5
    ) -> SendResult<Self::Ticket> {
500
5
        let universal_source = UniversalLocation::get();
501

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

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

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

            
527
2
    fn deliver(ticket: Self::Ticket) -> Result<XcmHash, SendError> {
528
2
        Bridges::deliver(ticket)
529
2
    }
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
}