universal_exports.rs 25.6 KiB
Newer Older
// Copyright (C) Parity Technologies (UK) Ltd.
Gavin Wood's avatar
Gavin Wood committed
// This file is part of Polkadot.

// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.

//! Traits and utilities to help with origin mutation and bridging.

use frame_support::{ensure, traits::Get};
use parity_scale_codec::{Decode, Encode};
use sp_std::{convert::TryInto, marker::PhantomData, prelude::*};
use xcm::prelude::*;
use xcm_executor::traits::{validate_export, ExportXcm};
use SendError::*;

/// Returns the network ID and consensus location within that network of the remote
/// location `dest` which is itself specified as a location relative to the local
/// chain, itself situated at `universal_local` within the consensus universe. If
/// `dest` is not a location in remote consensus, then an error is returned.
pub fn ensure_is_remote(
Francisco Aguirre's avatar
Francisco Aguirre committed
	universal_local: impl Into<InteriorLocation>,
	dest: impl Into<Location>,
) -> Result<(NetworkId, InteriorLocation), Location> {
Gavin Wood's avatar
Gavin Wood committed
	let dest = dest.into();
	let universal_local = universal_local.into();
	let local_net = match universal_local.global_consensus() {
		Ok(x) => x,
		Err(_) => return Err(dest),
	};
Francisco Aguirre's avatar
Francisco Aguirre committed
	let universal_destination: InteriorLocation = universal_local
Gavin Wood's avatar
Gavin Wood committed
		.into_location()
Francisco Aguirre's avatar
Francisco Aguirre committed
		.appended_with(dest.clone())
Gavin Wood's avatar
Gavin Wood committed
		.map_err(|x| x.1)?
		.try_into()?;
	let (remote_dest, remote_net) = match universal_destination.split_first() {
		(d, Some(GlobalConsensus(n))) if n != local_net => (d, n),
		_ => return Err(dest),
	};
	Ok((remote_net, remote_dest))
}

/// Implementation of `SendXcm` which uses the given `ExportXcm` implementation in order to forward
/// the message over a bridge.
///
/// No effort is made to charge for any bridge fees, so this can only be used when it is known
/// that the message sending cannot be abused in any way.
///
/// This is only useful when the local chain has bridging capabilities.
pub struct UnpaidLocalExporter<Exporter, UniversalLocation>(
	PhantomData<(Exporter, UniversalLocation)>,
);
Francisco Aguirre's avatar
Francisco Aguirre committed
impl<Exporter: ExportXcm, UniversalLocation: Get<InteriorLocation>> SendXcm
Gavin Wood's avatar
Gavin Wood committed
	for UnpaidLocalExporter<Exporter, UniversalLocation>
{
	type Ticket = Exporter::Ticket;

	fn validate(
Francisco Aguirre's avatar
Francisco Aguirre committed
		dest: &mut Option<Location>,
Gavin Wood's avatar
Gavin Wood committed
		xcm: &mut Option<Xcm<()>>,
	) -> SendResult<Exporter::Ticket> {
		let d = dest.take().ok_or(MissingArgument)?;
		let universal_source = UniversalLocation::get();
Francisco Aguirre's avatar
Francisco Aguirre committed
		let devolved = match ensure_is_remote(universal_source.clone(), d) {
Gavin Wood's avatar
Gavin Wood committed
			Ok(x) => x,
			Err(d) => {
				*dest = Some(d);
				return Err(NotApplicable)
			},
		};
		let (network, destination) = devolved;
		let xcm = xcm.take().ok_or(SendError::MissingArgument)?;
		validate_export::<Exporter>(network, 0, universal_source, destination, xcm)
	}

	fn deliver(ticket: Exporter::Ticket) -> Result<XcmHash, SendError> {
		Exporter::deliver(ticket)
	}
}

pub trait ExporterFor {
	/// Return the locally-routable bridge (if any) capable of forwarding `message` to the
	/// `remote_location` on the remote `network`, together with the payment which is required.
	///
	/// The payment is specified from the local context, not the bridge chain. This is the
	/// total amount to withdraw in to Holding and should cover both payment for the execution on
	/// the bridge chain as well as payment for the use of the `ExportMessage` instruction.
	fn exporter_for(
		network: &NetworkId,
Francisco Aguirre's avatar
Francisco Aguirre committed
		remote_location: &InteriorLocation,
Gavin Wood's avatar
Gavin Wood committed
		message: &Xcm<()>,
Francisco Aguirre's avatar
Francisco Aguirre committed
	) -> Option<(Location, Option<Asset>)>;
Gavin Wood's avatar
Gavin Wood committed
}

