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

            
38
pub struct EthereumBlobExporter<
39
    UniversalLocation,
40
    EthereumNetwork,
41
    OutboundQueue,
42
    ConvertAssetId,
43
    BridgeChannelInfo,
44
>(
45
    PhantomData<(
46
        UniversalLocation,
47
        EthereumNetwork,
48
        OutboundQueue,
49
        ConvertAssetId,
50
        BridgeChannelInfo,
51
    )>,
52
);
53

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

            
71
4
    fn validate(
72
4
        network: NetworkId,
73
4
        _channel: u32,
74
4
        universal_source: &mut Option<InteriorLocation>,
75
4
        destination: &mut Option<InteriorLocation>,
76
4
        message: &mut Option<Xcm<()>>,
77
4
    ) -> SendResult<Self::Ticket> {
78
4
        let expected_network = EthereumNetwork::get();
79
4
        let universal_location = UniversalLocation::get();
80
4

            
81
4
        if network != expected_network {
82
            log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched bridge network {network:?}.");
83
            return Err(SendError::NotApplicable);
84
4
        }
85

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

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

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

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

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

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

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

            
137
1
        let outbound_message = Message {
138
1
            id: Some(message_id.into()),
139
1
            channel_id,
140
1
            command,
141
1
        };
142

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

            
149
        // convert fee to Asset
150
1
        let fee = Asset::from((Location::here(), fee.total())).into();
151
1

            
152
1
        Ok(((ticket.encode(), message_id), fee))
153
4
    }
154

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

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

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

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

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

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

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

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

            
234
1
        Ok(result)
235
3
    }
236

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

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

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

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

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

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

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

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

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

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

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

            
322
        // transfer amount must be greater than 0.
323
2
        ensure!(amount > 0, ZeroAssetTransfer);
324

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

            
328
2
        Ok((
329
2
            Command::UnlockNativeToken {
330
2
                agent_id: self.agent_id,
331
2
                token,
332
2
                recipient,
333
2
                amount,
334
2
            },
335
2
            *topic_id,
336
2
        ))
337
2
    }
338

            
339
11
    fn next(&mut self) -> Result<&'a Instruction<Call>, XcmConverterError> {
340
11
        self.iter
341
11
            .next()
342
11
            .ok_or(XcmConverterError::UnexpectedEndOfXcm)
343
11
    }
344

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

            
351
4
    fn network_matches(&self, network: &Option<NetworkId>) -> bool {
352
4
        if let Some(network) = network {
353
4
            *network == self.ethereum_network
354
        } else {
355
            true
356
        }
357
4
    }
358

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

            
374
        // use XcmConverterError::*;
375

            
376
        // // Get the reserve assets.
377
        // let reserve_assets = match_expression!(
378
        //     self.next()?,
379
        //     ReserveAssetDeposited(reserve_assets),
380
        //     reserve_assets
381
        // )
382
        // .ok_or(ReserveAssetDepositedExpected)?;
383

            
384
        // // Check if clear origin exists and skip over it.
385
        // if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() {
386
        //     let _ = self.next();
387
        // }
388

            
389
        // // Get the fee asset item from BuyExecution or continue parsing.
390
        // let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees);
391
        // if fee_asset.is_some() {
392
        //     let _ = self.next();
393
        // }
394

            
395
        // let (deposit_assets, beneficiary) = match_expression!(
396
        //     self.next()?,
397
        //     DepositAsset {
398
        //         assets,
399
        //         beneficiary
400
        //     },
401
        //     (assets, beneficiary)
402
        // )
403
        // .ok_or(DepositAssetExpected)?;
404

            
405
        // // assert that the beneficiary is AccountKey20.
406
        // let recipient = match_expression!(
407
        //     beneficiary.unpack(),
408
        //     (0, [AccountKey20 { network, key }])
409
        //         if self.network_matches(network),
410
        //     H160(*key)
411
        // )
412
        // .ok_or(BeneficiaryResolutionFailed)?;
413

            
414
        // // Make sure there are reserved assets.
415
        // if reserve_assets.len() == 0 {
416
        //     return Err(NoReserveAssets);
