1#[cfg(test)]
6mod tests;
7
8use core::slice::Iter;
9
10use codec::{Decode, Encode};
11
12use super::message::{Command, Message, SendMessage};
13use frame_support::{ensure, traits::Get};
14use snowbridge_core::{AgentId, ChannelId, ParaId, TokenId, TokenIdOf};
15use sp_core::{H160, H256};
16use sp_runtime::traits::MaybeConvert;
17use sp_std::{iter::Peekable, marker::PhantomData, prelude::*};
18use xcm::prelude::*;
19use xcm_executor::traits::{ConvertLocation, ExportXcm};
20
21pub struct EthereumBlobExporter<
22 UniversalLocation,
23 EthereumNetwork,
24 OutboundQueue,
25 AgentHashedDescription,
26 ConvertAssetId,
27>(
28 PhantomData<(
29 UniversalLocation,
30 EthereumNetwork,
31 OutboundQueue,
32 AgentHashedDescription,
33 ConvertAssetId,
34 )>,
35);
36
37impl<UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription, ConvertAssetId>
38 ExportXcm
39 for EthereumBlobExporter<
40 UniversalLocation,
41 EthereumNetwork,
42 OutboundQueue,
43 AgentHashedDescription,
44 ConvertAssetId,
45 >
46where
47 UniversalLocation: Get<InteriorLocation>,
48 EthereumNetwork: Get<NetworkId>,
49 OutboundQueue: SendMessage<Balance = u128>,
50 AgentHashedDescription: ConvertLocation<H256>,
51 ConvertAssetId: MaybeConvert<TokenId, Location>,
52{
53 type Ticket = (Vec<u8>, XcmHash);
54
55 fn validate(
56 network: NetworkId,
57 _channel: u32,
58 universal_source: &mut Option<InteriorLocation>,
59 destination: &mut Option<InteriorLocation>,
60 message: &mut Option<Xcm<()>>,
61 ) -> SendResult<Self::Ticket> {
62 let expected_network = EthereumNetwork::get();
63 let universal_location = UniversalLocation::get();
64
65 if network != expected_network {
66 log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched bridge network {network:?}.");
67 return Err(SendError::NotApplicable)
68 }
69
70 let dest = destination.clone().ok_or(SendError::MissingArgument)?;
72 if dest != Here {
73 log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched remote destination {dest:?}.");
74 return Err(SendError::NotApplicable)
75 }
76
77 let (local_net, local_sub) = universal_source.clone()
79 .ok_or_else(|| {
80 log::error!(target: "xcm::ethereum_blob_exporter", "universal source not provided.");
81 SendError::MissingArgument
82 })?
83 .split_global()
84 .map_err(|()| {
85 log::error!(target: "xcm::ethereum_blob_exporter", "could not get global consensus from universal source '{universal_source:?}'.");
86 SendError::NotApplicable
87 })?;
88
89 if Ok(local_net) != universal_location.global_consensus() {
90 log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched relay network {local_net:?}.");
91 return Err(SendError::NotApplicable)
92 }
93
94 let para_id = match local_sub.as_slice() {
95 [Parachain(para_id)] => *para_id,
96 _ => {
97 log::error!(target: "xcm::ethereum_blob_exporter", "could not get parachain id from universal source '{local_sub:?}'.");
98 return Err(SendError::NotApplicable)
99 },
100 };
101
102 let source_location = Location::new(1, local_sub.clone());
103
104 let agent_id = match AgentHashedDescription::convert_location(&source_location) {
105 Some(id) => id,
106 None => {
107 log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to not being able to create agent id. '{source_location:?}'");
108 return Err(SendError::NotApplicable)
109 },
110 };
111
112 let message = message.take().ok_or_else(|| {
113 log::error!(target: "xcm::ethereum_blob_exporter", "xcm message not provided.");
114 SendError::MissingArgument
115 })?;
116
117 let mut converter =
118 XcmConverter::<ConvertAssetId, ()>::new(&message, expected_network, agent_id);
119 let (command, message_id) = converter.convert().map_err(|err|{
120 log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to pattern matching error '{err:?}'.");
121 SendError::Unroutable
122 })?;
123
124 let channel_id: ChannelId = ParaId::from(para_id).into();
125
126 let outbound_message = Message { id: Some(message_id.into()), channel_id, command };
127
128 let (ticket, fee) = OutboundQueue::validate(&outbound_message).map_err(|err| {
130 log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue validation of message failed. {err:?}");
131 SendError::Unroutable
132 })?;
133
134 let fee = Asset::from((Location::parent(), fee.total())).into();
136
137 Ok(((ticket.encode(), message_id), fee))
138 }
139
140 fn deliver(blob: (Vec<u8>, XcmHash)) -> Result<XcmHash, SendError> {
141 let ticket: OutboundQueue::Ticket = OutboundQueue::Ticket::decode(&mut blob.0.as_ref())
142 .map_err(|_| {
143 log::trace!(target: "xcm::ethereum_blob_exporter", "undeliverable due to decoding error");
144 SendError::NotApplicable
145 })?;
146
147 let message_id = OutboundQueue::deliver(ticket).map_err(|_| {
148 log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue submit of message failed");
149 SendError::Transport("other transport error")
150 })?;
151
152 log::info!(target: "xcm::ethereum_blob_exporter", "message delivered {message_id:#?}.");
153 Ok(message_id.into())
154 }
155}
156
157#[derive(PartialEq, Debug)]
159enum XcmConverterError {
160 UnexpectedEndOfXcm,
161 EndOfXcmMessageExpected,
162 WithdrawAssetExpected,
163 DepositAssetExpected,
164 NoReserveAssets,
165 FilterDoesNotConsumeAllAssets,
166 TooManyAssets,
167 ZeroAssetTransfer,
168 BeneficiaryResolutionFailed,
169 AssetResolutionFailed,
170 InvalidFeeAsset,
171 SetTopicExpected,
172 ReserveAssetDepositedExpected,
173 InvalidAsset,
174 UnexpectedInstruction,
175}
176
177macro_rules! match_expression {
181 ($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )?, $value:expr $(,)?) => {
182 match $expression {
183 $( $pattern )|+ $( if $guard )? => Some($value),
184 _ => None,
185 }
186 };
187}
188
189struct XcmConverter<'a, ConvertAssetId, Call> {
190 iter: Peekable<Iter<'a, Instruction<Call>>>,
191 ethereum_network: NetworkId,
192 agent_id: AgentId,
193 _marker: PhantomData<ConvertAssetId>,
194}
195impl<'a, ConvertAssetId, Call> XcmConverter<'a, ConvertAssetId, Call>
196where
197 ConvertAssetId: MaybeConvert<TokenId, Location>,
198{
199 fn new(message: &'a Xcm<Call>, ethereum_network: NetworkId, agent_id: AgentId) -> Self {
200 Self {
201 iter: message.inner().iter().peekable(),
202 ethereum_network,
203 agent_id,
204 _marker: Default::default(),
205 }
206 }
207
208 fn convert(&mut self) -> Result<(Command, [u8; 32]), XcmConverterError> {
209 let result = match self.peek() {
210 Ok(ReserveAssetDeposited { .. }) => self.make_mint_foreign_token_command(),
211 Ok(WithdrawAsset { .. }) => self.make_unlock_native_token_command(),
213 Err(e) => Err(e),
214 _ => return Err(XcmConverterError::UnexpectedInstruction),
215 }?;
216
217 if self.next().is_ok() {
219 return Err(XcmConverterError::EndOfXcmMessageExpected)
220 }
221
222 Ok(result)
223 }
224
225 fn make_unlock_native_token_command(
226 &mut self,
227 ) -> Result<(Command, [u8; 32]), XcmConverterError> {
228 use XcmConverterError::*;
229
230 let reserve_assets =
232 match_expression!(self.next()?, WithdrawAsset(reserve_assets), reserve_assets)
233 .ok_or(WithdrawAssetExpected)?;
234
235 if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() {
237 let _ = self.next();
238 }
239
240 let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees);
242 if fee_asset.is_some() {
243 let _ = self.next();
244 }
245
246 let (deposit_assets, beneficiary) = match_expression!(
247 self.next()?,
248 DepositAsset { assets, beneficiary },
249 (assets, beneficiary)
250 )
251 .ok_or(DepositAssetExpected)?;
252
253 let recipient = match_expression!(
255 beneficiary.unpack(),
256 (0, [AccountKey20 { network, key }])
257 if self.network_matches(network),
258 H160(*key)
259 )
260 .ok_or(BeneficiaryResolutionFailed)?;
261
262 if reserve_assets.len() == 0 {
264 return Err(NoReserveAssets)
265 }
266
267 if reserve_assets.inner().iter().any(|asset| !deposit_assets.matches(asset)) {
269 return Err(FilterDoesNotConsumeAllAssets)
270 }
271
272 ensure!(reserve_assets.len() == 1, TooManyAssets);
274 let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?;
275
276 if let Some(fee_asset) = fee_asset {
283 if fee_asset.id != reserve_asset.id || fee_asset.fun > reserve_asset.fun {
285 return Err(InvalidFeeAsset)
286 }
287 }
288
289 let (token, amount) = match reserve_asset {
290 Asset { id: AssetId(inner_location), fun: Fungible(amount) } =>
291 match inner_location.unpack() {
292 (0, [AccountKey20 { network, key }]) if self.network_matches(network) =>
294 Some((H160(*key), *amount)),
295 (0, []) => Some((H160([0; 20]), *amount)),
299 _ => None,
300 },
301 _ => None,
302 }
303 .ok_or(AssetResolutionFailed)?;
304
305 ensure!(amount > 0, ZeroAssetTransfer);
307
308 let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?;
310
311 Ok((
312 Command::UnlockNativeToken { agent_id: self.agent_id, token, recipient, amount },
313 *topic_id,
314 ))
315 }
316
317 fn next(&mut self) -> Result<&'a Instruction<Call>, XcmConverterError> {
318 self.iter.next().ok_or(XcmConverterError::UnexpectedEndOfXcm)
319 }
320
321 fn peek(&mut self) -> Result<&&'a Instruction<Call>, XcmConverterError> {
322 self.iter.peek().ok_or(XcmConverterError::UnexpectedEndOfXcm)
323 }
324
325 fn network_matches(&self, network: &Option<NetworkId>) -> bool {
326 if let Some(network) = network {
327 *network == self.ethereum_network
328 } else {
329 true
330 }
331 }
332
333 fn make_mint_foreign_token_command(
341 &mut self,
342 ) -> Result<(Command, [u8; 32]), XcmConverterError> {
343 use XcmConverterError::*;
344
345 let reserve_assets =
347 match_expression!(self.next()?, ReserveAssetDeposited(reserve_assets), reserve_assets)
348 .ok_or(ReserveAssetDepositedExpected)?;
349
350 if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() {
352 let _ = self.next();
353 }
354
355 let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees);
357 if fee_asset.is_some() {
358 let _ = self.next();
359 }
360
361 let (deposit_assets, beneficiary) = match_expression!(
362 self.next()?,
363 DepositAsset { assets, beneficiary },
364 (assets, beneficiary)
365 )
366 .ok_or(DepositAssetExpected)?;
367
368 let recipient = match_expression!(
370 beneficiary.unpack(),
371 (0, [AccountKey20 { network, key }])
372 if self.network_matches(network),
373 H160(*key)
374 )
375 .ok_or(BeneficiaryResolutionFailed)?;
376
377 if reserve_assets.len() == 0 {
379 return Err(NoReserveAssets)
380 }
381
382 if reserve_assets.inner().iter().any(|asset| !deposit_assets.matches(asset)) {
384 return Err(FilterDoesNotConsumeAllAssets)
385 }
386
387 ensure!(reserve_assets.len() == 1, TooManyAssets);
389 let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?;
390
391 if let Some(fee_asset) = fee_asset {
398 if fee_asset.id != reserve_asset.id || fee_asset.fun > reserve_asset.fun {
400 return Err(InvalidFeeAsset)
401 }
402 }
403
404 let (asset_id, amount) = match reserve_asset {
405 Asset { id: AssetId(inner_location), fun: Fungible(amount) } =>
406 Some((inner_location.clone(), *amount)),
407 _ => None,
408 }
409 .ok_or(AssetResolutionFailed)?;
410
411 ensure!(amount > 0, ZeroAssetTransfer);
413
414 let token_id = TokenIdOf::convert_location(&asset_id).ok_or(InvalidAsset)?;
415
416 ConvertAssetId::maybe_convert(token_id).ok_or(InvalidAsset)?;
417
418 let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?;
420
421 Ok((Command::MintForeignToken { token_id, recipient, amount }, *topic_id))
422 }
423}