#[impl_trait_for_tuples::impl_for_tuples(30)]
impl ExporterFor for Tuple {
	fn exporter_for(
		network: &NetworkId,
Francisco Aguirre's avatar
Francisco Aguirre committed
		remote_location: &InteriorLocation,
Gavin Wood's avatar
Gavin Wood committed
		message: &Xcm<()>,
Francisco Aguirre's avatar
Francisco Aguirre committed
	) -> Option<(Location, Option<Asset>)> {
Gavin Wood's avatar
Gavin Wood committed
		for_tuples!( #(
			if let Some(r) = Tuple::exporter_for(network, remote_location, message) {
				return Some(r);
			}
		)* );
		None
	}
}

/// Configuration item representing a single exporter in the `NetworkExportTable`.
pub struct NetworkExportTableItem {
	/// Supported remote network.
	pub remote_network: NetworkId,
	/// Remote location filter.
	/// If `Some`, the requested remote location must be equal to one of the items in the vector.
	/// These are locations in the remote network.
	/// If `None`, then the check is skipped.
Francisco Aguirre's avatar
Francisco Aguirre committed
	pub remote_location_filter: Option<Vec<InteriorLocation>>,
	/// Locally-routable bridge with bridging capabilities to the `remote_network` and
	/// `remote_location`. See [`ExporterFor`] for more details.
Francisco Aguirre's avatar
Francisco Aguirre committed
	pub bridge: Location,
	/// The local payment.
	/// See [`ExporterFor`] for more details.
Francisco Aguirre's avatar
Francisco Aguirre committed
	pub payment: Option<Asset>,
}

impl NetworkExportTableItem {
	pub fn new(
		remote_network: NetworkId,
Francisco Aguirre's avatar
Francisco Aguirre committed
		remote_location_filter: Option<Vec<InteriorLocation>>,
		bridge: Location,
		payment: Option<Asset>,
	) -> Self {
		Self { remote_network, remote_location_filter, bridge, payment }
	}
}

/// An adapter for the implementation of `ExporterFor`, which attempts to find the
/// `(bridge_location, payment)` for the requested `network` and `remote_location` in the provided
/// `T` table containing various exporters.
Gavin Wood's avatar
Gavin Wood committed
pub struct NetworkExportTable<T>(sp_std::marker::PhantomData<T>);
impl<T: Get<Vec<NetworkExportTableItem>>> ExporterFor for NetworkExportTable<T> {
Gavin Wood's avatar
Gavin Wood committed
	fn exporter_for(
		network: &NetworkId,
Francisco Aguirre's avatar
Francisco Aguirre committed
		remote_location: &InteriorLocation,
Gavin Wood's avatar
Gavin Wood committed
		_: &Xcm<()>,
Francisco Aguirre's avatar
Francisco Aguirre committed
	) -> Option<(Location, Option<Asset>)> {
		T::get()
			.into_iter()
			.find(|item| {
				&item.remote_network == network &&
					item.remote_location_filter
						.as_ref()
						.map(|filters| filters.iter().any(|filter| filter == remote_location))
						.unwrap_or(true)
			})
			.map(|item| (item.bridge, item.payment))
Gavin Wood's avatar
Gavin Wood committed
	}
}

pub fn forward_id_for(original_id: &XcmHash) -> XcmHash {
	(b"forward_id_for", original_id).using_encoded(sp_io::hashing::blake2_256)
}