417
        // }
418

            
419
        // // Check the the deposit asset filter matches what was reserved.
420
        // if reserve_assets
421
        //     .inner()
422
        //     .iter()
423
        //     .any(|asset| !deposit_assets.matches(asset))
424
        // {
425
        //     return Err(FilterDoesNotConsumeAllAssets);
426
        // }
427

            
428
        // // We only support a single asset at a time.
429
        // ensure!(reserve_assets.len() == 1, TooManyAssets);
430
        // let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?;
431

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

            
445
        // let (asset_id, amount) = match reserve_asset {
446
        //     Asset {
447
        //         id: AssetId(inner_location),
448
        //         fun: Fungible(amount),
449
        //     } => Some((inner_location.clone(), *amount)),
450
        //     _ => None,
451
        // }
452
        // .ok_or(AssetResolutionFailed)?;
453

            
454
        // // transfer amount must be greater than 0.
455
        // ensure!(amount > 0, ZeroAssetTransfer);
456

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

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

            
461
        // ensure!(asset_id == expected_asset_id, InvalidAsset);
462

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

            
466
        // Ok((
467
        //     Command::MintForeignToken {
468
        //         token_id,
469
        //         recipient,
470
        //         amount,
471
        //     },
472
        //     *topic_id,
473
        // ))
474
    }
475
}
476

            
477
pub struct SnowbrigeTokenTransferRouter<Bridges, UniversalLocation>(
478
    PhantomData<(Bridges, UniversalLocation)>,
479
);
480

            
481
impl<Bridges, UniversalLocation> SendXcm
482
    for SnowbrigeTokenTransferRouter<Bridges, UniversalLocation>
483
where
484
    Bridges: ExportXcm,
485
    UniversalLocation: Get<InteriorLocation>,
486
{
487
    type Ticket = Bridges::Ticket;
488

            
489
4
    fn validate(
490
4
        dest: &mut Option<Location>,
491
4
        msg: &mut Option<Xcm<()>>,
492
4
    ) -> SendResult<Self::Ticket> {
493
4
        let universal_source = UniversalLocation::get();
494

            
495
        // This `clone` ensures that `dest` is not consumed in any case.
496
4
        let dest = dest.clone().ok_or(MissingArgument)?;
497
4
        let (remote_network, remote_location) =
498
4
            ensure_is_remote(universal_source.clone(), dest).map_err(|_| NotApplicable)?;
499
4
        let xcm = msg.take().ok_or(MissingArgument)?;
500

            
501
        // Channel ID is ignored by the bridge which use a different type
502
4
        let channel = 0;
503
4

            
504
4
        // validate export message
505
4
        validate_export::<Bridges>(
506
4
            remote_network,
507
4
            channel,
508
4
            universal_source,
509
4
            remote_location,
510
4
            xcm.clone(),
511
4
        )
512
4
        .inspect_err(|err| {
513
3
            if let NotApplicable = err {
514
                // We need to make sure that msg is not consumed in case of `NotApplicable`.
515
                *msg = Some(xcm);
516
3
            }
517
4
        })
518
4
    }
519

            
520
1
    fn deliver(ticket: Self::Ticket) -> Result<XcmHash, SendError> {
521
1
        Bridges::deliver(ticket)
522
1
    }
523
}
524

            
525
impl<Bridge, UniversalLocation> InspectMessageQueues
526
    for SnowbrigeTokenTransferRouter<Bridge, UniversalLocation>
527
{
528
    fn clear_messages() {}
529
    fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
530
        Vec::new()
531
    }
532
}
533

            
534
pub struct SnowbridgeChannelToAgentId<T>(PhantomData<T>);
535
impl<T: snowbridge_pallet_system::Config> TryConvert<ChannelId, AgentId>
536
    for SnowbridgeChannelToAgentId<T>
537
{
538
    fn try_convert(channel_id: ChannelId) -> Result<AgentId, ChannelId> {
539
        let Some(channel) = snowbridge_pallet_system::Channels::<T>::get(channel_id) else {
540
            return Err(channel_id);
541
        };
542

            
543
        Ok(channel.agent_id)
544
    }
545
}