mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-31 12:11:02 +00:00
Adds Snowbridge to Rococo runtime (#2522)
# Description Adds Snowbridge to the Rococo bridge hub runtime. Includes config changes required in Rococo asset hub. --------- Co-authored-by: Alistair Singh <alistair.singh7@gmail.com> Co-authored-by: ron <yrong1997@gmail.com> Co-authored-by: Vincent Geddes <vincent.geddes@hey.com> Co-authored-by: claravanstaden <Cats 4 life!>
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Converts messages from Ethereum to XCM messages
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use core::marker::PhantomData;
|
||||
use frame_support::{traits::tokens::Balance as BalanceT, weights::Weight, PalletError};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::{Get, RuntimeDebug, H160};
|
||||
use sp_io::hashing::blake2_256;
|
||||
use sp_runtime::MultiAddress;
|
||||
use sp_std::prelude::*;
|
||||
use xcm::prelude::{Junction::AccountKey20, *};
|
||||
use xcm_executor::traits::ConvertLocation;
|
||||
|
||||
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` pallet
|
||||
RegisterToken {
|
||||
/// The address of the ERC20 token to be bridged over to AssetHub
|
||||
token: H160,
|
||||
/// XCM execution fee on AssetHub
|
||||
fee: u128,
|
||||
},
|
||||
/// Send a token to AssetHub or another parachain
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
/// 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 parachain `para_id` on
|
||||
/// AssetHub, Account `id` on the destination parachain will receive the funds via a
|
||||
/// reserve-backed transfer. See <https://github.com/paritytech/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 parachain `para_id` on
|
||||
/// AssetHub, Account `id` on the destination parachain will receive the funds via a
|
||||
/// reserve-backed transfer. See <https://github.com/paritytech/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,
|
||||
> where
|
||||
CreateAssetCall: Get<CallIndex>,
|
||||
CreateAssetDeposit: Get<u128>,
|
||||
Balance: BalanceT,
|
||||
{
|
||||
_phantom: PhantomData<(
|
||||
CreateAssetCall,
|
||||
CreateAssetDeposit,
|
||||
InboundQueuePalletInstance,
|
||||
AccountId,
|
||||
Balance,
|
||||
)>,
|
||||
}
|
||||
|
||||
/// Reason why a message conversion failed.
|
||||
#[derive(Copy, Clone, TypeInfo, PalletError, Encode, Decode, RuntimeDebug)]
|
||||
pub enum ConvertMessageError {
|
||||
/// The message version is not supported for conversion.
|
||||
UnsupportedVersion,
|
||||
}
|
||||
|
||||
/// 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: VersionedMessage) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>;
|
||||
}
|
||||
|
||||
pub type CallIndex = [u8; 2];
|
||||
|
||||
impl<CreateAssetCall, CreateAssetDeposit, InboundQueuePalletInstance, AccountId, Balance>
|
||||
ConvertMessage
|
||||
for MessageToXcm<
|
||||
CreateAssetCall,
|
||||
CreateAssetDeposit,
|
||||
InboundQueuePalletInstance,
|
||||
AccountId,
|
||||
Balance,
|
||||
> where
|
||||
CreateAssetCall: Get<CallIndex>,
|
||||
CreateAssetDeposit: Get<u128>,
|
||||
InboundQueuePalletInstance: Get<u8>,
|
||||
Balance: BalanceT + From<u128>,
|
||||
AccountId: Into<[u8; 32]>,
|
||||
{
|
||||
type Balance = Balance;
|
||||
type AccountId = AccountId;
|
||||
|
||||
fn convert(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(chain_id, token, fee)),
|
||||
V1(MessageV1 { chain_id, command: SendToken { token, destination, amount, fee } }) =>
|
||||
Ok(Self::convert_send_token(chain_id, token, destination, amount, fee)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<CreateAssetCall, CreateAssetDeposit, InboundQueuePalletInstance, AccountId, Balance>
|
||||
MessageToXcm<CreateAssetCall, CreateAssetDeposit, InboundQueuePalletInstance, AccountId, Balance>
|
||||
where
|
||||
CreateAssetCall: Get<CallIndex>,
|
||||
CreateAssetDeposit: Get<u128>,
|
||||
InboundQueuePalletInstance: Get<u8>,
|
||||
Balance: BalanceT + From<u128>,
|
||||
AccountId: Into<[u8; 32]>,
|
||||
{
|
||||
fn convert_register_token(chain_id: u64, token: H160, fee: u128) -> (Xcm<()>, Balance) {
|
||||
let network = Ethereum { chain_id };
|
||||
let xcm_fee: MultiAsset = (MultiLocation::parent(), fee).into();
|
||||
let deposit: MultiAsset = (MultiLocation::parent(), CreateAssetDeposit::get()).into();
|
||||
|
||||
let total_amount = fee + CreateAssetDeposit::get();
|
||||
let total: MultiAsset = (MultiLocation::parent(), total_amount).into();
|
||||
|
||||
let bridge_location: MultiLocation = (Parent, Parent, GlobalConsensus(network)).into();
|
||||
|
||||
let owner = GlobalConsensusEthereumConvertsFor::<[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 },
|
||||
// Only our inbound-queue pallet is allowed to invoke `UniversalOrigin`
|
||||
DescendOrigin(X1(PalletInstance(inbound_queue_pallet_index))),
|
||||
// Change origin to the bridge.
|
||||
UniversalOrigin(GlobalConsensus(network)),
|
||||
// Call create_asset on foreign assets pallet.
|
||||
Transact {
|
||||
origin_kind: OriginKind::Xcm,
|
||||
require_weight_at_most: Weight::from_parts(400_000_000, 8_000),
|
||||
call: (
|
||||
create_call_index,
|
||||
asset_id,
|
||||
MultiAddress::<[u8; 32], ()>::Id(owner),
|
||||
MINIMUM_DEPOSIT,
|
||||
)
|
||||
.encode()
|
||||
.into(),
|
||||
},
|
||||
RefundSurplus,
|
||||
// Clear the origin so that remaining assets in holding
|
||||
// are claimable by the physical origin (BridgeHub)
|
||||
ClearOrigin,
|
||||
]
|
||||
.into();
|
||||
|
||||
(xcm, total_amount.into())
|
||||
}
|
||||
|
||||
fn convert_send_token(
|
||||
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: MultiAsset = (MultiLocation::parent(), asset_hub_fee).into();
|
||||
let asset: MultiAsset = (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,
|
||||
MultiLocation { parents: 0, interior: X1(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),
|
||||
MultiLocation { parents: 0, interior: X1(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),
|
||||
MultiLocation { parents: 0, interior: X1(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: MultiAsset = (MultiLocation::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(X1(PalletInstance(inbound_queue_pallet_index))),
|
||||
UniversalOrigin(GlobalConsensus(network)),
|
||||
ReserveAssetDeposited(asset.clone().into()),
|
||||
ClearOrigin,
|
||||
];
|
||||
|
||||
match dest_para_id {
|
||||
Some(dest_para_id) => {
|
||||
let dest_para_fee_asset: MultiAsset =
|
||||
(MultiLocation::parent(), dest_para_fee).into();
|
||||
|
||||
instructions.extend(vec![
|
||||
// Perform a deposit reserve to send to destination chain.
|
||||
DepositReserveAsset {
|
||||
assets: Definite(vec![dest_para_fee_asset.clone(), asset.clone()].into()),
|
||||
dest: MultiLocation { parents: 1, interior: X1(Parachain(dest_para_id)) },
|
||||
xcm: vec![
|
||||
// Buy execution on target.
|
||||
BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited },
|
||||
// Deposit asset to beneficiary.
|
||||
DepositAsset { assets: Definite(asset.into()), beneficiary },
|
||||
]
|
||||
.into(),
|
||||
},
|
||||
]);
|
||||
},
|
||||
None => {
|
||||
instructions.extend(vec![
|
||||
// Deposit asset to beneficiary.
|
||||
DepositAsset { assets: Definite(asset.into()), beneficiary },
|
||||
]);
|
||||
},
|
||||
}
|
||||
|
||||
(instructions.into(), total_fees.into())
|
||||
}
|
||||
|
||||
// Convert ERC20 token address to a Multilocation that can be understood by Assets Hub.
|
||||
fn convert_token_address(network: NetworkId, token: H160) -> MultiLocation {
|
||||
MultiLocation {
|
||||
parents: 2,
|
||||
interior: X2(
|
||||
GlobalConsensus(network),
|
||||
AccountKey20 { network: None, key: token.into() },
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GlobalConsensusEthereumConvertsFor<AccountId>(PhantomData<AccountId>);
|
||||
impl<AccountId> ConvertLocation<AccountId> for GlobalConsensusEthereumConvertsFor<AccountId>
|
||||
where
|
||||
AccountId: From<[u8; 32]> + Clone,
|
||||
{
|
||||
fn convert_location(location: &MultiLocation) -> Option<AccountId> {
|
||||
if let MultiLocation { interior: X1(GlobalConsensus(Ethereum { chain_id })), .. } = location
|
||||
{
|
||||
Some(Self::from_chain_id(chain_id).into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<AccountId> GlobalConsensusEthereumConvertsFor<AccountId> {
|
||||
pub fn from_chain_id(chain_id: &u64) -> [u8; 32] {
|
||||
(b"ethereum-chain", chain_id).using_encoded(blake2_256)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use super::GlobalConsensusEthereumConvertsFor;
|
||||
use crate::inbound::CallIndex;
|
||||
use frame_support::parameter_types;
|
||||
use hex_literal::hex;
|
||||
use xcm::v3::prelude::*;
|
||||
use xcm_executor::traits::ConvertLocation;
|
||||
|
||||
const NETWORK: NetworkId = Ethereum { chain_id: 11155111 };
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contract_location_with_network_converts_successfully() {
|
||||
let expected_account: [u8; 32] =
|
||||
hex!("ce796ae65569a670d0c1cc1ac12515a3ce21b5fbf729d63d7b289baad070139d");
|
||||
let contract_location = MultiLocation { parents: 2, interior: X1(GlobalConsensus(NETWORK)) };
|
||||
|
||||
let account =
|
||||
GlobalConsensusEthereumConvertsFor::<[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 =
|
||||
MultiLocation { parents: 2, interior: X2(GlobalConsensus(Polkadot), Parachain(1000)) };
|
||||
|
||||
assert_eq!(
|
||||
GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&contract_location),
|
||||
None,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub mod inbound;
|
||||
pub mod outbound;
|
||||
@@ -0,0 +1,282 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Converts XCM messages into simpler commands that can be processed by the Gateway contract
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use core::slice::Iter;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
|
||||
use frame_support::{ensure, traits::Get};
|
||||
use snowbridge_core::{
|
||||
outbound::{AgentExecuteCommand, Command, Message, SendMessage},
|
||||
ChannelId, ParaId,
|
||||
};
|
||||
use sp_core::{H160, H256};
|
||||
use sp_std::{iter::Peekable, marker::PhantomData, prelude::*};
|
||||
use xcm::v3::prelude::*;
|
||||
use xcm_executor::traits::{ConvertLocation, ExportXcm};
|
||||
|
||||
pub struct EthereumBlobExporter<
|
||||
UniversalLocation,
|
||||
EthereumNetwork,
|
||||
OutboundQueue,
|
||||
AgentHashedDescription,
|
||||
>(PhantomData<(UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription)>);
|
||||
|
||||
impl<UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription> ExportXcm
|
||||
for EthereumBlobExporter<UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription>
|
||||
where
|
||||
UniversalLocation: Get<InteriorMultiLocation>,
|
||||
EthereumNetwork: Get<NetworkId>,
|
||||
OutboundQueue: SendMessage<Balance = u128>,
|
||||
AgentHashedDescription: ConvertLocation<H256>,
|
||||
{
|
||||
type Ticket = (Vec<u8>, XcmHash);
|
||||
|
||||
fn validate(
|
||||
network: NetworkId,
|
||||
_channel: u32,
|
||||
universal_source: &mut Option<InteriorMultiLocation>,
|
||||
destination: &mut Option<InteriorMultiLocation>,
|
||||
message: &mut Option<Xcm<()>>,
|
||||
) -> SendResult<Self::Ticket> {
|
||||
let expected_network = EthereumNetwork::get();
|
||||
let universal_location = UniversalLocation::get();
|
||||
|
||||
if network != expected_network {
|
||||
log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched bridge network {network:?}.");
|
||||
return Err(SendError::NotApplicable)
|
||||
}
|
||||
|
||||
let dest = destination.take().ok_or(SendError::MissingArgument)?;
|
||||
if dest != Here {
|
||||
log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched remote destination {dest:?}.");
|
||||
return Err(SendError::NotApplicable)
|
||||
}
|
||||
|
||||
let (local_net, local_sub) = universal_source
|
||||
.take()
|
||||
.ok_or_else(|| {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "universal source not provided.");
|
||||
SendError::MissingArgument
|
||||
})?
|
||||
.split_global()
|
||||
.map_err(|()| {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "could not get global consensus from universal source '{universal_source:?}'.");
|
||||
SendError::Unroutable
|
||||
})?;
|
||||
|
||||
if Ok(local_net) != universal_location.global_consensus() {
|
||||
log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched relay network {local_net:?}.");
|
||||
return Err(SendError::NotApplicable)
|
||||
}
|
||||
|
||||
let para_id = match local_sub {
|
||||
X1(Parachain(para_id)) => para_id,
|
||||
_ => {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "could not get parachain id from universal source '{local_sub:?}'.");
|
||||
return Err(SendError::MissingArgument)
|
||||
},
|
||||
};
|
||||
|
||||
let message = message.take().ok_or_else(|| {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "xcm message not provided.");
|
||||
SendError::MissingArgument
|
||||
})?;
|
||||
|
||||
let mut converter = XcmConverter::new(&message, &expected_network);
|
||||
let (agent_execute_command, message_id) = converter.convert().map_err(|err|{
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to pattern matching error '{err:?}'.");
|
||||
SendError::Unroutable
|
||||
})?;
|
||||
|
||||
let source_location: MultiLocation = MultiLocation { parents: 1, interior: local_sub };
|
||||
let agent_id = match AgentHashedDescription::convert_location(&source_location) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to not being able to create agent id. '{source_location:?}'");
|
||||
return Err(SendError::Unroutable)
|
||||
},
|
||||
};
|
||||
|
||||
let channel_id: ChannelId = ParaId::from(para_id).into();
|
||||
|
||||
let outbound_message = Message {
|
||||
id: Some(message_id.into()),
|
||||
channel_id,
|
||||
command: Command::AgentExecute { agent_id, command: agent_execute_command },
|
||||
};
|
||||
|
||||
// validate the message
|
||||
let (ticket, fee) = OutboundQueue::validate(&outbound_message).map_err(|err| {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue validation of message failed. {err:?}");
|
||||
SendError::Unroutable
|
||||
})?;
|
||||
|
||||
// convert fee to MultiAsset
|
||||
let fee = MultiAsset::from((MultiLocation::parent(), fee.total())).into();
|
||||
|
||||
Ok(((ticket.encode(), message_id), fee))
|
||||
}
|
||||
|
||||
fn deliver(blob: (Vec<u8>, XcmHash)) -> Result<XcmHash, SendError> {
|
||||
let ticket: OutboundQueue::Ticket = OutboundQueue::Ticket::decode(&mut blob.0.as_ref())
|
||||
.map_err(|_| {
|
||||
log::trace!(target: "xcm::ethereum_blob_exporter", "undeliverable due to decoding error");
|
||||
SendError::NotApplicable
|
||||
})?;
|
||||
|
||||
let message_id = OutboundQueue::deliver(ticket).map_err(|_| {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue submit of message failed");
|
||||
SendError::Transport("other transport error")
|
||||
})?;
|
||||
|
||||
log::info!(target: "xcm::ethereum_blob_exporter", "message delivered {message_id:#?}.");
|
||||
Ok(message_id.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can be thrown to the pattern matching step.
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum XcmConverterError {
|
||||
UnexpectedEndOfXcm,
|
||||
EndOfXcmMessageExpected,
|
||||
WithdrawAssetExpected,
|
||||
DepositAssetExpected,
|
||||
NoReserveAssets,
|
||||
FilterDoesNotConsumeAllAssets,
|
||||
TooManyAssets,
|
||||
ZeroAssetTransfer,
|
||||
BeneficiaryResolutionFailed,
|
||||
AssetResolutionFailed,
|
||||
InvalidFeeAsset,
|
||||
SetTopicExpected,
|
||||
}
|
||||
|
||||
macro_rules! match_expression {
|
||||
($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )?, $value:expr $(,)?) => {
|
||||
match $expression {
|
||||
$( $pattern )|+ $( if $guard )? => Some($value),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct XcmConverter<'a, Call> {
|
||||
iter: Peekable<Iter<'a, Instruction<Call>>>,
|
||||
ethereum_network: &'a NetworkId,
|
||||
}
|
||||
impl<'a, Call> XcmConverter<'a, Call> {
|
||||
fn new(message: &'a Xcm<Call>, ethereum_network: &'a NetworkId) -> Self {
|
||||
Self { iter: message.inner().iter().peekable(), ethereum_network }
|
||||
}
|
||||
|
||||
fn convert(&mut self) -> Result<(AgentExecuteCommand, [u8; 32]), XcmConverterError> {
|
||||
// Get withdraw/deposit and make native tokens create message.
|
||||
let result = self.native_tokens_unlock_message()?;
|
||||
|
||||
// All xcm instructions must be consumed before exit.
|
||||
if self.next().is_ok() {
|
||||
return Err(XcmConverterError::EndOfXcmMessageExpected)
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn native_tokens_unlock_message(
|
||||
&mut self,
|
||||
) -> Result<(AgentExecuteCommand, [u8; 32]), XcmConverterError> {
|
||||
use XcmConverterError::*;
|
||||
|
||||
// Get the reserve assets from WithdrawAsset.
|
||||
let reserve_assets =
|
||||
match_expression!(self.next()?, WithdrawAsset(reserve_assets), reserve_assets)
|
||||
.ok_or(WithdrawAssetExpected)?;
|
||||
|
||||
// Check if clear origin exists and skip over it.
|
||||
if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() {
|
||||
let _ = self.next();
|
||||
}
|
||||
|
||||
// Get the fee asset item from BuyExecution or continue parsing.
|
||||
let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees);
|
||||
if fee_asset.is_some() {
|
||||
let _ = self.next();
|
||||
}
|
||||
|
||||
let (deposit_assets, beneficiary) = match_expression!(
|
||||
self.next()?,
|
||||
DepositAsset { assets, beneficiary },
|
||||
(assets, beneficiary)
|
||||
)
|
||||
.ok_or(DepositAssetExpected)?;
|
||||
|
||||
// assert that the beneficiary is AccountKey20.
|
||||
let recipient = match_expression!(
|
||||
beneficiary,
|
||||
MultiLocation { parents: 0, interior: X1(AccountKey20 { network, key }) }
|
||||
if self.network_matches(network),
|
||||
H160(*key)
|
||||
)
|
||||
.ok_or(BeneficiaryResolutionFailed)?;
|
||||
|
||||
// Make sure there are reserved assets.
|
||||
if reserve_assets.len() == 0 {
|
||||
return Err(NoReserveAssets)
|
||||
}
|
||||
|
||||
// Check the the deposit asset filter matches what was reserved.
|
||||
if reserve_assets.inner().iter().any(|asset| !deposit_assets.matches(asset)) {
|
||||
return Err(FilterDoesNotConsumeAllAssets)
|
||||
}
|
||||
|
||||
// We only support a single asset at a time.
|
||||
ensure!(reserve_assets.len() == 1, TooManyAssets);
|
||||
let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?;
|
||||
|
||||
// If there was a fee specified verify it.
|
||||
if let Some(fee_asset) = fee_asset {
|
||||
// The fee asset must be the same as the reserve asset.
|
||||
if fee_asset.id != reserve_asset.id || fee_asset.fun > reserve_asset.fun {
|
||||
return Err(InvalidFeeAsset)
|
||||
}
|
||||
}
|
||||
|
||||
let (token, amount) = match_expression!(
|
||||
reserve_asset,
|
||||
MultiAsset {
|
||||
id: Concrete(MultiLocation { parents: 0, interior: X1(AccountKey20 { network , key })}),
|
||||
fun: Fungible(amount)
|
||||
} if self.network_matches(network),
|
||||
(H160(*key), *amount)
|
||||
)
|
||||
.ok_or(AssetResolutionFailed)?;
|
||||
|
||||
// transfer amount must be greater than 0.
|
||||
ensure!(amount > 0, ZeroAssetTransfer);
|
||||
|
||||
// Check if there is a SetTopic and skip over it if found.
|
||||
let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?;
|
||||
|
||||
Ok((AgentExecuteCommand::TransferToken { token, recipient, amount }, *topic_id))
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Result<&'a Instruction<Call>, XcmConverterError> {
|
||||
self.iter.next().ok_or(XcmConverterError::UnexpectedEndOfXcm)
|
||||
}
|
||||
|
||||
fn peek(&mut self) -> Result<&&'a Instruction<Call>, XcmConverterError> {
|
||||
self.iter.peek().ok_or(XcmConverterError::UnexpectedEndOfXcm)
|
||||
}
|
||||
|
||||
fn network_matches(&self, network: &Option<NetworkId>) -> bool {
|
||||
if let Some(network) = network {
|
||||
network == self.ethereum_network
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user