Gavin Wood's avatar
Gavin Wood committed
/// Implementation of `SendXcm` which wraps the message inside an `ExportMessage` instruction
/// and sends it to a destination known to be able to handle it.
///
/// No effort is made to make payment to the bridge for its services, so the bridge location
/// must have been configured with a barrier rule allowing unpaid execution for this message
/// coming from our origin.
///
/// This is only useful if we have special dispensation by the remote bridges to have the
/// `ExportMessage` instruction executed without payment.
///
/// The `XcmHash` value returned by `deliver` will always be the same as that returned by the
/// message exporter (`Bridges`). Generally this should take notice of the message should it
/// end with the `SetTopic` instruction.
///
/// In the case that the message ends with a `SetTopic(T)` (as should be the case if the top-level
/// router is `WithUniqueTopic`), then the forwarding message (i.e. the one carrying the
/// export instruction *to* the bridge in local consensus) will also end with a `SetTopic` whose
/// inner is `forward_id_for(T)`. If this is not the case then the onward message will not be given
/// the `SetTopic` afterword.
Gavin Wood's avatar
Gavin Wood committed
pub struct UnpaidRemoteExporter<Bridges, Router, UniversalLocation>(
	PhantomData<(Bridges, Router, UniversalLocation)>,
);
Francisco Aguirre's avatar
Francisco Aguirre committed
impl<Bridges: ExporterFor, Router: SendXcm, UniversalLocation: Get<InteriorLocation>> SendXcm
Gavin Wood's avatar
Gavin Wood committed
	for UnpaidRemoteExporter<Bridges, Router, UniversalLocation>
{
	type Ticket = Router::Ticket;

	fn validate(
Francisco Aguirre's avatar
Francisco Aguirre committed
		dest: &mut Option<Location>,
Gavin Wood's avatar
Gavin Wood committed
	) -> SendResult<Router::Ticket> {
Francisco Aguirre's avatar
Francisco Aguirre committed
		let d = dest.clone().ok_or(MissingArgument)?;
Gavin Wood's avatar
Gavin Wood committed
		let devolved = ensure_is_remote(UniversalLocation::get(), d).map_err(|_| NotApplicable)?;
		let (remote_network, remote_location) = devolved;
		let xcm = msg.take().ok_or(MissingArgument)?;
		let Some((bridge, maybe_payment)) =
			Bridges::exporter_for(&remote_network, &remote_location, &xcm)
		else {
			// We need to make sure that msg is not consumed in case of `NotApplicable`.
			*msg = Some(xcm);
			return Err(SendError::NotApplicable)
		};
Gavin Wood's avatar
Gavin Wood committed
		ensure!(maybe_payment.is_none(), Unroutable);

		// `xcm` should already end with `SetTopic` - if it does, then extract and derive into
		// an onward topic ID.
		let maybe_forward_id = match xcm.last() {
			Some(SetTopic(t)) => Some(forward_id_for(t)),
			_ => None,
		};

Gavin Wood's avatar
Gavin Wood committed
		// We then send a normal message to the bridge asking it to export the prepended
		// message to the remote chain. This will only work if the bridge will do the message
		// export for free. Common-good chains will typically be afforded this.
		let mut message = Xcm(vec![
			UnpaidExecution { weight_limit: Unlimited, check_origin: None },
			ExportMessage { network: remote_network, destination: remote_location, xcm },
		]);
		if let Some(forward_id) = maybe_forward_id {
			message.0.push(SetTopic(forward_id));
		}
		validate_send::<Router>(bridge, message)
Gavin Wood's avatar
Gavin Wood committed
	}

	fn deliver(validation: Self::Ticket) -> Result<XcmHash, SendError> {
Gavin Wood's avatar
Gavin Wood committed
		Router::deliver(validation)
	}
}

