1#[cfg(test)]
6mod mock;
7#[cfg(test)]
8mod tests;
9
10use codec::{Decode, Encode};
11use core::marker::PhantomData;
12use frame_support::{traits::tokens::Balance as BalanceT, PalletError};
13use scale_info::TypeInfo;
14use snowbridge_core::TokenId;
15use sp_core::{Get, RuntimeDebug, H160, H256};
16use sp_io::hashing::blake2_256;
17use sp_runtime::{traits::MaybeEquivalence, MultiAddress};
18use sp_std::prelude::*;
19use xcm::prelude::{Junction::AccountKey20, *};
20use xcm_executor::traits::ConvertLocation;
21
22const MINIMUM_DEPOSIT: u128 = 1;
23
24#[derive(Clone, Encode, Decode, RuntimeDebug)]
28pub enum VersionedMessage {
29 V1(MessageV1),
30}
31
32#[derive(Clone, Encode, Decode, RuntimeDebug)]
35pub struct MessageV1 {
36 pub chain_id: u64,
38 pub command: Command,
40}
41
42#[derive(Clone, Encode, Decode, RuntimeDebug)]
43pub enum Command {
44 RegisterToken {
46 token: H160,
48 fee: u128,
50 },
51 SendToken {
53 token: H160,
55 destination: Destination,
57 amount: u128,
59 fee: u128,
61 },
62 SendNativeToken {
64 token_id: TokenId,
66 destination: Destination,
68 amount: u128,
70 fee: u128,
72 },
73}
74
75#[derive(Clone, Encode, Decode, RuntimeDebug)]
77pub enum Destination {
78 AccountId32 { id: [u8; 32] },
80 ForeignAccountId32 {
84 para_id: u32,
85 id: [u8; 32],
86 fee: u128,
88 },
89 ForeignAccountId20 {
93 para_id: u32,
94 id: [u8; 20],
95 fee: u128,
97 },
98}
99
100pub struct MessageToXcm<
101 CreateAssetCall,
102 CreateAssetDeposit,
103 InboundQueuePalletInstance,
104 AccountId,
105 Balance,
106 ConvertAssetId,
107 EthereumUniversalLocation,
108 GlobalAssetHubLocation,
109> where
110 CreateAssetCall: Get<CallIndex>,
111 CreateAssetDeposit: Get<u128>,
112 Balance: BalanceT,
113 ConvertAssetId: MaybeEquivalence<TokenId, Location>,
114 EthereumUniversalLocation: Get<InteriorLocation>,
115 GlobalAssetHubLocation: Get<Location>,
116{
117 _phantom: PhantomData<(
118 CreateAssetCall,
119 CreateAssetDeposit,
120 InboundQueuePalletInstance,
121 AccountId,
122 Balance,
123 ConvertAssetId,
124 EthereumUniversalLocation,
125 GlobalAssetHubLocation,
126 )>,
127}
128
129#[derive(Copy, Clone, TypeInfo, PalletError, Encode, Decode, RuntimeDebug)]
131pub enum ConvertMessageError {
132 UnsupportedVersion,
134 InvalidDestination,
135 InvalidToken,
136 UnsupportedFeeAsset,
138 CannotReanchor,
139}
140
141pub trait ConvertMessage {
143 type Balance: BalanceT + From<u128>;
144 type AccountId;
145 fn convert(
147 message_id: H256,
148 message: VersionedMessage,
149 ) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>;
150}
151
152pub type CallIndex = [u8; 2];
153
154impl<
155 CreateAssetCall,
156 CreateAssetDeposit,
157 InboundQueuePalletInstance,
158 AccountId,
159 Balance,
160 ConvertAssetId,
161 EthereumUniversalLocation,
162 GlobalAssetHubLocation,
163 > ConvertMessage
164 for MessageToXcm<
165 CreateAssetCall,
166 CreateAssetDeposit,
167 InboundQueuePalletInstance,
168 AccountId,
169 Balance,
170 ConvertAssetId,
171 EthereumUniversalLocation,
172 GlobalAssetHubLocation,
173 >
174where
175 CreateAssetCall: Get<CallIndex>,
176 CreateAssetDeposit: Get<u128>,
177 InboundQueuePalletInstance: Get<u8>,
178 Balance: BalanceT + From<u128>,
179 AccountId: Into<[u8; 32]>,
180 ConvertAssetId: MaybeEquivalence<TokenId, Location>,
181 EthereumUniversalLocation: Get<InteriorLocation>,
182 GlobalAssetHubLocation: Get<Location>,
183{
184 type Balance = Balance;
185 type AccountId = AccountId;
186
187 fn convert(
188 message_id: H256,
189 message: VersionedMessage,
190 ) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError> {
191 use Command::*;
192 use VersionedMessage::*;
193 match message {
194 V1(MessageV1 { chain_id, command: RegisterToken { token, fee } }) =>
195 Ok(Self::convert_register_token(message_id, chain_id, token, fee)),
196 V1(MessageV1 { chain_id, command: SendToken { token, destination, amount, fee } }) =>
197 Ok(Self::convert_send_token(message_id, chain_id, token, destination, amount, fee)),
198 V1(MessageV1 {
199 chain_id,
200 command: SendNativeToken { token_id, destination, amount, fee },
201 }) => Self::convert_send_native_token(
202 message_id,
203 chain_id,
204 token_id,
205 destination,
206 amount,
207 fee,
208 ),
209 }
210 }
211}
212
213impl<
214 CreateAssetCall,
215 CreateAssetDeposit,
216 InboundQueuePalletInstance,
217 AccountId,
218 Balance,
219 ConvertAssetId,
220 EthereumUniversalLocation,
221 GlobalAssetHubLocation,
222 >
223 MessageToXcm<
224 CreateAssetCall,
225 CreateAssetDeposit,
226 InboundQueuePalletInstance,
227 AccountId,
228 Balance,
229 ConvertAssetId,
230 EthereumUniversalLocation,
231 GlobalAssetHubLocation,
232 >
233where
234 CreateAssetCall: Get<CallIndex>,
235 CreateAssetDeposit: Get<u128>,
236 InboundQueuePalletInstance: Get<u8>,
237 Balance: BalanceT + From<u128>,
238 AccountId: Into<[u8; 32]>,
239 ConvertAssetId: MaybeEquivalence<TokenId, Location>,
240 EthereumUniversalLocation: Get<InteriorLocation>,
241 GlobalAssetHubLocation: Get<Location>,
242{
243 fn convert_register_token(
244 message_id: H256,
245 chain_id: u64,
246 token: H160,
247 fee: u128,
248 ) -> (Xcm<()>, Balance) {
249 let network = Ethereum { chain_id };
250 let xcm_fee: Asset = (Location::parent(), fee).into();
251 let deposit: Asset = (Location::parent(), CreateAssetDeposit::get()).into();
252
253 let total_amount = fee + CreateAssetDeposit::get();
254 let total: Asset = (Location::parent(), total_amount).into();
255
256 let bridge_location = Location::new(2, GlobalConsensus(network));
257
258 let owner = EthereumLocationsConverterFor::<[u8; 32]>::from_chain_id(&chain_id);
259 let asset_id = Self::convert_token_address(network, token);
260 let create_call_index: [u8; 2] = CreateAssetCall::get();
261 let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
262
263 let xcm: Xcm<()> = vec![
264 ReceiveTeleportedAsset(total.into()),
266 BuyExecution { fees: xcm_fee, weight_limit: Unlimited },
268 DepositAsset { assets: Definite(deposit.into()), beneficiary: bridge_location.clone() },
270 SetAppendix(Xcm(vec![
274 RefundSurplus,
275 DepositAsset { assets: AllCounted(1).into(), beneficiary: bridge_location },
276 ])),
277 DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()),
279 UniversalOrigin(GlobalConsensus(network)),
281 Transact {
283 origin_kind: OriginKind::Xcm,
284 fallback_max_weight: Some(Weight::from_parts(400_000_000, 8_000)),
285 call: (
286 create_call_index,
287 asset_id,
288 MultiAddress::<[u8; 32], ()>::Id(owner),
289 MINIMUM_DEPOSIT,
290 )
291 .encode()
292 .into(),
293 },
294 SetTopic(message_id.into()),
296 ]
299 .into();
300
301 (xcm, total_amount.into())
302 }
303
304 fn convert_send_token(
305 message_id: H256,
306 chain_id: u64,
307 token: H160,
308 destination: Destination,
309 amount: u128,
310 asset_hub_fee: u128,
311 ) -> (Xcm<()>, Balance) {
312 let network = Ethereum { chain_id };
313 let asset_hub_fee_asset: Asset = (Location::parent(), asset_hub_fee).into();
314 let asset: Asset = (Self::convert_token_address(network, token), amount).into();
315
316 let (dest_para_id, beneficiary, dest_para_fee) = match destination {
317 Destination::AccountId32 { id } =>
319 (None, Location::new(0, [AccountId32 { network: None, id }]), 0),
320 Destination::ForeignAccountId32 { para_id, id, fee } => (
322 Some(para_id),
323 Location::new(0, [AccountId32 { network: None, id }]),
324 fee,
326 ),
327 Destination::ForeignAccountId20 { para_id, id, fee } => (
329 Some(para_id),
330 Location::new(0, [AccountKey20 { network: None, key: id }]),
331 fee,
333 ),
334 };
335
336 let total_fees = asset_hub_fee.saturating_add(dest_para_fee);
337 let total_fee_asset: Asset = (Location::parent(), total_fees).into();
338 let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
339
340 let mut instructions = vec![
341 ReceiveTeleportedAsset(total_fee_asset.into()),
342 BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited },
343 DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()),
344 UniversalOrigin(GlobalConsensus(network)),
345 ReserveAssetDeposited(asset.clone().into()),
346 ClearOrigin,
347 ];
348
349 match dest_para_id {
350 Some(dest_para_id) => {
351 let dest_para_fee_asset: Asset = (Location::parent(), dest_para_fee).into();
352 let bridge_location = Location::new(2, GlobalConsensus(network));
353
354 instructions.extend(vec![
355 SetAppendix(Xcm(vec![DepositAsset {
358 assets: Wild(AllCounted(2)),
359 beneficiary: bridge_location,
360 }])),
361 DepositReserveAsset {
363 assets: Wild(AllCounted(2)),
366 dest: Location::new(1, [Parachain(dest_para_id)]),
367 xcm: vec![
368 BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited },
370 DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
372 SetTopic(message_id.into()),
374 ]
375 .into(),
376 },
377 ]);
378 },
379 None => {
380 instructions.extend(vec![
381 DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
385 ]);
386 },
387 }
388
389 instructions.push(SetTopic(message_id.into()));
391
392 (instructions.into(), total_fees.into())
395 }
396
397 fn convert_token_address(network: NetworkId, token: H160) -> Location {
399 if token == H160([0; 20]) {
402 Location::new(2, [GlobalConsensus(network)])
403 } else {
404 Location::new(
405 2,
406 [GlobalConsensus(network), AccountKey20 { network: None, key: token.into() }],
407 )
408 }
409 }
410
411 fn convert_send_native_token(
415 message_id: H256,
416 chain_id: u64,
417 token_id: TokenId,
418 destination: Destination,
419 amount: u128,
420 asset_hub_fee: u128,
421 ) -> Result<(Xcm<()>, Balance), ConvertMessageError> {
422 let network = Ethereum { chain_id };
423 let asset_hub_fee_asset: Asset = (Location::parent(), asset_hub_fee).into();
424
425 let beneficiary = match destination {
426 Destination::AccountId32 { id } =>
428 Ok(Location::new(0, [AccountId32 { network: None, id }])),
429 _ => Err(ConvertMessageError::InvalidDestination),
432 }?;
433
434 let total_fee_asset: Asset = (Location::parent(), asset_hub_fee).into();
435
436 let asset_loc =
437 ConvertAssetId::convert(&token_id).ok_or(ConvertMessageError::InvalidToken)?;
438
439 let mut reanchored_asset_loc = asset_loc.clone();
440 reanchored_asset_loc
441 .reanchor(&GlobalAssetHubLocation::get(), &EthereumUniversalLocation::get())
442 .map_err(|_| ConvertMessageError::CannotReanchor)?;
443
444 let asset: Asset = (reanchored_asset_loc, amount).into();
445
446 let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
447
448 let instructions = vec![
449 ReceiveTeleportedAsset(total_fee_asset.clone().into()),
450 BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited },
451 DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()),
452 UniversalOrigin(GlobalConsensus(network)),
453 WithdrawAsset(asset.clone().into()),
454 DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
458 SetTopic(message_id.into()),
459 ];
460
461 Ok((instructions.into(), asset_hub_fee.into()))
464 }
465}
466
467pub struct EthereumLocationsConverterFor<AccountId>(PhantomData<AccountId>);
468impl<AccountId> ConvertLocation<AccountId> for EthereumLocationsConverterFor<AccountId>
469where
470 AccountId: From<[u8; 32]> + Clone,
471{
472 fn convert_location(location: &Location) -> Option<AccountId> {
473 match location.unpack() {
474 (2, [GlobalConsensus(Ethereum { chain_id })]) =>
475 Some(Self::from_chain_id(chain_id).into()),
476 (2, [GlobalConsensus(Ethereum { chain_id }), AccountKey20 { network: _, key }]) =>
477 Some(Self::from_chain_id_with_key(chain_id, *key).into()),
478 _ => None,
479 }
480 }
481}
482
483impl<AccountId> EthereumLocationsConverterFor<AccountId> {
484 pub fn from_chain_id(chain_id: &u64) -> [u8; 32] {
485 (b"ethereum-chain", chain_id).using_encoded(blake2_256)
486 }
487 pub fn from_chain_id_with_key(chain_id: &u64, key: [u8; 20]) -> [u8; 32] {
488 (b"ethereum-chain", chain_id, key).using_encoded(blake2_256)
489 }
490}