674 lines
22 KiB
Rust
674 lines
22 KiB
Rust
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
|
//! Converts messages from Ethereum to XCM messages
|
|
|
|
use crate::{CallIndex, EthereumLocationsConverterFor};
|
|
use codec::{Decode, DecodeWithMemTracking, Encode};
|
|
use core::marker::PhantomData;
|
|
use pezframe_support::{traits::tokens::Balance as BalanceT, PalletError};
|
|
use pezsnowbridge_core::TokenId;
|
|
use pezsp_core::{Get, RuntimeDebug, H160, H256};
|
|
use pezsp_runtime::{traits::MaybeConvert, MultiAddress};
|
|
use pezsp_std::prelude::*;
|
|
use scale_info::TypeInfo;
|
|
use xcm::prelude::{Junction::AccountKey20, *};
|
|
|
|
const MINIMUM_DEPOSIT: u128 = 1;
|
|
|
|
/// Messages from Ethereum are versioned. This is because in future,
|
|
/// we may want to evolve the protocol so that the ethereum side sends XCM messages directly.
|
|
/// Instead having BridgeHub transcode the messages into XCM.
|
|
#[derive(Clone, Encode, Decode, RuntimeDebug)]
|
|
pub enum VersionedMessage {
|
|
V1(MessageV1),
|
|
}
|
|
|
|
/// For V1, the ethereum side sends messages which are transcoded into XCM. These messages are
|
|
/// self-contained, in that they can be transcoded using only information in the message.
|
|
#[derive(Clone, Encode, Decode, RuntimeDebug)]
|
|
pub struct MessageV1 {
|
|
/// EIP-155 chain id of the origin Ethereum network
|
|
pub chain_id: u64,
|
|
/// The command originating from the Gateway contract
|
|
pub command: Command,
|
|
}
|
|
|
|
#[derive(Clone, Encode, Decode, RuntimeDebug)]
|
|
pub enum Command {
|
|
/// Register a wrapped token on the AssetHub `ForeignAssets` pezpallet
|
|
RegisterToken {
|
|
/// The address of the ERC20 token to be bridged over to AssetHub
|
|
token: H160,
|
|
/// XCM execution fee on AssetHub
|
|
fee: u128,
|
|
},
|
|
/// Send Ethereum token to AssetHub or another teyrchain
|
|
SendToken {
|
|
/// The address of the ERC20 token to be bridged over to AssetHub
|
|
token: H160,
|
|
/// The destination for the transfer
|
|
destination: Destination,
|
|
/// Amount to transfer
|
|
amount: u128,
|
|
/// XCM execution fee on AssetHub
|
|
fee: u128,
|
|
},
|
|
/// Send Pezkuwi token back to the original teyrchain
|
|
SendNativeToken {
|
|
/// The Id of the token
|
|
token_id: TokenId,
|
|
/// The destination for the transfer
|
|
destination: Destination,
|
|
/// Amount to transfer
|
|
amount: u128,
|
|
/// XCM execution fee on AssetHub
|
|
fee: u128,
|
|
},
|
|
}
|
|
|
|
/// Destination for bridged tokens
|
|
#[derive(Clone, Encode, Decode, RuntimeDebug)]
|
|
pub enum Destination {
|
|
/// The funds will be deposited into account `id` on AssetHub
|
|
AccountId32 { id: [u8; 32] },
|
|
/// The funds will deposited into the sovereign account of destination teyrchain `para_id` on
|
|
/// AssetHub, Account `id` on the destination teyrchain will receive the funds via a
|
|
/// reserve-backed transfer. See <https://github.com/pezkuwichain/xcm-format#depositreserveasset>
|
|
ForeignAccountId32 {
|
|
para_id: u32,
|
|
id: [u8; 32],
|
|
/// XCM execution fee on final destination
|
|
fee: u128,
|
|
},
|
|
/// The funds will deposited into the sovereign account of destination teyrchain `para_id` on
|
|
/// AssetHub, Account `id` on the destination teyrchain will receive the funds via a
|
|
/// reserve-backed transfer. See <https://github.com/pezkuwichain/xcm-format#depositreserveasset>
|
|
ForeignAccountId20 {
|
|
para_id: u32,
|
|
id: [u8; 20],
|
|
/// XCM execution fee on final destination
|
|
fee: u128,
|
|
},
|
|
}
|
|
|
|
pub struct MessageToXcm<
|
|
CreateAssetCall,
|
|
CreateAssetDeposit,
|
|
InboundQueuePalletInstance,
|
|
AccountId,
|
|
Balance,
|
|
ConvertAssetId,
|
|
EthereumUniversalLocation,
|
|
GlobalAssetHubLocation,
|
|
> where
|
|
CreateAssetCall: Get<CallIndex>,
|
|
CreateAssetDeposit: Get<u128>,
|
|
Balance: BalanceT,
|
|
ConvertAssetId: MaybeConvert<TokenId, Location>,
|
|
EthereumUniversalLocation: Get<InteriorLocation>,
|
|
GlobalAssetHubLocation: Get<Location>,
|
|
{
|
|
_phantom: PhantomData<(
|
|
CreateAssetCall,
|
|
CreateAssetDeposit,
|
|
InboundQueuePalletInstance,
|
|
AccountId,
|
|
Balance,
|
|
ConvertAssetId,
|
|
EthereumUniversalLocation,
|
|
GlobalAssetHubLocation,
|
|
)>,
|
|
}
|
|
|
|
/// Reason why a message conversion failed.
|
|
#[derive(
|
|
Copy, Clone, TypeInfo, PalletError, Encode, Decode, DecodeWithMemTracking, RuntimeDebug,
|
|
)]
|
|
pub enum ConvertMessageError {
|
|
/// The message version is not supported for conversion.
|
|
UnsupportedVersion,
|
|
InvalidDestination,
|
|
InvalidToken,
|
|
/// The fee asset is not supported for conversion.
|
|
UnsupportedFeeAsset,
|
|
CannotReanchor,
|
|
}
|
|
|
|
/// convert the inbound message to xcm which will be forwarded to the destination chain
|
|
pub trait ConvertMessage {
|
|
type Balance: BalanceT + From<u128>;
|
|
type AccountId;
|
|
/// Converts a versioned message into an XCM message and an optional topicID
|
|
fn convert(
|
|
message_id: H256,
|
|
message: VersionedMessage,
|
|
) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>;
|
|
}
|
|
|
|
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: MaybeConvert<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: MaybeConvert<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![
|
|
// Teleport required fees.
|
|
ReceiveTeleportedAsset(total.into()),
|
|
// Pay for execution.
|
|
BuyExecution { fees: xcm_fee, weight_limit: Unlimited },
|
|
// Fund the snowbridge sovereign with the required deposit for creation.
|
|
DepositAsset { assets: Definite(deposit.into()), beneficiary: bridge_location.clone() },
|
|
// This `SetAppendix` ensures that `xcm_fee` not spent by `Transact` will be
|
|
// deposited to snowbridge sovereign, instead of being trapped, regardless of
|
|
// `Transact` success or not.
|
|
SetAppendix(Xcm(vec![
|
|
RefundSurplus,
|
|
DepositAsset { assets: AllCounted(1).into(), beneficiary: bridge_location },
|
|
])),
|
|
// Only our inbound-queue pezpallet is allowed to invoke `UniversalOrigin`.
|
|
DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()),
|
|
// Change origin to the bridge.
|
|
UniversalOrigin(GlobalConsensus(network)),
|
|
// Call create_asset on foreign assets pezpallet.
|
|
Transact {
|
|
origin_kind: OriginKind::Xcm,
|
|
fallback_max_weight: Some(Weight::from_parts(400_000_000, 8_000)),
|
|
call: (
|
|
create_call_index,
|
|
asset_id,
|
|
MultiAddress::<[u8; 32], ()>::Id(owner),
|
|
MINIMUM_DEPOSIT,
|
|
)
|
|
.encode()
|
|
.into(),
|
|
},
|
|
// Forward message id to Asset Hub
|
|
SetTopic(message_id.into()),
|
|
// Once the program ends here, appendix program will run, which will deposit any
|
|
// leftover fee to snowbridge sovereign.
|
|
]
|
|
.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 {
|
|
// Final destination is a 32-byte account on AssetHub
|
|
Destination::AccountId32 { id } =>
|
|
(None, Location::new(0, [AccountId32 { network: None, id }]), 0),
|
|
// Final destination is a 32-byte account on a sibling of AssetHub
|
|
Destination::ForeignAccountId32 { para_id, id, fee } => (
|
|
Some(para_id),
|
|
Location::new(0, [AccountId32 { network: None, id }]),
|
|
// Total fee needs to cover execution on AssetHub and Sibling
|
|
fee,
|
|
),
|
|
// Final destination is a 20-byte account on a sibling of AssetHub
|
|
Destination::ForeignAccountId20 { para_id, id, fee } => (
|
|
Some(para_id),
|
|
Location::new(0, [AccountKey20 { network: None, key: id }]),
|
|
// Total fee needs to cover execution on AssetHub and Sibling
|
|
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![
|
|
// After program finishes deposit any leftover assets to the snowbridge
|
|
// sovereign.
|
|
SetAppendix(Xcm(vec![DepositAsset {
|
|
assets: Wild(AllCounted(2)),
|
|
beneficiary: bridge_location,
|
|
}])),
|
|
// Perform a deposit reserve to send to destination chain.
|
|
DepositReserveAsset {
|
|
// Send over assets and unspent fees, XCM delivery fee will be charged from
|
|
// here.
|
|
assets: Wild(AllCounted(2)),
|
|
dest: Location::new(1, [Teyrchain(dest_para_id)]),
|
|
xcm: vec![
|
|
// Buy execution on target.
|
|
BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited },
|
|
// Deposit assets to beneficiary.
|
|
DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
|
|
// Forward message id to destination teyrchain.
|
|
SetTopic(message_id.into()),
|
|
]
|
|
.into(),
|
|
},
|
|
]);
|
|
},
|
|
None => {
|
|
instructions.extend(vec![
|
|
// Deposit both asset and fees to beneficiary so the fees will not get
|
|
// trapped. Another benefit is when fees left more than ED on AssetHub could be
|
|
// used to create the beneficiary account in case it does not exist.
|
|
DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
|
|
]);
|
|
},
|
|
}
|
|
|
|
// Forward message id to Asset Hub.
|
|
instructions.push(SetTopic(message_id.into()));
|
|
|
|
// The `instructions` to forward to AssetHub, and the `total_fees` to locally burn (since
|
|
// they are teleported within `instructions`).
|
|
(instructions.into(), total_fees.into())
|
|
}
|
|
|
|
// Convert ERC20 token address to a location that can be understood by Assets Hub.
|
|
fn convert_token_address(network: NetworkId, token: H160) -> Location {
|
|
if token == H160([0; 20]) {
|
|
Location::new(2, [GlobalConsensus(network)])
|
|
} else {
|
|
Location::new(
|
|
2,
|
|
[GlobalConsensus(network), AccountKey20 { network: None, key: token.into() }],
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Constructs an XCM message destined for AssetHub that withdraws assets from the sovereign
|
|
/// account of the Gateway contract and either deposits those assets into a recipient account or
|
|
/// forwards the assets to another teyrchain.
|
|
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 {
|
|
// Final destination is a 32-byte account on AssetHub
|
|
Destination::AccountId32 { id } =>
|
|
Ok(Location::new(0, [AccountId32 { network: None, id }])),
|
|
// Forwarding to a destination teyrchain is not allowed for PNA and is validated on the
|
|
// Ethereum side. https://github.com/Snowfork/snowbridge/blob/e87ddb2215b513455c844463a25323bb9c01ff36/contracts/src/Assets.sol#L216-L224
|
|
_ => Err(ConvertMessageError::InvalidDestination),
|
|
}?;
|
|
|
|
let total_fee_asset: Asset = (Location::parent(), asset_hub_fee).into();
|
|
|
|
let asset_loc =
|
|
ConvertAssetId::maybe_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()),
|
|
// Deposit both asset and fees to beneficiary so the fees will not get
|
|
// trapped. Another benefit is when fees left more than ED on AssetHub could be
|
|
// used to create the beneficiary account in case it does not exist.
|
|
DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
|
|
SetTopic(message_id.into()),
|
|
];
|
|
|
|
// `total_fees` to burn on this chain when sending `instructions` to run on AH (which also
|
|
// teleport fees)
|
|
Ok((instructions.into(), asset_hub_fee.into()))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::{
|
|
v1::{Command, ConvertMessage, Destination, MessageToXcm, MessageV1, VersionedMessage},
|
|
CallIndex, EthereumLocationsConverterFor,
|
|
};
|
|
use hex_literal::hex;
|
|
use pezframe_support::{assert_ok, parameter_types};
|
|
use pezsnowbridge_test_utils::mock_converter::{
|
|
add_location_override, reanchor_to_ethereum, LocationIdConvert,
|
|
};
|
|
use pezsp_core::H160;
|
|
use pezsp_runtime::{
|
|
traits::{IdentifyAccount, Verify},
|
|
MultiSignature,
|
|
};
|
|
use xcm::prelude::*;
|
|
use xcm_executor::traits::ConvertLocation;
|
|
|
|
pub const CHAIN_ID: u64 = 1;
|
|
const NETWORK: NetworkId = Ethereum { chain_id: CHAIN_ID };
|
|
|
|
parameter_types! {
|
|
pub EthereumNetwork: NetworkId = NETWORK;
|
|
pub const CreateAssetCall: CallIndex = [1, 1];
|
|
pub const CreateAssetExecutionFee: u128 = 123;
|
|
pub const CreateAssetDeposit: u128 = 891;
|
|
pub const SendTokenExecutionFee: u128 = 592;
|
|
pub const InboundQueuePalletInstance: u8 = 80;
|
|
pub EthereumUniversalLocation: InteriorLocation =
|
|
[GlobalConsensus(NETWORK)].into();
|
|
pub AssetHubFromEthereum: Location = Location::new(1,[GlobalConsensus(Pezkuwi),Teyrchain(1000)]);
|
|
pub EthereumLocation: Location = Location::new(2,EthereumUniversalLocation::get());
|
|
pub BridgeHubContext: InteriorLocation = [GlobalConsensus(Pezkuwi),Teyrchain(1002)].into();
|
|
}
|
|
|
|
type AccountId = <<MultiSignature as Verify>::Signer as IdentifyAccount>::AccountId;
|
|
type Balance = u128;
|
|
|
|
pub type MessageConverter = MessageToXcm<
|
|
CreateAssetCall,
|
|
CreateAssetDeposit,
|
|
InboundQueuePalletInstance,
|
|
AccountId,
|
|
Balance,
|
|
LocationIdConvert,
|
|
EthereumUniversalLocation,
|
|
AssetHubFromEthereum,
|
|
>;
|
|
|
|
#[test]
|
|
fn test_contract_location_with_network_converts_successfully() {
|
|
let expected_account: [u8; 32] =
|
|
hex!("204dfe37731e8e2b4866ad0da9a17c49f434542c3477c5f914a3349acd88ba1a");
|
|
let contract_location = Location::new(2, [GlobalConsensus(NETWORK)]);
|
|
|
|
let account =
|
|
EthereumLocationsConverterFor::<[u8; 32]>::convert_location(&contract_location)
|
|
.unwrap();
|
|
assert_eq!(account, expected_account);
|
|
}
|
|
|
|
#[test]
|
|
fn test_contract_location_with_incorrect_location_fails_convert() {
|
|
let contract_location = Location::new(2, [GlobalConsensus(Pezkuwi), Teyrchain(1000)]);
|
|
|
|
assert_eq!(
|
|
EthereumLocationsConverterFor::<[u8; 32]>::convert_location(&contract_location),
|
|
None,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_reanchor_all_assets() {
|
|
let ethereum_context: InteriorLocation = [GlobalConsensus(Ethereum { chain_id: 1 })].into();
|
|
let ethereum = Location::new(2, ethereum_context.clone());
|
|
let ah_context: InteriorLocation = [GlobalConsensus(Pezkuwi), Teyrchain(1000)].into();
|
|
let global_ah = Location::new(1, ah_context.clone());
|
|
let assets = vec![
|
|
// HEZ
|
|
Location::new(1, []),
|
|
// GLMR (Some Pezkuwi teyrchain currency)
|
|
Location::new(1, [Teyrchain(2004)]),
|
|
// AH asset
|
|
Location::new(0, [PalletInstance(50), GeneralIndex(42)]),
|
|
// KSM
|
|
Location::new(2, [GlobalConsensus(Kusama)]),
|
|
// KAR (Some Kusama teyrchain currency)
|
|
Location::new(2, [GlobalConsensus(Kusama), Teyrchain(2000)]),
|
|
];
|
|
for asset in assets.iter() {
|
|
// reanchor logic in pezpallet_xcm on AH
|
|
let mut reanchored_asset = asset.clone();
|
|
assert_ok!(reanchored_asset.reanchor(ðereum, &ah_context));
|
|
// reanchor back to original location in context of Ethereum
|
|
let mut reanchored_asset_with_ethereum_context = reanchored_asset.clone();
|
|
assert_ok!(
|
|
reanchored_asset_with_ethereum_context.reanchor(&global_ah, ðereum_context)
|
|
);
|
|
assert_eq!(reanchored_asset_with_ethereum_context, asset.clone());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_convert_send_weth() {
|
|
const WETH: H160 = H160([0xff; 20]);
|
|
const AMOUNT: u128 = 1_000_000;
|
|
const FEE: u128 = 1_000;
|
|
const ACCOUNT_ID: [u8; 32] = [0xBA; 32];
|
|
const MESSAGE: VersionedMessage = VersionedMessage::V1(MessageV1 {
|
|
chain_id: CHAIN_ID,
|
|
command: Command::SendToken {
|
|
token: WETH,
|
|
destination: Destination::AccountId32 { id: ACCOUNT_ID },
|
|
amount: AMOUNT,
|
|
fee: FEE,
|
|
},
|
|
});
|
|
let result = MessageConverter::convert([1; 32].into(), MESSAGE);
|
|
assert_ok!(&result);
|
|
let (xcm, fee) = result.unwrap();
|
|
assert_eq!(FEE, fee);
|
|
|
|
let expected_assets = ReserveAssetDeposited(
|
|
vec![Asset {
|
|
id: AssetId(Location {
|
|
parents: 2,
|
|
interior: Junctions::X2(
|
|
[
|
|
GlobalConsensus(NETWORK),
|
|
AccountKey20 { network: None, key: WETH.into() },
|
|
]
|
|
.into(),
|
|
),
|
|
}),
|
|
fun: Fungible(AMOUNT),
|
|
}]
|
|
.into(),
|
|
);
|
|
let actual_assets = xcm.into_iter().find(|x| matches!(x, ReserveAssetDeposited(..)));
|
|
assert_eq!(actual_assets, Some(expected_assets))
|
|
}
|
|
|
|
#[test]
|
|
fn test_convert_send_eth() {
|
|
const ETH: H160 = H160([0x00; 20]);
|
|
const AMOUNT: u128 = 1_000_000;
|
|
const FEE: u128 = 1_000;
|
|
const ACCOUNT_ID: [u8; 32] = [0xBA; 32];
|
|
const MESSAGE: VersionedMessage = VersionedMessage::V1(MessageV1 {
|
|
chain_id: CHAIN_ID,
|
|
command: Command::SendToken {
|
|
token: ETH,
|
|
destination: Destination::AccountId32 { id: ACCOUNT_ID },
|
|
amount: AMOUNT,
|
|
fee: FEE,
|
|
},
|
|
});
|
|
let result = MessageConverter::convert([1; 32].into(), MESSAGE);
|
|
assert_ok!(&result);
|
|
let (xcm, fee) = result.unwrap();
|
|
assert_eq!(FEE, fee);
|
|
|
|
let expected_assets = ReserveAssetDeposited(
|
|
vec![Asset {
|
|
id: AssetId(Location {
|
|
parents: 2,
|
|
interior: Junctions::X1([GlobalConsensus(NETWORK)].into()),
|
|
}),
|
|
fun: Fungible(AMOUNT),
|
|
}]
|
|
.into(),
|
|
);
|
|
let actual_assets = xcm.into_iter().find(|x| matches!(x, ReserveAssetDeposited(..)));
|
|
assert_eq!(actual_assets, Some(expected_assets))
|
|
}
|
|
|
|
#[test]
|
|
fn test_convert_send_dot() {
|
|
let dot_location = Location::parent();
|
|
let (token_id, _) = reanchor_to_ethereum(
|
|
dot_location.clone(),
|
|
EthereumLocation::get(),
|
|
BridgeHubContext::get(),
|
|
);
|
|
add_location_override(
|
|
dot_location.clone(),
|
|
EthereumLocation::get(),
|
|
BridgeHubContext::get(),
|
|
);
|
|
const AMOUNT: u128 = 1_000_000;
|
|
const FEE: u128 = 1_000;
|
|
const ACCOUNT_ID: [u8; 32] = [0xBA; 32];
|
|
let message: VersionedMessage = VersionedMessage::V1(MessageV1 {
|
|
chain_id: CHAIN_ID,
|
|
command: Command::SendNativeToken {
|
|
token_id,
|
|
destination: Destination::AccountId32 { id: ACCOUNT_ID },
|
|
amount: AMOUNT,
|
|
fee: FEE,
|
|
},
|
|
});
|
|
|
|
let result = MessageConverter::convert([1; 32].into(), message);
|
|
assert_ok!(&result);
|
|
let (xcm, fee) = result.unwrap();
|
|
assert_eq!(FEE, fee);
|
|
|
|
let expected_assets = WithdrawAsset(
|
|
vec![Asset { id: AssetId(Location::parent()), fun: Fungible(AMOUNT) }].into(),
|
|
);
|
|
let actual_assets = xcm.into_iter().find(|x| matches!(x, WithdrawAsset(..)));
|
|
assert_eq!(actual_assets, Some(expected_assets))
|
|
}
|
|
}
|