/// Implementation of `SendXcm` which wraps the message inside an `ExportMessage` instruction
/// and sends it to a destination known to be able to handle it.
///
/// The `ExportMessage` instruction on the bridge is paid for from the local chain's sovereign
/// account on the bridge. The amount paid is determined through the `ExporterFor` trait.
///
/// The `XcmHash` value returned by `deliver` will always be the same as that returned by the
/// message exporter (`Bridges`). Generally this should take notice of the message should it
/// end with the `SetTopic` instruction.
///
/// In the case that the message ends with a `SetTopic(T)` (as should be the case if the top-level
/// router is `WithUniqueTopic`), then the forwarding message (i.e. the one carrying the
/// export instruction *to* the bridge in local consensus) will also end with a `SetTopic` whose
/// inner is `forward_id_for(T)`. If this is not the case then the onward message will not be given
/// the `SetTopic` afterword.
Gavin Wood's avatar
Gavin Wood committed
pub struct SovereignPaidRemoteExporter<Bridges, Router, UniversalLocation>(
	PhantomData<(Bridges, Router, UniversalLocation)>,
);
Francisco Aguirre's avatar
Francisco Aguirre committed
impl<Bridges: ExporterFor, Router: SendXcm, UniversalLocation: Get<InteriorLocation>> SendXcm
Gavin Wood's avatar
Gavin Wood committed
	for SovereignPaidRemoteExporter<Bridges, Router, UniversalLocation>
{
	type Ticket = Router::Ticket;

	fn validate(
Francisco Aguirre's avatar
Francisco Aguirre committed
		dest: &mut Option<Location>,
Gavin Wood's avatar
Gavin Wood committed
	) -> SendResult<Router::Ticket> {
Francisco Aguirre's avatar
Francisco Aguirre committed
		let d = dest.as_ref().ok_or(MissingArgument)?;
		let devolved =
			ensure_is_remote(UniversalLocation::get(), d.clone()).map_err(|_| NotApplicable)?;
Gavin Wood's avatar
Gavin Wood committed
		let (remote_network, remote_location) = devolved;
		let xcm = msg.take().ok_or(MissingArgument)?;
Gavin Wood's avatar
Gavin Wood committed

		let Some((bridge, maybe_payment)) =
			Bridges::exporter_for(&remote_network, &remote_location, &xcm)
		else {
			// We need to make sure that msg is not consumed in case of `NotApplicable`.
			*msg = Some(xcm);
			return Err(SendError::NotApplicable)
		};

		// `xcm` should already end with `SetTopic` - if it does, then extract and derive into
		// an onward topic ID.
		let maybe_forward_id = match xcm.last() {
			Some(SetTopic(t)) => Some(forward_id_for(t)),
			_ => None,
		};

Gavin Wood's avatar
Gavin Wood committed
		let local_from_bridge =
			UniversalLocation::get().invert_target(&bridge).map_err(|_| Unroutable)?;
		let export_instruction =
			ExportMessage { network: remote_network, destination: remote_location, xcm };

		let mut message = Xcm(if let Some(ref payment) = maybe_payment {
Gavin Wood's avatar
Gavin Wood committed
			let fees = payment
				.clone()
Francisco Aguirre's avatar
Francisco Aguirre committed
				.reanchored(&bridge, &UniversalLocation::get())
Gavin Wood's avatar
Gavin Wood committed
				.map_err(|_| Unroutable)?;
			vec![
				WithdrawAsset(fees.clone().into()),
				BuyExecution { fees, weight_limit: Unlimited },
				// `SetAppendix` ensures that `fees` are not trapped in any case, for example, when
				// `ExportXcm::validate` encounters an error during the processing of
				// `ExportMessage`.
				SetAppendix(Xcm(vec![DepositAsset {
					assets: AllCounted(1).into(),
					beneficiary: local_from_bridge,
				}])),
Gavin Wood's avatar
Gavin Wood committed
				export_instruction,
			]
		} else {
			vec![export_instruction]
		});
		if let Some(forward_id) = maybe_forward_id {
			message.0.push(SetTopic(forward_id));
		}
Gavin Wood's avatar
Gavin Wood committed

		// We then send a normal message to the bridge asking it to export the prepended
		// message to the remote chain.
		let (v, mut cost) = validate_send::<Router>(bridge, message)?;
		if let Some(bridge_payment) = maybe_payment {
			cost.push(bridge_payment);
		}
		Ok((v, cost))
	}

	fn deliver(ticket: Router::Ticket) -> Result<XcmHash, SendError> {
		Router::deliver(ticket)
	}
}

pub trait DispatchBlob {
Gavin Wood's avatar
Gavin Wood committed
	/// Takes an incoming blob from over some point-to-point link (usually from some sort of
	/// inter-consensus bridge) and then does what needs to be done with it. Usually this means
	/// forwarding it on into some other location sharing our consensus or possibly just enqueuing
	/// it for execution locally if it is destined for the local chain.
	///
	/// NOTE: The API does not provide for any kind of weight or fee management; the size of the
	/// `blob` is known to the caller and so the operation must have a linear weight relative to
	/// `blob`'s length. This means that you will generally only want to **enqueue** the blob, not
	/// enact it. Fees must be handled by the caller.
Gavin Wood's avatar
Gavin Wood committed
	fn dispatch_blob(blob: Vec<u8>) -> Result<(), DispatchBlobError>;
}

