#[cfg(test)]
mod tests;
use codec::{Decode, Encode};
use core::marker::PhantomData;
use frame_support::{traits::tokens::Balance as BalanceT, PalletError};
use scale_info::TypeInfo;
use snowbridge_core::TokenId;
use sp_core::{Get, RuntimeDebug, H160, H256};
use sp_io::hashing::blake2_256;
use sp_runtime::{traits::MaybeEquivalence, MultiAddress};
use sp_std::prelude::*;
use xcm::prelude::{Junction::AccountKey20, *};
use xcm_executor::traits::ConvertLocation;
const MINIMUM_DEPOSIT: u128 = 1;
#[derive(Clone, Encode, Decode, RuntimeDebug)]
pub enum VersionedMessage {
V1(MessageV1),
}
#[derive(Clone, Encode, Decode, RuntimeDebug)]
pub struct MessageV1 {
pub chain_id: u64,
pub command: Command,
}
#[derive(Clone, Encode, Decode, RuntimeDebug)]
pub enum Command {
RegisterToken {
token: H160,
fee: u128,
},
SendToken {
token: H160,
destination: Destination,
amount: u128,
fee: u128,
},
SendNativeToken {
token_id: TokenId,
destination: Destination,
amount: u128,
fee: u128,
},
}
#[derive(Clone, Encode, Decode, RuntimeDebug)]
pub enum Destination {
AccountId32 { id: [u8; 32] },
ForeignAccountId32 {
para_id: u32,
id: [u8; 32],
fee: u128,
},
ForeignAccountId20 {
para_id: u32,
id: [u8; 20],
fee: u128,
},
}
pub struct MessageToXcm<
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
ConvertAssetId,
EthereumUniversalLocation,
GlobalAssetHubLocation,
> where
CreateAssetCall: Get<CallIndex>,
CreateAssetDeposit: Get<u128>,
Balance: BalanceT,
ConvertAssetId: MaybeEquivalence<TokenId, Location>,
EthereumUniversalLocation: Get<InteriorLocation>,
GlobalAssetHubLocation: Get<Location>,
{
_phantom: PhantomData<(
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
ConvertAssetId,
EthereumUniversalLocation,
GlobalAssetHubLocation,
)>,
}
#[derive(Copy, Clone, TypeInfo, PalletError, Encode, Decode, RuntimeDebug)]
pub enum ConvertMessageError {
UnsupportedVersion,
InvalidDestination,
InvalidToken,
UnsupportedFeeAsset,
CannotReanchor,
}
pub trait ConvertMessage {
type Balance: BalanceT + From<u128>;
type AccountId;
fn convert(
message_id: H256,
message: VersionedMessage,
) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>;
}
pub type CallIndex = [u8; 2];
impl<
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
ConvertAssetId,
EthereumUniversalLocation,
GlobalAssetHubLocation,
> ConvertMessage
for MessageToXcm<
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
ConvertAssetId,
EthereumUniversalLocation,
GlobalAssetHubLocation,
>
where
CreateAssetCall: Get<CallIndex>,
CreateAssetDeposit: Get<u128>,
InboundQueuePalletInstance: Get<u8>,
Balance: BalanceT + From<u128>,
AccountId: Into<[u8; 32]>,
ConvertAssetId: MaybeEquivalence<TokenId, Location>,
EthereumUniversalLocation: Get<InteriorLocation>,
GlobalAssetHubLocation: Get<Location>,
{
type Balance = Balance;
type AccountId = AccountId;
fn convert(
message_id: H256,
message: VersionedMessage,
) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError> {
use Command::*;
use VersionedMessage::*;
match message {
V1(MessageV1 { chain_id, command: RegisterToken { token, fee } }) =>
Ok(Self::convert_register_token(message_id, chain_id, token, fee)),
V1(MessageV1 { chain_id, command: SendToken { token, destination, amount, fee } }) =>
Ok(Self::convert_send_token(message_id, chain_id, token, destination, amount, fee)),
V1(MessageV1 {
chain_id,
command: SendNativeToken { token_id, destination, amount, fee },
}) => Self::convert_send_native_token(
message_id,
chain_id,
token_id,
destination,
amount,
fee,
),
}
}
}
impl<
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
ConvertAssetId,
EthereumUniversalLocation,
GlobalAssetHubLocation,
>
MessageToXcm<
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
ConvertAssetId,
EthereumUniversalLocation,
GlobalAssetHubLocation,
>
where
CreateAssetCall: Get<CallIndex>,
CreateAssetDeposit: Get<u128>,
InboundQueuePalletInstance: Get<u8>,
Balance: BalanceT + From<u128>,
AccountId: Into<[u8; 32]>,
ConvertAssetId: MaybeEquivalence<TokenId, Location>,
EthereumUniversalLocation: Get<InteriorLocation>,
GlobalAssetHubLocation: Get<Location>,
{
fn convert_register_token(
message_id: H256,
chain_id: u64,
token: H160,
fee: u128,
) -> (Xcm<()>, Balance) {
let network = Ethereum { chain_id };
let xcm_fee: Asset = (Location::parent(), fee).into();
let deposit: Asset = (Location::parent(), CreateAssetDeposit::get()).into();
let total_amount = fee + CreateAssetDeposit::get();
let total: Asset = (Location::parent(), total_amount).into();
let bridge_location = Location::new(2, GlobalConsensus(network));
let owner = EthereumLocationsConverterFor::<[u8; 32]>::from_chain_id(&chain_id);
let asset_id = Self::convert_token_address(network, token);
let create_call_index: [u8; 2] = CreateAssetCall::get();
let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
let xcm: Xcm<()> = vec![
ReceiveTeleportedAsset(total.into()),
BuyExecution { fees: xcm_fee, weight_limit: Unlimited },
DepositAsset { assets: Definite(deposit.into()), beneficiary: bridge_location.clone() },
SetAppendix(Xcm(vec![
RefundSurplus,
DepositAsset { assets: AllCounted(1).into(), beneficiary: bridge_location },
])),
DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()),
UniversalOrigin(GlobalConsensus(network)),
Transact {
origin_kind: OriginKind::Xcm,
call: (
create_call_index,
asset_id,
MultiAddress::<[u8; 32], ()>::Id(owner),
MINIMUM_DEPOSIT,
)
.encode()
.into(),
},
SetTopic(message_id.into()),
]
.into();
(xcm, total_amount.into())
}
fn convert_send_token(
message_id: H256,
chain_id: u64,
token: H160,
destination: Destination,
amount: u128,
asset_hub_fee: u128,
) -> (Xcm<()>, Balance) {
let network = Ethereum { chain_id };
let asset_hub_fee_asset: Asset = (Location::parent(), asset_hub_fee).into();
let asset: Asset = (Self::convert_token_address(network, token), amount).into();
let (dest_para_id, beneficiary, dest_para_fee) = match destination {
Destination::AccountId32 { id } =>
(None, Location::new(0, [AccountId32 { network: None, id }]), 0),
Destination::ForeignAccountId32 { para_id, id, fee } => (
Some(para_id),
Location::new(0, [AccountId32 { network: None, id }]),
fee,
),
Destination::ForeignAccountId20 { para_id, id, fee } => (
Some(para_id),
Location::new(0, [AccountKey20 { network: None, key: id }]),
fee,
),
};
let total_fees = asset_hub_fee.saturating_add(dest_para_fee);
let total_fee_asset: Asset = (Location::parent(), total_fees).into();
let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
let mut instructions = vec![
ReceiveTeleportedAsset(total_fee_asset.into()),
BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited },
DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()),
UniversalOrigin(GlobalConsensus(network)),
ReserveAssetDeposited(asset.clone().into()),
ClearOrigin,
];
match dest_para_id {
Some(dest_para_id) => {
let dest_para_fee_asset: Asset = (Location::parent(), dest_para_fee).into();
let bridge_location = Location::new(2, GlobalConsensus(network));
instructions.extend(vec![
SetAppendix(Xcm(vec![DepositAsset {
assets: Wild(AllCounted(2)),
beneficiary: bridge_location,
}])),
DepositReserveAsset {
assets: Definite(vec![dest_para_fee_asset.clone(), asset].into()),
dest: Location::new(1, [Parachain(dest_para_id)]),
xcm: vec![
BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited },
DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
SetTopic(message_id.into()),
]
.into(),
},
]);
},
None => {
instructions.extend(vec![
DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
]);
},
}
instructions.push(SetTopic(message_id.into()));
(instructions.into(), total_fees.into())
}
fn convert_token_address(network: NetworkId, token: H160) -> Location {
Location::new(
2,
[GlobalConsensus(network), AccountKey20 { network: None, key: token.into() }],
)
}
fn convert_send_native_token(
message_id: H256,
chain_id: u64,
token_id: TokenId,
destination: Destination,
amount: u128,
asset_hub_fee: u128,
) -> Result<(Xcm<()>, Balance), ConvertMessageError> {
let network = Ethereum { chain_id };
let asset_hub_fee_asset: Asset = (Location::parent(), asset_hub_fee).into();
let beneficiary = match destination {
Destination::AccountId32 { id } =>
Ok(Location::new(0, [AccountId32 { network: None, id }])),
_ => Err(ConvertMessageError::InvalidDestination),
}?;
let total_fee_asset: Asset = (Location::parent(), asset_hub_fee).into();
let asset_loc =
ConvertAssetId::convert(&token_id).ok_or(ConvertMessageError::InvalidToken)?;
let mut reanchored_asset_loc = asset_loc.clone();
reanchored_asset_loc
.reanchor(&GlobalAssetHubLocation::get(), &EthereumUniversalLocation::get())
.map_err(|_| ConvertMessageError::CannotReanchor)?;
let asset: Asset = (reanchored_asset_loc, amount).into();
let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
let instructions = vec![
ReceiveTeleportedAsset(total_fee_asset.clone().into()),
BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited },
DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()),
UniversalOrigin(GlobalConsensus(network)),
WithdrawAsset(asset.clone().into()),
DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
SetTopic(message_id.into()),
];
Ok((instructions.into(), asset_hub_fee.into()))
}
}
pub struct EthereumLocationsConverterFor<AccountId>(PhantomData<AccountId>);
impl<AccountId> ConvertLocation<AccountId> for EthereumLocationsConverterFor<AccountId>
where
AccountId: From<[u8; 32]> + Clone,
{
fn convert_location(location: &Location) -> Option<AccountId> {
match location.unpack() {
(2, [GlobalConsensus(Ethereum { chain_id })]) =>
Some(Self::from_chain_id(chain_id).into()),
(2, [GlobalConsensus(Ethereum { chain_id }), AccountKey20 { network: _, key }]) =>
Some(Self::from_chain_id_with_key(chain_id, *key).into()),
_ => None,
}
}
}
impl<AccountId> EthereumLocationsConverterFor<AccountId> {
pub fn from_chain_id(chain_id: &u64) -> [u8; 32] {
(b"ethereum-chain", chain_id).using_encoded(blake2_256)
}
pub fn from_chain_id_with_key(chain_id: &u64, key: [u8; 20]) -> [u8; 32] {
(b"ethereum-chain", chain_id, key).using_encoded(blake2_256)
}
}