pub trait HaulBlob {
	/// Sends a blob over some point-to-point link. This will generally be implemented by a bridge.
	fn haul_blob(blob: Vec<u8>) -> Result<(), HaulBlobError>;
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HaulBlobError {
	/// Represents point-to-point link failure with a human-readable explanation of the specific
	/// issue is provided.
Gavin Wood's avatar
Gavin Wood committed
	Transport(&'static str),
}

impl From<HaulBlobError> for SendError {
	fn from(err: HaulBlobError) -> Self {
		match err {
			HaulBlobError::Transport(reason) => SendError::Transport(reason),
		}
	}
}

#[derive(Clone, Encode, Decode)]
pub struct BridgeMessage {
	/// The message destination as a *Universal Location*. This means it begins with a
	/// `GlobalConsensus` junction describing the network under which global consensus happens.
	/// If this does not match our global consensus then it's a fatal error.
Francisco Aguirre's avatar
Francisco Aguirre committed
	pub universal_dest: VersionedInteriorLocation,
Gavin Wood's avatar
Gavin Wood committed
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DispatchBlobError {
	Unbridgable,
	InvalidEncoding,
	UnsupportedLocationVersion,
	UnsupportedXcmVersion,
	RoutingError,
	NonUniversalDestination,
	WrongGlobal,
}

pub struct BridgeBlobDispatcher<Router, OurPlace, OurPlaceBridgeInstance>(
	PhantomData<(Router, OurPlace, OurPlaceBridgeInstance)>,
);
impl<
		Router: SendXcm,
Francisco Aguirre's avatar
Francisco Aguirre committed
		OurPlace: Get<InteriorLocation>,
		OurPlaceBridgeInstance: Get<Option<InteriorLocation>>,
	> DispatchBlob for BridgeBlobDispatcher<Router, OurPlace, OurPlaceBridgeInstance>
Gavin Wood's avatar
Gavin Wood committed
{
	fn dispatch_blob(blob: Vec<u8>) -> Result<(), DispatchBlobError> {
		let our_universal = OurPlace::get();
		let our_global =
			our_universal.global_consensus().map_err(|()| DispatchBlobError::Unbridgable)?;
		let BridgeMessage { universal_dest, message } =
			Decode::decode(&mut &blob[..]).map_err(|_| DispatchBlobError::InvalidEncoding)?;
Francisco Aguirre's avatar
Francisco Aguirre committed
		let universal_dest: InteriorLocation = universal_dest
Gavin Wood's avatar
Gavin Wood committed
			.try_into()
			.map_err(|_| DispatchBlobError::UnsupportedLocationVersion)?;
		// `universal_dest` is the desired destination within the universe: first we need to check
		// we're in the right global consensus.
		let intended_global = universal_dest
			.global_consensus()
			.map_err(|()| DispatchBlobError::NonUniversalDestination)?;
		ensure!(intended_global == our_global, DispatchBlobError::WrongGlobal);
		let dest = universal_dest.relative_to(&our_universal);
Gavin Wood's avatar
Gavin Wood committed
			message.try_into().map_err(|_| DispatchBlobError::UnsupportedXcmVersion)?;

		// Prepend our bridge instance discriminator.
		// Can be used for fine-grained control of origin on destination in case of multiple bridge
		// instances, e.g. restrict `type UniversalAliases` and `UniversalOrigin` instruction to
		// trust just particular bridge instance for `NetworkId`.
		if let Some(bridge_instance) = OurPlaceBridgeInstance::get() {
			message.0.insert(0, DescendOrigin(bridge_instance));
		}

Gavin Wood's avatar
Gavin Wood committed
		let _ = send_xcm::<Router>(dest, message).map_err(|_| DispatchBlobError::RoutingError)?;
		Ok(())
	}
}

pub struct HaulBlobExporter<Bridge, BridgedNetwork, DestinationVersion, Price>(
	PhantomData<(Bridge, BridgedNetwork, DestinationVersion, Price)>,
Gavin Wood's avatar
Gavin Wood committed
);
/// `ExportXcm` implementation for `HaulBlobExporter`.
///
/// # Type Parameters
///
/// ```text
/// - Bridge: Implements `HaulBlob`.
/// - BridgedNetwork: The relative location of the bridged consensus system with the expected `GlobalConsensus` junction.
/// - DestinationVersion: Implements `GetVersion` for retrieving XCM version for the destination.
/// - Price: potential fees for exporting.
/// ```
impl<
		Bridge: HaulBlob,
Francisco Aguirre's avatar
Francisco Aguirre committed
		BridgedNetwork: Get<Location>,
		DestinationVersion: GetVersion,
Francisco Aguirre's avatar
Francisco Aguirre committed
		Price: Get<Assets>,
	> ExportXcm for HaulBlobExporter<Bridge, BridgedNetwork, DestinationVersion, Price>
Gavin Wood's avatar
Gavin Wood committed
{
	type Ticket = (Vec<u8>, XcmHash);

	fn validate(
		network: NetworkId,
		_channel: u32,
Francisco Aguirre's avatar
Francisco Aguirre committed
		universal_source: &mut Option<InteriorLocation>,
		destination: &mut Option<InteriorLocation>,
Gavin Wood's avatar
Gavin Wood committed
		message: &mut Option<Xcm<()>>,
Francisco Aguirre's avatar
Francisco Aguirre committed
	) -> Result<((Vec<u8>, XcmHash), Assets), SendError> {
		let (bridged_network, bridged_network_location_parents) = {
Francisco Aguirre's avatar
Francisco Aguirre committed
			let Location { parents, interior: mut junctions } = BridgedNetwork::get();
			match junctions.take_first() {
				Some(GlobalConsensus(network)) => (network, parents),
				_ => return Err(SendError::NotApplicable),
			}
		};
Gavin Wood's avatar
Gavin Wood committed
		ensure!(&network == &bridged_network, SendError::NotApplicable);
		// We don't/can't use the `channel` for this adapter.
		let dest = destination.take().ok_or(SendError::MissingArgument)?;

		// Let's resolve the known/supported XCM version for the destination because we don't know
		// if it supports the same/latest version.
		let (universal_dest, version) =
			match dest.pushed_front_with(GlobalConsensus(bridged_network)) {
				Ok(d) => {
Francisco Aguirre's avatar
Francisco Aguirre committed
					let version = DestinationVersion::get_version_for(&Location::from(
						AncestorThen(bridged_network_location_parents, d.clone()),
					))
					.ok_or(SendError::DestinationUnsupported)?;
					(d, version)
				},
				Err((dest, _)) => {
					*destination = Some(dest);
					return Err(SendError::NotApplicable)
				},
			};

		// Let's adjust XCM with `UniversalOrigin`, `DescendOrigin` and`SetTopic`.
Gavin Wood's avatar
Gavin Wood committed
		let (local_net, local_sub) = universal_source
			.take()
			.ok_or(SendError::MissingArgument)?
			.split_global()
			.map_err(|()| SendError::Unroutable)?;
		let mut message = message.take().ok_or(SendError::MissingArgument)?;
		let maybe_id = match message.last() {
			Some(SetTopic(t)) => Some(*t),
			_ => None,
		};
		message.0.insert(0, UniversalOrigin(GlobalConsensus(local_net)));
Gavin Wood's avatar
Gavin Wood committed
		if local_sub != Here {
			message.0.insert(1, DescendOrigin(local_sub));
Gavin Wood's avatar
Gavin Wood committed
		}

		// We cannot use the latest `Versioned` because we don't know if the target chain already
		// supports the same version. Therefore, we better control the destination version with best
		// efforts.
		let message = VersionedXcm::from(message)
			.into_version(version)
			.map_err(|()| SendError::DestinationUnsupported)?;
Francisco Aguirre's avatar
Francisco Aguirre committed
		let universal_dest = VersionedInteriorLocation::from(universal_dest)
			.into_version(version)
			.map_err(|()| SendError::DestinationUnsupported)?;

		let id = maybe_id.unwrap_or_else(|| message.using_encoded(sp_io::hashing::blake2_256));
Gavin Wood's avatar
Gavin Wood committed
		let blob = BridgeMessage { universal_dest, message }.encode();
		Ok(((blob, id), Price::get()))
Gavin Wood's avatar
Gavin Wood committed
	}

	fn deliver((blob, id): (Vec<u8>, XcmHash)) -> Result<XcmHash, SendError> {
Gavin Wood's avatar
Gavin Wood committed
		Bridge::haul_blob(blob)?;
Gavin Wood's avatar
Gavin Wood committed
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn ensure_is_remote_works() {
		// A Kusama parachain is remote from the Polkadot Relay.
		let x = ensure_is_remote(Polkadot, (Parent, Kusama, Parachain(1000)));
		assert_eq!(x, Ok((Kusama, Parachain(1000).into())));

		// Polkadot Relay is remote from a Kusama parachain.
		let x = ensure_is_remote((Kusama, Parachain(1000)), (Parent, Parent, Polkadot));
		assert_eq!(x, Ok((Polkadot, Here)));

		// Our own parachain is local.
		let x = ensure_is_remote(Polkadot, Parachain(1000));
		assert_eq!(x, Err(Parachain(1000).into()));

		// Polkadot's parachain is not remote if we are Polkadot.
		let x = ensure_is_remote(Polkadot, (Parent, Polkadot, Parachain(1000)));
		assert_eq!(x, Err((Parent, Polkadot, Parachain(1000)).into()));

		// If we don't have a consensus ancestor, then we cannot determine remoteness.
		let x = ensure_is_remote((), (Parent, Polkadot, Parachain(1000)));
		assert_eq!(x, Err((Parent, Polkadot, Parachain(1000)).into()));
	}

	pub struct OkSender;
	impl SendXcm for OkSender {
		type Ticket = ();

		fn validate(
Francisco Aguirre's avatar
Francisco Aguirre committed
			_destination: &mut Option<Location>,
			_message: &mut Option<Xcm<()>>,
		) -> SendResult<Self::Ticket> {
Francisco Aguirre's avatar
Francisco Aguirre committed
			Ok(((), Assets::new()))
		}

		fn deliver(_ticket: Self::Ticket) -> Result<XcmHash, SendError> {
			Ok([0; 32])
		}
	}

	/// Generic test case asserting that dest and msg is not consumed by `validate` implementation
	/// of `SendXcm` in case of expected result.
	fn ensure_validate_does_not_consume_dest_or_msg<S: SendXcm>(
Francisco Aguirre's avatar
Francisco Aguirre committed
		dest: Location,
		assert_result: impl Fn(SendResult<S::Ticket>),
	) {
Francisco Aguirre's avatar
Francisco Aguirre committed
		let mut dest_wrapper = Some(dest.clone());
		let msg = Xcm::<()>::new();
		let mut msg_wrapper = Some(msg.clone());

		assert_result(S::validate(&mut dest_wrapper, &mut msg_wrapper));

		// ensure dest and msg are untouched
		assert_eq!(Some(dest), dest_wrapper);
		assert_eq!(Some(msg), msg_wrapper);
	}

	#[test]
	fn remote_exporters_does_not_consume_dest_or_msg_on_not_applicable() {
		frame_support::parameter_types! {
			pub Local: NetworkId = ByGenesis([0; 32]);
Francisco Aguirre's avatar
Francisco Aguirre committed
			pub UniversalLocation: InteriorLocation = [GlobalConsensus(Local::get()), Parachain(1234)].into();
			pub DifferentRemote: NetworkId = ByGenesis([22; 32]);
			// no routers
			pub BridgeTable: Vec<NetworkExportTableItem> = vec![];
Francisco Aguirre's avatar
Francisco Aguirre committed
		let local_dest: Location = (Parent, Parachain(5678)).into();
		assert!(ensure_is_remote(UniversalLocation::get(), local_dest.clone()).is_err());

		ensure_validate_does_not_consume_dest_or_msg::<
			UnpaidRemoteExporter<NetworkExportTable<BridgeTable>, OkSender, UniversalLocation>,
Francisco Aguirre's avatar
Francisco Aguirre committed
		>(local_dest.clone(), |result| assert_eq!(Err(NotApplicable), result));

		ensure_validate_does_not_consume_dest_or_msg::<
			SovereignPaidRemoteExporter<
				NetworkExportTable<BridgeTable>,
				OkSender,
				UniversalLocation,
			>,
		>(local_dest, |result| assert_eq!(Err(NotApplicable), result));

		// check with not applicable destination
Francisco Aguirre's avatar
Francisco Aguirre committed
		let remote_dest: Location = (Parent, Parent, DifferentRemote::get()).into();
		assert!(ensure_is_remote(UniversalLocation::get(), remote_dest.clone()).is_ok());

		ensure_validate_does_not_consume_dest_or_msg::<
			UnpaidRemoteExporter<NetworkExportTable<BridgeTable>, OkSender, UniversalLocation>,
Francisco Aguirre's avatar
Francisco Aguirre committed
		>(remote_dest.clone(), |result| assert_eq!(Err(NotApplicable), result));

		ensure_validate_does_not_consume_dest_or_msg::<
			SovereignPaidRemoteExporter<
				NetworkExportTable<BridgeTable>,
				OkSender,
				UniversalLocation,
			>,
		>(remote_dest, |result| assert_eq!(Err(NotApplicable), result));
	}

	#[test]
	fn network_export_table_works() {
		frame_support::parameter_types! {
			pub NetworkA: NetworkId = ByGenesis([0; 32]);
Francisco Aguirre's avatar
Francisco Aguirre committed
			pub Parachain1000InNetworkA: InteriorLocation = [Parachain(1000)].into();
			pub Parachain2000InNetworkA: InteriorLocation = [Parachain(2000)].into();
Francisco Aguirre's avatar
Francisco Aguirre committed
			pub BridgeToALocation: Location = Location::new(1, [Parachain(1234)]);
			pub BridgeToBLocation: Location = Location::new(1, [Parachain(4321)]);
Francisco Aguirre's avatar
Francisco Aguirre committed
			pub PaymentForNetworkAAndParachain2000: Asset = (Location::parent(), 150).into();

			pub BridgeTable: sp_std::vec::Vec<NetworkExportTableItem> = sp_std::vec![
				// NetworkA allows `Parachain(1000)` as remote location WITHOUT payment.
				NetworkExportTableItem::new(
					NetworkA::get(),
					Some(vec![Parachain1000InNetworkA::get()]),
					BridgeToALocation::get(),
					None
				),
				// NetworkA allows `Parachain(2000)` as remote location WITH payment.
				NetworkExportTableItem::new(
					NetworkA::get(),
					Some(vec![Parachain2000InNetworkA::get()]),
					BridgeToALocation::get(),
					Some(PaymentForNetworkAAndParachain2000::get())
				),
				// NetworkB allows all remote location.
				NetworkExportTableItem::new(
					NetworkB::get(),
					None,
					BridgeToBLocation::get(),
					None
				)
			];
		}

Francisco Aguirre's avatar
Francisco Aguirre committed
		let test_data: Vec<(NetworkId, InteriorLocation, Option<(Location, Option<Asset>)>)> = vec![
			(NetworkA::get(), [Parachain(1000)].into(), Some((BridgeToALocation::get(), None))),
			(NetworkA::get(), [Parachain(1000), GeneralIndex(1)].into(), None),
Francisco Aguirre's avatar
Francisco Aguirre committed
				[Parachain(2000)].into(),
				Some((BridgeToALocation::get(), Some(PaymentForNetworkAAndParachain2000::get()))),
			),
Francisco Aguirre's avatar
Francisco Aguirre committed
			(NetworkA::get(), [Parachain(2000), GeneralIndex(1)].into(), None),
			(NetworkA::get(), [Parachain(3000)].into(), None),
			(NetworkB::get(), [Parachain(1000)].into(), Some((BridgeToBLocation::get(), None))),
			(NetworkB::get(), [Parachain(2000)].into(), Some((BridgeToBLocation::get(), None))),
			(NetworkB::get(), [Parachain(3000)].into(), Some((BridgeToBLocation::get(), None))),
		];

		for (network, remote_location, expected_result) in test_data {
			assert_eq!(
				NetworkExportTable::<BridgeTable>::exporter_for(
					&network,
					&remote_location,
					&Xcm::default()
				),
				expected_result,
				"expected_result: {:?} not matched for network: {:?} and remote_location: {:?}",
				expected_result,
				network,
				remote_location,
			)
		}
	}
Gavin Wood's avatar
Gavin Wood committed
}