diff --git a/Cargo.toml b/Cargo.toml
index 70e66323126bb..ce77b74a7632a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -676,8 +676,10 @@ bp-rococo = { path = "bridges/chains/chain-rococo", default-features = false }
bp-runtime = { path = "bridges/primitives/runtime", default-features = false }
bp-test-utils = { path = "bridges/primitives/test-utils", default-features = false }
bp-westend = { path = "bridges/chains/chain-westend", default-features = false }
+bp-xcm-bridge = { path = "bridges/primitives/xcm-bridge", default-features = false }
bp-xcm-bridge-hub = { path = "bridges/primitives/xcm-bridge-hub", default-features = false }
bp-xcm-bridge-hub-router = { path = "bridges/primitives/xcm-bridge-hub-router", default-features = false }
+bp-xcm-bridge-router = { path = "bridges/primitives/xcm-bridge-router", default-features = false }
bridge-hub-common = { path = "cumulus/parachains/runtimes/bridge-hubs/common", default-features = false }
bridge-hub-rococo-emulated-chain = { path = "cumulus/parachains/integration-tests/emulated/chains/parachains/bridges/bridge-hub-rococo" }
bridge-hub-rococo-runtime = { path = "cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo", default-features = false }
@@ -1273,6 +1275,7 @@ snowbridge-pallet-outbound-queue-v2 = { path = "bridges/snowbridge/pallets/outbo
snowbridge-pallet-system = { path = "bridges/snowbridge/pallets/system", default-features = false }
snowbridge-pallet-system-frontend = { path = "bridges/snowbridge/pallets/system-frontend", default-features = false }
snowbridge-pallet-system-v2 = { path = "bridges/snowbridge/pallets/system-v2", default-features = false }
+snowbridge-router-primitives = { path = "bridges/snowbridge/primitives/router", default-features = false }
snowbridge-runtime-common = { path = "bridges/snowbridge/runtime/runtime-common", default-features = false }
snowbridge-runtime-test-common = { path = "bridges/snowbridge/runtime/test-common", default-features = false }
snowbridge-system-runtime-api = { path = "bridges/snowbridge/pallets/system/runtime-api", default-features = false }
diff --git a/bridges/chains/chain-asset-hub-rococo/Cargo.toml b/bridges/chains/chain-asset-hub-rococo/Cargo.toml
index 16425544076e1..7289854066538 100644
--- a/bridges/chains/chain-asset-hub-rococo/Cargo.toml
+++ b/bridges/chains/chain-asset-hub-rococo/Cargo.toml
@@ -19,11 +19,17 @@ scale-info = { features = ["derive"], workspace = true }
# Substrate Dependencies
frame-support = { workspace = true }
+sp-api = { workspace = true }
sp-core = { workspace = true }
+sp-runtime = { workspace = true }
+sp-std = { workspace = true }
# Bridge Dependencies
-bp-xcm-bridge-hub-router = { workspace = true }
-testnet-parachains-constants = { features = ["rococo"], workspace = true }
+bp-bridge-hub-cumulus = { workspace = true }
+bp-messages = { workspace = true }
+bp-runtime = { workspace = true }
+bp-xcm-bridge = { workspace = true }
+bp-xcm-bridge-router = { workspace = true }
# Polkadot dependencies
xcm = { workspace = true }
@@ -31,11 +37,19 @@ xcm = { workspace = true }
[features]
default = ["std"]
std = [
- "bp-xcm-bridge-hub-router/std",
+ "bp-bridge-hub-cumulus/std",
+ "bp-messages/std",
+ "bp-runtime/std",
+ "bp-xcm-bridge-router/std",
+ "bp-xcm-bridge/std",
"codec/std",
+ "codec/std",
+ "frame-support/std",
"frame-support/std",
"scale-info/std",
+ "sp-api/std",
"sp-core/std",
- "testnet-parachains-constants/std",
+ "sp-runtime/std",
+ "sp-std/std",
"xcm/std",
]
diff --git a/bridges/chains/chain-asset-hub-rococo/src/lib.rs b/bridges/chains/chain-asset-hub-rococo/src/lib.rs
index f4fb78b4e1d63..c60a79f27c01a 100644
--- a/bridges/chains/chain-asset-hub-rococo/src/lib.rs
+++ b/bridges/chains/chain-asset-hub-rococo/src/lib.rs
@@ -14,7 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see .
-//! Module with configuration which reflects AssetHubRococo runtime setup.
+//! Module with configuration which reflects AssetHubRococo runtime setup (AccountId, Headers,
+//! Hashes...)
#![cfg_attr(not(feature = "std"), no_std)]
@@ -23,8 +24,16 @@ extern crate alloc;
use codec::{Decode, Encode};
use scale_info::TypeInfo;
-pub use bp_xcm_bridge_hub_router::XcmBridgeHubRouterCall;
-use testnet_parachains_constants::rococo::currency::UNITS;
+pub use bp_bridge_hub_cumulus::*;
+use bp_messages::*;
+use bp_runtime::{
+ decl_bridge_finality_runtime_apis, decl_bridge_messages_runtime_apis, Chain, ChainId, Parachain,
+};
+pub use bp_xcm_bridge_router::XcmBridgeHubCall;
+use frame_support::{
+ dispatch::DispatchClass,
+ sp_runtime::{MultiAddress, MultiSigner, RuntimeDebug, StateVersion},
+};
use xcm::latest::prelude::*;
/// `AssetHubRococo` Runtime `Call` enum.
@@ -40,14 +49,12 @@ use xcm::latest::prelude::*;
pub enum Call {
/// `ToWestendXcmRouter` bridge pallet.
#[codec(index = 45)]
- ToWestendXcmRouter(XcmBridgeHubRouterCall),
+ ToWestendXcmRouter(XcmBridgeHubCall),
}
frame_support::parameter_types! {
/// Some sane weight to execute `xcm::Transact(pallet-xcm-bridge-hub-router::Call::report_bridge_status)`.
pub const XcmBridgeHubRouterTransactCallMaxWeight: frame_support::weights::Weight = frame_support::weights::Weight::from_parts(200_000_000, 6144);
- /// Should match the `AssetDeposit` of the `ForeignAssets` pallet on Asset Hub.
- pub const CreateForeignAssetDeposit: u128 = UNITS / 10;
}
/// Builds an (un)congestion XCM program with the `report_bridge_status` call for
@@ -61,7 +68,7 @@ pub fn build_congestion_message(
Transact {
origin_kind: OriginKind::Xcm,
fallback_max_weight: Some(XcmBridgeHubRouterTransactCallMaxWeight::get()),
- call: Call::ToWestendXcmRouter(XcmBridgeHubRouterCall::report_bridge_status {
+ call: Call::ToWestendXcmRouter(XcmBridgeHubCall::update_bridge_status {
bridge_id,
is_congested,
})
@@ -74,3 +81,93 @@ pub fn build_congestion_message(
/// Identifier of AssetHubRococo in the Rococo relay chain.
pub const ASSET_HUB_ROCOCO_PARACHAIN_ID: u32 = 1000;
+
+/// AssetHubRococo parachain.
+#[derive(RuntimeDebug)]
+pub struct AssetHubRococo;
+
+impl Chain for AssetHubRococo {
+ const ID: ChainId = *b"ahro";
+
+ type BlockNumber = BlockNumber;
+ type Hash = Hash;
+ type Hasher = Hasher;
+ type Header = Header;
+
+ type AccountId = AccountId;
+ type Balance = Balance;
+ type Nonce = Nonce;
+ type Signature = Signature;
+
+ const STATE_VERSION: StateVersion = StateVersion::V1;
+
+ fn max_extrinsic_size() -> u32 {
+ *BlockLength::get().max.get(DispatchClass::Normal)
+ }
+
+ fn max_extrinsic_weight() -> Weight {
+ BlockWeightsForAsyncBacking::get()
+ .get(DispatchClass::Normal)
+ .max_extrinsic
+ .unwrap_or(Weight::MAX)
+ }
+}
+
+impl Parachain for AssetHubRococo {
+ const PARACHAIN_ID: u32 = ASSET_HUB_ROCOCO_PARACHAIN_ID;
+ const MAX_HEADER_SIZE: u32 = MAX_BRIDGE_HUB_HEADER_SIZE;
+}
+
+/// Describing permissionless lanes instance
+impl ChainWithMessages for AssetHubRococo {
+ const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str =
+ WITH_ASSET_HUB_ROCOCO_MESSAGES_PALLET_NAME;
+
+ const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce =
+ MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX;
+ const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce =
+ MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
+}
+
+/// Public key of the chain account that may be used to verify signatures.
+pub type AccountSigner = MultiSigner;
+
+/// The address format for describing accounts.
+pub type Address = MultiAddress;
+
+/// Name of the With-AssetHubRococo messages pallet instance that is deployed at bridged chains.
+pub const WITH_ASSET_HUB_ROCOCO_MESSAGES_PALLET_NAME: &str = "BridgeRococoMessages";
+
+/// Name of the With-AssetHubRococo bridge-relayers pallet instance that is deployed at bridged
+/// chains.
+pub const WITH_ASSET_HUB_ROCOCO_RELAYERS_PALLET_NAME: &str = "BridgeRelayers";
+
+/// Pallet index of `BridgeWestendMessages: pallet_bridge_messages::`.
+pub const WITH_BRIDGE_ROCOCO_TO_WESTEND_MESSAGES_PALLET_INDEX: u8 = 62;
+
+decl_bridge_finality_runtime_apis!(asset_hub_rococo);
+decl_bridge_messages_runtime_apis!(asset_hub_rococo, HashedLaneId);
+
+frame_support::parameter_types! {
+ /// TODO: FAIL-CI - probably not needed or ise for MessageExporterPrice
+ /// The XCM fee that is paid for executing XCM program (with `ExportMessage` instruction) at the Rococo
+ /// BridgeHub.
+ /// (initially was calculated by test `AssetHubRococo::can_calculate_weight_for_paid_export_message_with_reserve_transfer` + `33%`)
+ pub const AssetHubRococoBaseXcmFeeInRocs: u128 = 57_325_000;
+
+ /// Transaction fee that is paid at the Rococo BridgeHub for delivering single inbound message.
+ /// (initially was calculated by test `AssetHubRococo::can_calculate_fee_for_standalone_message_delivery_transaction` + `33%`)
+ pub const AssetHubRococoBaseDeliveryFeeInRocs: u128 = 297_685_840;
+
+ /// Transaction fee that is paid at the Rococo BridgeHub for delivering single outbound message confirmation.
+ /// (initially was calculated by test `AssetHubRococo::can_calculate_fee_for_standalone_message_confirmation_transaction` + `33%`)
+ pub const AssetHubRococoBaseConfirmationFeeInRocs: u128 = 56_782_099;
+}
+
+/// Wrapper over `AssetHubRococo`'s `RuntimeCall` that can be used without a runtime.
+#[derive(Decode, Encode)]
+pub enum RuntimeCall {
+ /// Points to the `pallet_xcm_bridge` pallet instance for `AssetHubWestend`.
+ #[codec(index = 61)]
+ XcmOverAssetHubWestend(bp_xcm_bridge::XcmBridgeCall),
+}
diff --git a/bridges/chains/chain-asset-hub-westend/Cargo.toml b/bridges/chains/chain-asset-hub-westend/Cargo.toml
index 3ad2cf9359bfd..2f695ecdb336c 100644
--- a/bridges/chains/chain-asset-hub-westend/Cargo.toml
+++ b/bridges/chains/chain-asset-hub-westend/Cargo.toml
@@ -19,11 +19,17 @@ scale-info = { features = ["derive"], workspace = true }
# Substrate Dependencies
frame-support = { workspace = true }
+sp-api = { workspace = true }
sp-core = { workspace = true }
+sp-runtime = { workspace = true }
+sp-std = { workspace = true }
# Bridge Dependencies
-bp-xcm-bridge-hub-router = { workspace = true }
-testnet-parachains-constants = { features = ["westend"], workspace = true }
+bp-bridge-hub-cumulus = { workspace = true }
+bp-messages = { workspace = true }
+bp-runtime = { workspace = true }
+bp-xcm-bridge = { workspace = true }
+bp-xcm-bridge-router = { workspace = true }
# Polkadot dependencies
xcm = { workspace = true }
@@ -31,11 +37,19 @@ xcm = { workspace = true }
[features]
default = ["std"]
std = [
- "bp-xcm-bridge-hub-router/std",
+ "bp-bridge-hub-cumulus/std",
+ "bp-messages/std",
+ "bp-runtime/std",
+ "bp-xcm-bridge-router/std",
+ "bp-xcm-bridge/std",
"codec/std",
+ "codec/std",
+ "frame-support/std",
"frame-support/std",
"scale-info/std",
+ "sp-api/std",
"sp-core/std",
- "testnet-parachains-constants/std",
+ "sp-runtime/std",
+ "sp-std/std",
"xcm/std",
]
diff --git a/bridges/chains/chain-asset-hub-westend/src/lib.rs b/bridges/chains/chain-asset-hub-westend/src/lib.rs
index 89b5cddf2d532..a146a1887d25c 100644
--- a/bridges/chains/chain-asset-hub-westend/src/lib.rs
+++ b/bridges/chains/chain-asset-hub-westend/src/lib.rs
@@ -14,17 +14,27 @@
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see .
-//! Module with configuration which reflects AssetHubWestend runtime setup.
+//! Module with configuration which reflects AssetHubWestend runtime setup (AccountId, Headers,
+//! Hashes...)
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
+pub use bp_bridge_hub_cumulus::*;
+use bp_messages::*;
+use bp_runtime::{
+ decl_bridge_finality_runtime_apis, decl_bridge_messages_runtime_apis, Chain, ChainId, Parachain,
+};
+use frame_support::{
+ dispatch::DispatchClass,
+ sp_runtime::{MultiAddress, MultiSigner, RuntimeDebug, StateVersion},
+};
+
use codec::{Decode, Encode};
use scale_info::TypeInfo;
-pub use bp_xcm_bridge_hub_router::XcmBridgeHubRouterCall;
-use testnet_parachains_constants::westend::currency::UNITS;
+pub use bp_xcm_bridge_router::XcmBridgeHubCall;
use xcm::latest::prelude::*;
/// `AssetHubWestend` Runtime `Call` enum.
@@ -40,15 +50,12 @@ use xcm::latest::prelude::*;
pub enum Call {
/// `ToRococoXcmRouter` bridge pallet.
#[codec(index = 34)]
- ToRococoXcmRouter(XcmBridgeHubRouterCall),
+ ToRococoXcmRouter(XcmBridgeHubCall),
}
frame_support::parameter_types! {
/// Some sane weight to execute `xcm::Transact(pallet-xcm-bridge-hub-router::Call::report_bridge_status)`.
pub const XcmBridgeHubRouterTransactCallMaxWeight: frame_support::weights::Weight = frame_support::weights::Weight::from_parts(200_000_000, 6144);
-
- /// Should match the `AssetDeposit` of the `ForeignAssets` pallet on Asset Hub.
- pub const CreateForeignAssetDeposit: u128 = UNITS / 10;
}
/// Builds an (un)congestion XCM program with the `report_bridge_status` call for
@@ -62,7 +69,7 @@ pub fn build_congestion_message(
Transact {
origin_kind: OriginKind::Xcm,
fallback_max_weight: Some(XcmBridgeHubRouterTransactCallMaxWeight::get()),
- call: Call::ToRococoXcmRouter(XcmBridgeHubRouterCall::report_bridge_status {
+ call: Call::ToRococoXcmRouter(XcmBridgeHubCall::update_bridge_status {
bridge_id,
is_congested,
})
@@ -75,3 +82,93 @@ pub fn build_congestion_message(
/// Identifier of AssetHubWestend in the Westend relay chain.
pub const ASSET_HUB_WESTEND_PARACHAIN_ID: u32 = 1000;
+
+/// Westend parachain.
+#[derive(RuntimeDebug)]
+pub struct AssetHubWestend;
+
+impl Chain for AssetHubWestend {
+ const ID: ChainId = *b"ahwd";
+
+ type BlockNumber = BlockNumber;
+ type Hash = Hash;
+ type Hasher = Hasher;
+ type Header = Header;
+
+ type AccountId = AccountId;
+ type Balance = Balance;
+ type Nonce = Nonce;
+ type Signature = Signature;
+
+ const STATE_VERSION: StateVersion = StateVersion::V1;
+
+ fn max_extrinsic_size() -> u32 {
+ *BlockLength::get().max.get(DispatchClass::Normal)
+ }
+
+ fn max_extrinsic_weight() -> Weight {
+ BlockWeightsForAsyncBacking::get()
+ .get(DispatchClass::Normal)
+ .max_extrinsic
+ .unwrap_or(Weight::MAX)
+ }
+}
+
+impl Parachain for AssetHubWestend {
+ const PARACHAIN_ID: u32 = ASSET_HUB_WESTEND_PARACHAIN_ID;
+ const MAX_HEADER_SIZE: u32 = MAX_BRIDGE_HUB_HEADER_SIZE; // TODO: FAIL-CI - MAX_ASSET_HUB_HEADER_SIZE
+}
+
+/// Describing permissionless lanes instance
+impl ChainWithMessages for AssetHubWestend {
+ const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str =
+ WITH_ASSET_HUB_WESTEND_MESSAGES_PALLET_NAME;
+
+ const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce =
+ MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX;
+ const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce =
+ MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
+}
+
+/// Public key of the chain account that may be used to verify signatures.
+pub type AccountSigner = MultiSigner;
+
+/// The address format for describing accounts.
+pub type Address = MultiAddress;
+
+/// Name of the With-AssetHubWestend messages pallet instance that is deployed at bridged chains.
+pub const WITH_ASSET_HUB_WESTEND_MESSAGES_PALLET_NAME: &str = "BridgeWestendMessages";
+
+/// Name of the With-AssetHubWestend bridge-relayers pallet instance that is deployed at bridged
+/// chains.
+pub const WITH_ASSET_HUB_WESTEND_RELAYERS_PALLET_NAME: &str = "BridgeRelayers";
+
+/// Pallet index of `BridgeRococoMessages: pallet_bridge_messages::`.
+pub const WITH_BRIDGE_WESTEND_TO_ROCOCO_MESSAGES_PALLET_INDEX: u8 = 60; // TODO: FAIL-CI - correct index when AssetHubWestend
+
+decl_bridge_finality_runtime_apis!(asset_hub_westend);
+decl_bridge_messages_runtime_apis!(asset_hub_westend, HashedLaneId);
+
+frame_support::parameter_types! {
+ /// TODO: FAIL-CI - probably not needed
+ /// The XCM fee that is paid for executing XCM program (with `ExportMessage` instruction) at the Westend
+ /// AssetHub.
+ /// (initially was calculated by test `AssetHubWestend::can_calculate_weight_for_paid_export_message_with_reserve_transfer` + `33%`)
+ pub const AssetHubWestendBaseXcmFeeInWnds: u128 = 57_325_000;
+
+ /// Transaction fee that is paid at the Westend AssetHub for delivering single inbound message.
+ /// (initially was calculated by test `AssetHubWestend::can_calculate_fee_for_standalone_message_delivery_transaction` + `33%`)
+ pub const AssetHubWestendBaseDeliveryFeeInWnds: u128 = 297_685_840;
+
+ /// Transaction fee that is paid at the Westend AssetHub for delivering single outbound message confirmation.
+ /// (initially was calculated by test `AssetHubWestend::can_calculate_fee_for_standalone_message_confirmation_transaction` + `33%`)
+ pub const AssetHubWestendBaseConfirmationFeeInWnds: u128 = 56_782_099;
+}
+
+/// Wrapper over `AssetHubWestend`'s `RuntimeCall` that can be used without a runtime.
+#[derive(Decode, Encode)]
+pub enum RuntimeCall {
+ /// Points to the `pallet_xcm_bridge_hub` pallet instance for `AssetHubRococo`.
+ #[codec(index = 62)] // TODO: FAIL-CI - corect index when AssetHubWestend
+ XcmOverAssetHubRococo(bp_xcm_bridge::XcmBridgeCall),
+}
diff --git a/bridges/modules/grandpa/src/lib.rs b/bridges/modules/grandpa/src/lib.rs
index 12014f00c7cf8..5613355098326 100644
--- a/bridges/modules/grandpa/src/lib.rs
+++ b/bridges/modules/grandpa/src/lib.rs
@@ -788,9 +788,12 @@ where
pub fn synced_headers_grandpa_info() -> Vec>> {
frame_system::Pallet::::read_events_no_consensus()
.filter_map(|event| {
- let Event::::UpdatedBestFinalizedHeader { grandpa_info, .. } =
- event.event.try_into().ok()?;
- Some(grandpa_info)
+ if let Event::::UpdatedBestFinalizedHeader { grandpa_info, .. } =
+ event.event.try_into().ok()?
+ {
+ return Some(grandpa_info)
+ }
+ None
})
.collect()
}
diff --git a/bridges/primitives/xcm-bridge-router/Cargo.toml b/bridges/primitives/xcm-bridge-router/Cargo.toml
new file mode 100644
index 0000000000000..655f9fb073bdd
--- /dev/null
+++ b/bridges/primitives/xcm-bridge-router/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "bp-xcm-bridge-router"
+description = "Primitives of the xcm-bridge-hub fee pallet."
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+repository.workspace = true
+
+[lints]
+workspace = true
+
+[dependencies]
+codec = { features = ["bit-vec", "derive"], workspace = true }
+scale-info = { features = ["bit-vec", "derive"], workspace = true }
+
+# Substrate Dependencies
+sp-core = { workspace = true }
+sp-runtime = { workspace = true }
+
+# Polkadot Dependencies
+xcm = { workspace = true }
+
+[features]
+default = ["std"]
+std = [
+ "codec/std",
+ "scale-info/std",
+ "sp-core/std",
+ "sp-runtime/std",
+ "xcm/std",
+]
diff --git a/bridges/primitives/xcm-bridge-router/src/lib.rs b/bridges/primitives/xcm-bridge-router/src/lib.rs
new file mode 100644
index 0000000000000..0fb38fbe1fa78
--- /dev/null
+++ b/bridges/primitives/xcm-bridge-router/src/lib.rs
@@ -0,0 +1,74 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common 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.
+
+// Parity Bridges Common 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 Parity Bridges Common. If not, see .
+
+//! Primitives of the `xcm-bridge-hub-router` pallet.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use codec::{Decode, Encode, FullCodec, MaxEncodedLen};
+use scale_info::TypeInfo;
+use sp_core::sp_std::fmt::Debug;
+use sp_runtime::{FixedU128, RuntimeDebug};
+use xcm::latest::prelude::{InteriorLocation, Location, NetworkId};
+
+/// Current status of the bridge.
+#[derive(Clone, Decode, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, RuntimeDebug)]
+pub struct BridgeState {
+ /// Current delivery fee factor.
+ pub delivery_fee_factor: FixedU128,
+ /// Bridge congestion flag.
+ pub is_congested: bool,
+}
+
+/// Trait that resolves a specific `BridgeId` for `dest`.
+pub trait ResolveBridgeId {
+ /// Bridge identifier.
+ type BridgeId: FullCodec + MaxEncodedLen + TypeInfo + Debug + Clone + PartialEq + Eq;
+ /// Resolves `Self::BridgeId` for `dest`. If `None`, it means there is no supported bridge ID.
+ fn resolve_for_dest(bridged_dest: &Location) -> Option;
+
+ /// Resolves `Self::BridgeId` for `bridged_network` and `bridged_dest`. If `None`, it means
+ /// there is no supported bridge ID.
+ fn resolve_for(
+ bridged_network: &NetworkId,
+ bridged_dest: &InteriorLocation,
+ ) -> Option;
+}
+
+/// The default implementation of `ResolveBridgeId` for `()` returns `None`.
+impl ResolveBridgeId for () {
+ type BridgeId = ();
+
+ fn resolve_for_dest(_dest: &Location) -> Option {
+ None
+ }
+
+ fn resolve_for(
+ _bridged_network: &NetworkId,
+ _bridged_dest: &InteriorLocation,
+ ) -> Option {
+ None
+ }
+}
+
+/// A minimized version of `pallet-xcm-bridge-router::Call` that can be used without a runtime.
+#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
+#[allow(non_camel_case_types)]
+pub enum XcmBridgeHubCall {
+ /// `pallet-xcm-bridge-router::Call::update_bridge_status`
+ #[codec(index = 0)]
+ update_bridge_status { bridge_id: BridgeId, is_congested: bool },
+}
diff --git a/bridges/primitives/xcm-bridge/Cargo.toml b/bridges/primitives/xcm-bridge/Cargo.toml
new file mode 100644
index 0000000000000..a324729b91541
--- /dev/null
+++ b/bridges/primitives/xcm-bridge/Cargo.toml
@@ -0,0 +1,45 @@
+[package]
+name = "bp-xcm-bridge"
+description = "Primitives of the xcm-bridge-hub pallet."
+version = "0.2.0"
+authors.workspace = true
+edition.workspace = true
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+repository.workspace = true
+
+[lints]
+workspace = true
+
+[dependencies]
+codec = { features = ["derive"], workspace = true }
+impl-trait-for-tuples = { workspace = true }
+scale-info = { features = ["derive"], workspace = true }
+serde = { features = ["alloc", "derive"], workspace = true }
+
+# Bridge Dependencies
+bp-messages = { workspace = true }
+bp-runtime = { workspace = true }
+
+# Substrate Dependencies
+frame-support = { workspace = true }
+sp-core = { workspace = true }
+sp-io = { workspace = true }
+sp-std = { workspace = true }
+
+# Polkadot Dependencies
+xcm = { workspace = true }
+
+[features]
+default = ["std"]
+std = [
+ "bp-messages/std",
+ "bp-runtime/std",
+ "codec/std",
+ "frame-support/std",
+ "scale-info/std",
+ "serde/std",
+ "sp-core/std",
+ "sp-io/std",
+ "sp-std/std",
+ "xcm/std",
+]
diff --git a/bridges/primitives/xcm-bridge/src/call_info.rs b/bridges/primitives/xcm-bridge/src/call_info.rs
new file mode 100644
index 0000000000000..d040746fae9b4
--- /dev/null
+++ b/bridges/primitives/xcm-bridge/src/call_info.rs
@@ -0,0 +1,47 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common 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.
+
+// Parity Bridges Common 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 Parity Bridges Common. If not, see .
+
+//! Defines structures related to calls of the `pallet-xcm-bridge` pallet.
+
+use crate::Receiver;
+use bp_messages::MessageNonce;
+use codec::{Decode, Encode};
+use scale_info::TypeInfo;
+use sp_std::boxed::Box;
+use xcm::prelude::VersionedInteriorLocation;
+
+/// A minimized version of `pallet_xcm_bridge::Call` that can be used without a runtime.
+#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
+#[allow(non_camel_case_types)]
+pub enum XcmBridgeCall {
+ /// `pallet_xcm_bridge::Call::open_bridge`
+ #[codec(index = 0)]
+ open_bridge {
+ /// Universal `InteriorLocation` from the bridged consensus.
+ bridge_destination_universal_location: Box,
+ /// Optional `maybe_notify` holds data about the `bridge_origin_relative_location` where
+ /// notifications can be sent to handle congestion.
+ maybe_notify: Option,
+ },
+ /// `pallet_xcm_bridge::Call::close_bridge`
+ #[codec(index = 1)]
+ close_bridge {
+ /// Universal `InteriorLocation` from the bridged consensus.
+ bridge_destination_universal_location: Box,
+ /// The number of messages that we may prune in a single call.
+ may_prune_messages: MessageNonce,
+ },
+}
diff --git a/bridges/primitives/xcm-bridge/src/lib.rs b/bridges/primitives/xcm-bridge/src/lib.rs
new file mode 100644
index 0000000000000..b791b71a1dc3b
--- /dev/null
+++ b/bridges/primitives/xcm-bridge/src/lib.rs
@@ -0,0 +1,775 @@
+// Copyright 2019-2021 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common 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.
+
+// Parity Bridges Common 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 Parity Bridges Common. If not, see .
+
+//! Primitives of the xcm-bridge-hub pallet.
+
+#![warn(missing_docs)]
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use bp_messages::LaneIdType;
+use bp_runtime::{AccountIdOf, BalanceOf, Chain};
+pub use call_info::XcmBridgeCall;
+use codec::{Decode, Encode, MaxEncodedLen};
+use frame_support::{
+ ensure, sp_runtime::RuntimeDebug, CloneNoBound, PalletError, PartialEqNoBound,
+ RuntimeDebugNoBound,
+};
+use scale_info::TypeInfo;
+use serde::{Deserialize, Serialize};
+use sp_core::H256;
+use sp_io::hashing::blake2_256;
+use sp_std::boxed::Box;
+use xcm::{
+ latest::prelude::*, prelude::XcmVersion, IntoVersion, VersionedInteriorLocation,
+ VersionedLocation,
+};
+
+mod call_info;
+
+/// Encoded XCM blob. We expect the bridge messages pallet to use this blob type for both inbound
+/// and outbound payloads.
+pub type XcmAsPlainPayload = sp_std::vec::Vec;
+
+/// Bridge identifier - used **only** for communicating with sibling/parent chains in the same
+/// consensus.
+///
+/// For example, `SendXcm` implementations (which use the `latest` XCM) can use it to identify a
+/// bridge and the corresponding `LaneId` that is used for over-consensus communication between
+/// bridge hubs.
+///
+/// This identifier is constructed from the `latest` XCM, so it is expected to ensure migration to
+/// the `latest` XCM version. This could change the `BridgeId`, but it will not affect the `LaneId`.
+/// In other words, `LaneId` will never change, while `BridgeId` could change with (every) XCM
+/// upgrade.
+#[derive(
+ Clone,
+ Copy,
+ Decode,
+ Encode,
+ Eq,
+ Ord,
+ PartialOrd,
+ PartialEq,
+ TypeInfo,
+ MaxEncodedLen,
+ Serialize,
+ Deserialize,
+)]
+pub struct BridgeId(H256);
+
+impl BridgeId {
+ /// Create bridge identifier from two universal locations.
+ ///
+ /// Note: The `BridgeId` is constructed from `latest` XCM, so if stored, you need to ensure
+ /// compatibility with newer XCM versions.
+ pub fn new(
+ universal_source: &InteriorLocation,
+ universal_destination: &InteriorLocation,
+ ) -> Self {
+ const VALUES_SEPARATOR: [u8; 33] = *b"bridges-bridge-id-value-separator";
+
+ BridgeId(
+ (universal_source, VALUES_SEPARATOR, universal_destination)
+ .using_encoded(blake2_256)
+ .into(),
+ )
+ }
+
+ /// Access the inner representation.
+ pub fn inner(&self) -> H256 {
+ self.0
+ }
+}
+
+impl core::fmt::Debug for BridgeId {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ core::fmt::Debug::fmt(&self.0, f)
+ }
+}
+
+/// Local XCM channel manager.
+pub trait LocalXcmChannelManager {
+ /// Error that may be returned when suspending/resuming the bridge.
+ type Error: sp_std::fmt::Debug;
+
+ /// Suspend the bridge, opened by given origin.
+ ///
+ /// The `local_origin` is guaranteed to be in the same consensus. However, it may point to
+ /// something below the chain level - like the contract or pallet instance, for example.
+ fn suspend_bridge(local_origin: &Location, bridge: Bridge) -> Result<(), Self::Error>;
+
+ /// Resume the previously suspended bridge, opened by given origin.
+ ///
+ /// The `local_origin` is guaranteed to be in the same consensus. However, it may point to
+ /// something below the chain level - like the contract or pallet instance, for example.
+ fn resume_bridge(local_origin: &Location, bridge: Bridge) -> Result<(), Self::Error>;
+}
+
+impl LocalXcmChannelManager for () {
+ type Error = ();
+
+ fn suspend_bridge(_local_origin: &Location, _bridge: Bridge) -> Result<(), Self::Error> {
+ Ok(())
+ }
+
+ fn resume_bridge(_local_origin: &Location, _bridge: Bridge) -> Result<(), Self::Error> {
+ Ok(())
+ }
+}
+
+/// Channel status provider that may report whether it is congested or not.
+pub trait ChannelStatusProvider {
+ /// Returns true if the channel is currently active and can be used.
+ fn is_congested(with: &Location) -> bool;
+}
+
+/// Tuple implementation of `ChannelStatusProvider`, by default indicating no congestion.
+#[impl_trait_for_tuples::impl_for_tuples(30)]
+impl ChannelStatusProvider for Tuple {
+ fn is_congested(with: &Location) -> bool {
+ for_tuples!( #(
+ if Tuple::is_congested(with) {
+ return true;
+ }
+ )* );
+
+ false
+ }
+}
+
+/// Bridge state.
+#[derive(Clone, Copy, Decode, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, RuntimeDebug)]
+pub enum BridgeState {
+ /// Bridge is opened. Associated lanes are also opened.
+ Opened,
+ /// Bridge is suspended. Associated lanes are opened.
+ /// *suspended* means that we have sent the "Suspended" message/signal to the local bridge
+ /// origin.
+ ///
+ /// We keep accepting messages to the bridge to allow any inflight messages to be processed.
+ SoftSuspended,
+ /// Bridge is suspended and new messages are now being actively dropped.
+ HardSuspended,
+ /// Bridge is closed. Associated lanes are also closed.
+ /// After all outbound messages will be pruned, the bridge will vanish without any traces.
+ Closed,
+}
+
+/// Bridge metadata.
+#[derive(
+ CloneNoBound, Decode, Encode, Eq, PartialEqNoBound, TypeInfo, MaxEncodedLen, RuntimeDebugNoBound,
+)]
+#[scale_info(skip_type_params(ThisChain, LaneId))]
+pub struct Bridge {
+ /// Relative location of the bridge origin chain. This is expected to be **convertible** to the
+ /// `latest` XCM, so the check and migration needs to be ensured.
+ pub bridge_origin_relative_location: Box,
+
+ /// See [`BridgeLocations::bridge_origin_universal_location`].
+ /// Stored for `BridgeId` sanity check.
+ pub bridge_origin_universal_location: Box,
+ /// See [`BridgeLocations::bridge_destination_universal_location`].
+ /// Stored for `BridgeId` sanity check.
+ pub bridge_destination_universal_location: Box,
+
+ /// Current bridge state.
+ pub state: BridgeState,
+
+ /// Reserved amount on the sovereign account of the sibling bridge origin.
+ /// The account is derived from `self.bridge_origin_relative_location`.
+ pub deposit: Option>,
+
+ /// Mapping to the unique `LaneId`.
+ pub lane_id: LaneId,
+
+ /// Holds data about the `bridge_origin_relative_location` where notifications can be sent for
+ /// handling congestion.
+ pub maybe_notify: Option,
+}
+
+/// Receiver metadata.
+#[derive(
+ CloneNoBound,
+ Decode,
+ Encode,
+ Eq,
+ PartialEqNoBound,
+ TypeInfo,
+ MaxEncodedLen,
+ RuntimeDebugNoBound,
+ Serialize,
+ Deserialize,
+)]
+pub struct Receiver {
+ /// Pallet index.
+ pub pallet_index: u8,
+ /// Call/extrinsic index.
+ pub call_index: u8,
+}
+
+impl Receiver {
+ /// Create a new receiver.
+ pub fn new(pallet_index: u8, call_index: u8) -> Self {
+ Self { pallet_index, call_index }
+ }
+}
+
+/// An alias for the bridge deposit of `ThisChain`.
+pub type DepositOf = Deposit, BalanceOf>;
+
+/// A structure containing information about from whom the deposit is reserved.
+#[derive(Clone, Decode, Encode, Eq, PartialEq, TypeInfo, MaxEncodedLen, RuntimeDebug)]
+pub struct Deposit {
+ /// Account with the reserved funds.
+ pub account: AccountId,
+ /// Reserved amount.
+ pub amount: Balance,
+}
+
+impl Deposit {
+ /// Create new deposit.
+ pub fn new(account: AccountId, amount: Balance) -> Self {
+ Self { account, amount }
+ }
+}
+
+/// Locations of bridge endpoints at both sides of the bridge.
+#[derive(Clone, RuntimeDebug, PartialEq, Eq)]
+pub struct BridgeLocations {
+ /// Relative (to this bridge hub) location of this side of the bridge.
+ bridge_origin_relative_location: Location,
+ /// Universal (unique) location of this side of the bridge.
+ bridge_origin_universal_location: InteriorLocation,
+ /// Universal (unique) location of other side of the bridge.
+ bridge_destination_universal_location: InteriorLocation,
+ /// An identifier of the dedicated bridge message lane.
+ bridge_id: BridgeId,
+}
+
+/// Errors that may happen when we check bridge locations.
+#[derive(Encode, Decode, RuntimeDebug, PartialEq, Eq, PalletError, TypeInfo)]
+pub enum BridgeLocationsError {
+ /// Origin or destination locations are not universal.
+ NonUniversalLocation,
+ /// Bridge origin location is not supported.
+ InvalidBridgeOrigin,
+ /// Bridge destination is not supported (in general).
+ InvalidBridgeDestination,
+ /// Destination location is within the same global consensus.
+ DestinationIsLocal,
+ /// Destination network is not the network we are bridged with.
+ UnreachableDestination,
+ /// Destination location is unsupported. We only support bridges with relay
+ /// chain or its parachains.
+ UnsupportedDestinationLocation,
+ /// The version of XCM location argument is unsupported.
+ UnsupportedXcmVersion,
+ /// The `LaneIdType` generator is not supported.
+ UnsupportedLaneIdType,
+}
+
+impl BridgeLocations {
+ /// Given XCM locations, generate lane id and universal locations of bridge endpoints.
+ ///
+ /// The `here_universal_location` is the universal location of the bridge hub runtime.
+ ///
+ /// The `bridge_origin_relative_location` is the relative (to the `here_universal_location`)
+ /// location of the bridge endpoint at this side of the bridge. It may be the parent relay
+ /// chain or the sibling parachain. All junctions below parachain level are dropped.
+ ///
+ /// The `bridge_destination_universal_location` is the universal location of the bridge
+ /// destination. It may be the parent relay or the sibling parachain of the **bridged**
+ /// bridge hub. All junctions below parachain level are dropped.
+ ///
+ /// Why we drop all junctions between parachain level - that's because the lane is a bridge
+ /// between two chains. All routing under this level happens when the message is delivered
+ /// to the bridge destination. So at bridge level we don't care about low level junctions.
+ ///
+ /// Returns error if `bridge_origin_relative_location` is outside of `here_universal_location`
+ /// local consensus OR if `bridge_destination_universal_location` is not a universal location.
+ pub fn bridge_locations(
+ here_universal_location: InteriorLocation,
+ bridge_origin_relative_location: Location,
+ bridge_destination_universal_location: InteriorLocation,
+ expected_remote_network: NetworkId,
+ ) -> Result, BridgeLocationsError> {
+ fn strip_low_level_junctions(
+ location: InteriorLocation,
+ ) -> Result {
+ let mut junctions = location.into_iter();
+
+ let global_consensus = junctions
+ .next()
+ .filter(|junction| matches!(junction, GlobalConsensus(_)))
+ .ok_or(BridgeLocationsError::NonUniversalLocation)?;
+
+ // we only expect `Parachain` junction here. There are other junctions that
+ // may need to be supported (like `GeneralKey` and `OnlyChild`), but now we
+ // only support bridges with relay and parachans
+ //
+ // if there's something other than parachain, let's strip it
+ let maybe_parachain =
+ junctions.next().filter(|junction| matches!(junction, Parachain(_)));
+ Ok(match maybe_parachain {
+ Some(parachain) => [global_consensus, parachain].into(),
+ None => [global_consensus].into(),
+ })
+ }
+
+ // ensure that the `here_universal_location` and `bridge_destination_universal_location`
+ // are universal locations within different consensus systems
+ let local_network = here_universal_location
+ .global_consensus()
+ .map_err(|_| BridgeLocationsError::NonUniversalLocation)?;
+ let remote_network = bridge_destination_universal_location
+ .global_consensus()
+ .map_err(|_| BridgeLocationsError::NonUniversalLocation)?;
+ ensure!(local_network != remote_network, BridgeLocationsError::DestinationIsLocal);
+ ensure!(
+ remote_network == expected_remote_network,
+ BridgeLocationsError::UnreachableDestination
+ );
+
+ // get universal location of endpoint, located at this side of the bridge
+ let bridge_origin_universal_location = here_universal_location
+ .within_global(bridge_origin_relative_location.clone())
+ .map_err(|_| BridgeLocationsError::InvalidBridgeOrigin)?;
+ // strip low-level junctions within universal locations
+ let bridge_origin_universal_location =
+ strip_low_level_junctions(bridge_origin_universal_location)?;
+ let bridge_destination_universal_location =
+ strip_low_level_junctions(bridge_destination_universal_location)?;
+
+ // we know that the `bridge_destination_universal_location` starts from the
+ // `GlobalConsensus` and we know that the `bridge_origin_universal_location`
+ // is also within the `GlobalConsensus`. So we know that the lane id will be
+ // the same on both ends of the bridge
+ let bridge_id = BridgeId::new(
+ &bridge_origin_universal_location,
+ &bridge_destination_universal_location,
+ );
+
+ Ok(Box::new(BridgeLocations {
+ bridge_origin_relative_location,
+ bridge_origin_universal_location,
+ bridge_destination_universal_location,
+ bridge_id,
+ }))
+ }
+
+ /// Getter for `bridge_origin_relative_location`
+ pub fn bridge_origin_relative_location(&self) -> &Location {
+ &self.bridge_origin_relative_location
+ }
+
+ /// Getter for `bridge_origin_universal_location`
+ pub fn bridge_origin_universal_location(&self) -> &InteriorLocation {
+ &self.bridge_origin_universal_location
+ }
+
+ /// Getter for `bridge_destination_universal_location`
+ pub fn bridge_destination_universal_location(&self) -> &InteriorLocation {
+ &self.bridge_destination_universal_location
+ }
+
+ /// Getter for `bridge_id`
+ pub fn bridge_id(&self) -> &BridgeId {
+ &self.bridge_id
+ }
+
+ /// Generates the exact same `LaneId` on the both bridge hubs.
+ ///
+ /// Note: Use this **only** when opening a new bridge.
+ pub fn calculate_lane_id(
+ &self,
+ xcm_version: XcmVersion,
+ ) -> Result {
+ // a tricky helper struct that adds required `Ord` support for
+ // `VersionedInteriorLocation`
+ #[derive(Eq, PartialEq, Ord, PartialOrd)]
+ struct EncodedVersionedInteriorLocation(sp_std::vec::Vec);
+ impl Encode for EncodedVersionedInteriorLocation {
+ fn encode(&self) -> sp_std::vec::Vec {
+ self.0.clone()
+ }
+ }
+
+ let universal_location1 =
+ VersionedInteriorLocation::from(self.bridge_origin_universal_location.clone())
+ .into_version(xcm_version)
+ .map_err(|_| BridgeLocationsError::UnsupportedXcmVersion);
+ let universal_location2 =
+ VersionedInteriorLocation::from(self.bridge_destination_universal_location.clone())
+ .into_version(xcm_version)
+ .map_err(|_| BridgeLocationsError::UnsupportedXcmVersion);
+
+ LaneId::try_new(
+ EncodedVersionedInteriorLocation(universal_location1.encode()),
+ EncodedVersionedInteriorLocation(universal_location2.encode()),
+ )
+ .map_err(|_| BridgeLocationsError::UnsupportedLaneIdType)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use xcm::latest::ROCOCO_GENESIS_HASH;
+
+ const LOCAL_NETWORK: NetworkId = Kusama;
+ const REMOTE_NETWORK: NetworkId = Polkadot;
+ const UNREACHABLE_NETWORK: NetworkId = NetworkId::ByGenesis(ROCOCO_GENESIS_HASH);
+ const SIBLING_PARACHAIN: u32 = 1000;
+ const LOCAL_BRIDGE_HUB: u32 = 1001;
+ const REMOTE_PARACHAIN: u32 = 2000;
+
+ struct SuccessfulTest {
+ here_universal_location: InteriorLocation,
+ bridge_origin_relative_location: Location,
+
+ bridge_origin_universal_location: InteriorLocation,
+ bridge_destination_universal_location: InteriorLocation,
+
+ expected_remote_network: NetworkId,
+ }
+
+ fn run_successful_test(test: SuccessfulTest) -> BridgeLocations {
+ let locations = BridgeLocations::bridge_locations(
+ test.here_universal_location,
+ test.bridge_origin_relative_location.clone(),
+ test.bridge_destination_universal_location.clone(),
+ test.expected_remote_network,
+ );
+ assert_eq!(
+ locations,
+ Ok(Box::new(BridgeLocations {
+ bridge_origin_relative_location: test.bridge_origin_relative_location,
+ bridge_origin_universal_location: test.bridge_origin_universal_location.clone(),
+ bridge_destination_universal_location: test
+ .bridge_destination_universal_location
+ .clone(),
+ bridge_id: BridgeId::new(
+ &test.bridge_origin_universal_location,
+ &test.bridge_destination_universal_location,
+ ),
+ })),
+ );
+
+ *locations.unwrap()
+ }
+
+ // successful tests that with various origins and destinations
+
+ #[test]
+ fn at_relay_from_local_relay_to_remote_relay_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: Here.into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+
+ expected_remote_network: REMOTE_NETWORK,
+ });
+ }
+
+ #[test]
+ fn at_relay_from_sibling_parachain_to_remote_relay_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: [Parachain(SIBLING_PARACHAIN)].into(),
+
+ bridge_origin_universal_location: [
+ GlobalConsensus(LOCAL_NETWORK),
+ Parachain(SIBLING_PARACHAIN),
+ ]
+ .into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+
+ expected_remote_network: REMOTE_NETWORK,
+ });
+ }
+
+ #[test]
+ fn at_relay_from_local_relay_to_remote_parachain_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: Here.into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [
+ GlobalConsensus(REMOTE_NETWORK),
+ Parachain(REMOTE_PARACHAIN),
+ ]
+ .into(),
+
+ expected_remote_network: REMOTE_NETWORK,
+ });
+ }
+
+ #[test]
+ fn at_relay_from_sibling_parachain_to_remote_parachain_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: [Parachain(SIBLING_PARACHAIN)].into(),
+
+ bridge_origin_universal_location: [
+ GlobalConsensus(LOCAL_NETWORK),
+ Parachain(SIBLING_PARACHAIN),
+ ]
+ .into(),
+ bridge_destination_universal_location: [
+ GlobalConsensus(REMOTE_NETWORK),
+ Parachain(REMOTE_PARACHAIN),
+ ]
+ .into(),
+
+ expected_remote_network: REMOTE_NETWORK,
+ });
+ }
+
+ #[test]
+ fn at_bridge_hub_from_local_relay_to_remote_relay_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK), Parachain(LOCAL_BRIDGE_HUB)]
+ .into(),
+ bridge_origin_relative_location: Parent.into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+
+ expected_remote_network: REMOTE_NETWORK,
+ });
+ }
+
+ #[test]
+ fn at_bridge_hub_from_sibling_parachain_to_remote_relay_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK), Parachain(LOCAL_BRIDGE_HUB)]
+ .into(),
+ bridge_origin_relative_location: ParentThen([Parachain(SIBLING_PARACHAIN)].into())
+ .into(),
+
+ bridge_origin_universal_location: [
+ GlobalConsensus(LOCAL_NETWORK),
+ Parachain(SIBLING_PARACHAIN),
+ ]
+ .into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+
+ expected_remote_network: REMOTE_NETWORK,
+ });
+ }
+
+ #[test]
+ fn at_bridge_hub_from_local_relay_to_remote_parachain_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK), Parachain(LOCAL_BRIDGE_HUB)]
+ .into(),
+ bridge_origin_relative_location: Parent.into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [
+ GlobalConsensus(REMOTE_NETWORK),
+ Parachain(REMOTE_PARACHAIN),
+ ]
+ .into(),
+
+ expected_remote_network: REMOTE_NETWORK,
+ });
+ }
+
+ #[test]
+ fn at_bridge_hub_from_sibling_parachain_to_remote_parachain_works() {
+ run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK), Parachain(LOCAL_BRIDGE_HUB)]
+ .into(),
+ bridge_origin_relative_location: ParentThen([Parachain(SIBLING_PARACHAIN)].into())
+ .into(),
+
+ bridge_origin_universal_location: [
+ GlobalConsensus(LOCAL_NETWORK),
+ Parachain(SIBLING_PARACHAIN),
+ ]
+ .into(),
+ bridge_destination_universal_location: [
+ GlobalConsensus(REMOTE_NETWORK),
+ Parachain(REMOTE_PARACHAIN),
+ ]
+ .into(),
+
+ expected_remote_network: REMOTE_NETWORK,
+ });
+ }
+
+ // successful tests that show that we are ignoring low-level junctions of bridge origins
+
+ #[test]
+ fn low_level_junctions_at_bridge_origin_are_stripped() {
+ let locations1 = run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: Here.into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+
+ expected_remote_network: REMOTE_NETWORK,
+ });
+ let locations2 = run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: [PalletInstance(0)].into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+
+ expected_remote_network: REMOTE_NETWORK,
+ });
+
+ assert_eq!(locations1.bridge_id, locations2.bridge_id);
+ }
+
+ #[test]
+ fn low_level_junctions_at_bridge_destination_are_stripped() {
+ let locations1 = run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: Here.into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+
+ expected_remote_network: REMOTE_NETWORK,
+ });
+ let locations2 = run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_origin_relative_location: Here.into(),
+
+ bridge_origin_universal_location: [GlobalConsensus(LOCAL_NETWORK)].into(),
+ bridge_destination_universal_location: [GlobalConsensus(REMOTE_NETWORK)].into(),
+
+ expected_remote_network: REMOTE_NETWORK,
+ });
+
+ assert_eq!(locations1.bridge_id, locations2.bridge_id);
+ }
+
+ #[test]
+ fn calculate_lane_id_works() {
+ type TestLaneId = bp_messages::HashedLaneId;
+
+ let from_local_to_remote = run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(LOCAL_NETWORK), Parachain(LOCAL_BRIDGE_HUB)]
+ .into(),
+ bridge_origin_relative_location: ParentThen([Parachain(SIBLING_PARACHAIN)].into())
+ .into(),
+
+ bridge_origin_universal_location: [
+ GlobalConsensus(LOCAL_NETWORK),
+ Parachain(SIBLING_PARACHAIN),
+ ]
+ .into(),
+ bridge_destination_universal_location: [
+ GlobalConsensus(REMOTE_NETWORK),
+ Parachain(REMOTE_PARACHAIN),
+ ]
+ .into(),
+
+ expected_remote_network: REMOTE_NETWORK,
+ });
+
+ let from_remote_to_local = run_successful_test(SuccessfulTest {
+ here_universal_location: [GlobalConsensus(REMOTE_NETWORK), Parachain(LOCAL_BRIDGE_HUB)]
+ .into(),
+ bridge_origin_relative_location: ParentThen([Parachain(REMOTE_PARACHAIN)].into())
+ .into(),
+
+ bridge_origin_universal_location: [
+ GlobalConsensus(REMOTE_NETWORK),
+ Parachain(REMOTE_PARACHAIN),
+ ]
+ .into(),
+ bridge_destination_universal_location: [
+ GlobalConsensus(LOCAL_NETWORK),
+ Parachain(SIBLING_PARACHAIN),
+ ]
+ .into(),
+
+ expected_remote_network: LOCAL_NETWORK,
+ });
+
+ assert_ne!(
+ from_local_to_remote.calculate_lane_id::(xcm::latest::VERSION),
+ from_remote_to_local.calculate_lane_id::(xcm::latest::VERSION - 1),
+ );
+ assert_eq!(
+ from_local_to_remote.calculate_lane_id::(xcm::latest::VERSION),
+ from_remote_to_local.calculate_lane_id::(xcm::latest::VERSION),
+ );
+ }
+
+ // negative tests
+
+ #[test]
+ fn bridge_locations_fails_when_here_is_not_universal_location() {
+ assert_eq!(
+ BridgeLocations::bridge_locations(
+ [Parachain(1000)].into(),
+ Here.into(),
+ [GlobalConsensus(REMOTE_NETWORK)].into(),
+ REMOTE_NETWORK,
+ ),
+ Err(BridgeLocationsError::NonUniversalLocation),
+ );
+ }
+
+ #[test]
+ fn bridge_locations_fails_when_computed_destination_is_not_universal_location() {
+ assert_eq!(
+ BridgeLocations::bridge_locations(
+ [GlobalConsensus(LOCAL_NETWORK)].into(),
+ Here.into(),
+ [OnlyChild].into(),
+ REMOTE_NETWORK,
+ ),
+ Err(BridgeLocationsError::NonUniversalLocation),
+ );
+ }
+
+ #[test]
+ fn bridge_locations_fails_when_computed_destination_is_local() {
+ assert_eq!(
+ BridgeLocations::bridge_locations(
+ [GlobalConsensus(LOCAL_NETWORK)].into(),
+ Here.into(),
+ [GlobalConsensus(LOCAL_NETWORK), OnlyChild].into(),
+ REMOTE_NETWORK,
+ ),
+ Err(BridgeLocationsError::DestinationIsLocal),
+ );
+ }
+
+ #[test]
+ fn bridge_locations_fails_when_computed_destination_is_unreachable() {
+ assert_eq!(
+ BridgeLocations::bridge_locations(
+ [GlobalConsensus(LOCAL_NETWORK)].into(),
+ Here.into(),
+ [GlobalConsensus(UNREACHABLE_NETWORK)].into(),
+ REMOTE_NETWORK,
+ ),
+ Err(BridgeLocationsError::UnreachableDestination),
+ );
+ }
+}
diff --git a/bridges/snowbridge/primitives/router/Cargo.toml b/bridges/snowbridge/primitives/router/Cargo.toml
new file mode 100644
index 0000000000000..e44cca077ef32
--- /dev/null
+++ b/bridges/snowbridge/primitives/router/Cargo.toml
@@ -0,0 +1,55 @@
+[package]
+name = "snowbridge-router-primitives"
+description = "Snowbridge Router Primitives"
+version = "0.9.0"
+authors = ["Snowfork "]
+edition.workspace = true
+repository.workspace = true
+license = "Apache-2.0"
+categories = ["cryptography::cryptocurrencies"]
+
+[lints]
+workspace = true
+
+[dependencies]
+codec = { workspace = true }
+log = { workspace = true }
+scale-info = { features = ["derive"], workspace = true }
+
+frame-support = { workspace = true }
+sp-core = { workspace = true }
+sp-io = { workspace = true }
+sp-runtime = { workspace = true }
+sp-std = { workspace = true }
+
+xcm = { workspace = true }
+xcm-executor = { workspace = true }
+
+snowbridge-core = { workspace = true }
+
+hex-literal = { workspace = true, default-features = true }
+
+[dev-dependencies]
+
+[features]
+default = ["std"]
+std = [
+ "codec/std",
+ "frame-support/std",
+ "log/std",
+ "scale-info/std",
+ "snowbridge-core/std",
+ "sp-core/std",
+ "sp-io/std",
+ "sp-runtime/std",
+ "sp-std/std",
+ "xcm-executor/std",
+ "xcm/std",
+]
+runtime-benchmarks = [
+ "frame-support/runtime-benchmarks",
+ "snowbridge-core/runtime-benchmarks",
+ "sp-runtime/runtime-benchmarks",
+ "xcm-executor/runtime-benchmarks",
+ "xcm/runtime-benchmarks",
+]
diff --git a/bridges/snowbridge/primitives/router/README.md b/bridges/snowbridge/primitives/router/README.md
new file mode 100644
index 0000000000000..45967cbf76ca5
--- /dev/null
+++ b/bridges/snowbridge/primitives/router/README.md
@@ -0,0 +1,4 @@
+# Router Primitives
+
+Inbound and outbound router logic. Does XCM conversion to a lowered, simpler format the Ethereum contracts can
+understand.
diff --git a/bridges/snowbridge/primitives/router/src/inbound/mock.rs b/bridges/snowbridge/primitives/router/src/inbound/mock.rs
new file mode 100644
index 0000000000000..537853b324f63
--- /dev/null
+++ b/bridges/snowbridge/primitives/router/src/inbound/mock.rs
@@ -0,0 +1,48 @@
+use crate::inbound::{MessageToXcm, TokenId};
+use frame_support::parameter_types;
+use sp_runtime::{
+ traits::{IdentifyAccount, MaybeEquivalence, Verify},
+ MultiSignature,
+};
+use xcm::{latest::WESTEND_GENESIS_HASH, prelude::*};
+
+pub const CHAIN_ID: u64 = 11155111;
+pub const NETWORK: NetworkId = Ethereum { chain_id: CHAIN_ID };
+
+parameter_types! {
+ pub EthereumNetwork: NetworkId = NETWORK;
+
+ pub const CreateAssetCall: [u8;2] = [53, 0];
+ pub const CreateAssetExecutionFee: u128 = 2_000_000_000;
+ pub const CreateAssetDeposit: u128 = 100_000_000_000;
+ pub const SendTokenExecutionFee: u128 = 1_000_000_000;
+ pub const InboundQueuePalletInstance: u8 = 80;
+ pub UniversalLocation: InteriorLocation =
+ [GlobalConsensus(ByGenesis(WESTEND_GENESIS_HASH)), Parachain(1002)].into();
+ pub AssetHubFromEthereum: Location = Location::new(1,[GlobalConsensus(ByGenesis(WESTEND_GENESIS_HASH)),Parachain(1000)]);
+}
+
+type Signature = MultiSignature;
+type AccountId = <::Signer as IdentifyAccount>::AccountId;
+type Balance = u128;
+
+pub(crate) struct MockTokenIdConvert;
+impl MaybeEquivalence for MockTokenIdConvert {
+ fn convert(_id: &TokenId) -> Option {
+ Some(Location::parent())
+ }
+ fn convert_back(_loc: &Location) -> Option {
+ None
+ }
+}
+
+pub(crate) type MessageConverter = MessageToXcm<
+ CreateAssetCall,
+ CreateAssetDeposit,
+ InboundQueuePalletInstance,
+ AccountId,
+ Balance,
+ MockTokenIdConvert,
+ UniversalLocation,
+ AssetHubFromEthereum,
+>;
diff --git a/bridges/snowbridge/primitives/router/src/inbound/mod.rs b/bridges/snowbridge/primitives/router/src/inbound/mod.rs
new file mode 100644
index 0000000000000..1c210afb1f740
--- /dev/null
+++ b/bridges/snowbridge/primitives/router/src/inbound/mod.rs
@@ -0,0 +1,490 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: 2023 Snowfork
+//! Converts messages from Ethereum to XCM messages
+
+#[cfg(test)]
+mod mock;
+#[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;
+
+/// 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 Ethereum 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,
+ },
+ /// Send Polkadot token back to the original parachain
+ 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 parachain `para_id` on
+ /// AssetHub, Account `id` on the destination parachain will receive the funds via a
+ /// reserve-backed transfer. See
+ 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
+ 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,
+ CreateAssetDeposit: Get,
+ Balance: BalanceT,
+ ConvertAssetId: MaybeEquivalence,
+ EthereumUniversalLocation: Get,
+ GlobalAssetHubLocation: Get,
+{
+ _phantom: PhantomData<(
+ CreateAssetCall,
+ CreateAssetDeposit,
+ InboundQueuePalletInstance,
+ AccountId,
+ Balance,
+ ConvertAssetId,
+ EthereumUniversalLocation,
+ GlobalAssetHubLocation,
+ )>,
+}
+
+/// 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,
+ 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;
+ 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>;
+}
+
+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,
+ CreateAssetDeposit: Get,
+ InboundQueuePalletInstance: Get,
+ Balance: BalanceT + From,
+ AccountId: Into<[u8; 32]>,
+ ConvertAssetId: MaybeEquivalence,
+ EthereumUniversalLocation: Get,
+ GlobalAssetHubLocation: Get,
+{
+ 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,
+ CreateAssetDeposit: Get,
+ InboundQueuePalletInstance: Get,
+ Balance: BalanceT + From,
+ AccountId: Into<[u8; 32]>,
+ ConvertAssetId: MaybeEquivalence,
+ EthereumUniversalLocation: Get,
+ GlobalAssetHubLocation: Get,
+{
+ 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 pallet 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 pallet.
+ 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, [Parachain(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 parachain.
+ 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 the token is `0x0000000000000000000000000000000000000000` then return the location of
+ // native Ether.
+ 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 parachain.
+ 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 parachain 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::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()))
+ }
+}
+
+pub struct EthereumLocationsConverterFor(PhantomData);
+impl ConvertLocation for EthereumLocationsConverterFor
+where
+ AccountId: From<[u8; 32]> + Clone,
+{
+ fn convert_location(location: &Location) -> Option {
+ 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 EthereumLocationsConverterFor {
+ 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)
+ }
+}
diff --git a/bridges/snowbridge/primitives/router/src/inbound/tests.rs b/bridges/snowbridge/primitives/router/src/inbound/tests.rs
new file mode 100644
index 0000000000000..11d7928602c6e
--- /dev/null
+++ b/bridges/snowbridge/primitives/router/src/inbound/tests.rs
@@ -0,0 +1,145 @@
+use super::EthereumLocationsConverterFor;
+use crate::inbound::{
+ mock::*, Command, ConvertMessage, Destination, MessageV1, VersionedMessage, H160,
+};
+use frame_support::assert_ok;
+use hex_literal::hex;
+use xcm::prelude::*;
+use xcm_executor::traits::ConvertLocation;
+
+#[test]
+fn test_ethereum_network_converts_successfully() {
+ let expected_account: [u8; 32] =
+ hex!("ce796ae65569a670d0c1cc1ac12515a3ce21b5fbf729d63d7b289baad070139d");
+ 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_network_converts_successfully() {
+ let expected_account: [u8; 32] =
+ hex!("9038d35aba0e78e072d29b2d65be9df5bb4d7d94b4609c9cf98ea8e66e544052");
+ let contract_location = Location::new(
+ 2,
+ [GlobalConsensus(NETWORK), AccountKey20 { network: None, key: [123u8; 20] }],
+ );
+
+ 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(Polkadot), Parachain(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(Polkadot), Parachain(1000)].into();
+ let global_ah = Location::new(1, ah_context.clone());
+ let assets = vec![
+ // DOT
+ Location::new(1, []),
+ // GLMR (Some Polkadot parachain currency)
+ Location::new(1, [Parachain(2004)]),
+ // AH asset
+ Location::new(0, [PalletInstance(50), GeneralIndex(42)]),
+ // KSM
+ Location::new(2, [GlobalConsensus(Kusama)]),
+ // KAR (Some Kusama parachain currency)
+ Location::new(2, [GlobalConsensus(Kusama), Parachain(2000)]),
+ ];
+ for asset in assets.iter() {
+ // reanchor logic in pallet_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_token_with_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_token_with_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))
+}
diff --git a/bridges/snowbridge/primitives/router/src/lib.rs b/bridges/snowbridge/primitives/router/src/lib.rs
new file mode 100644
index 0000000000000..d9031c69b22b8
--- /dev/null
+++ b/bridges/snowbridge/primitives/router/src/lib.rs
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: 2023 Snowfork
+#![cfg_attr(not(feature = "std"), no_std)]
+
+pub mod inbound;
+pub mod outbound;
diff --git a/bridges/snowbridge/primitives/router/src/outbound/mod.rs b/bridges/snowbridge/primitives/router/src/outbound/mod.rs
new file mode 100644
index 0000000000000..622c408070150
--- /dev/null
+++ b/bridges/snowbridge/primitives/router/src/outbound/mod.rs
@@ -0,0 +1,428 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: 2023 Snowfork
+//! 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},
+ AgentId, ChannelId, ParaId, TokenId, TokenIdOf,
+};
+use sp_core::{H160, H256};
+use sp_runtime::traits::MaybeEquivalence;
+use sp_std::{iter::Peekable, marker::PhantomData, prelude::*};
+use xcm::prelude::*;
+use xcm_executor::traits::{ConvertLocation, ExportXcm};
+
+pub struct EthereumBlobExporter<
+ UniversalLocation,
+ EthereumNetwork,
+ OutboundQueue,
+ AgentHashedDescription,
+ ConvertAssetId,
+>(
+ PhantomData<(
+ UniversalLocation,
+ EthereumNetwork,
+ OutboundQueue,
+ AgentHashedDescription,
+ ConvertAssetId,
+ )>,
+);
+
+impl
+ ExportXcm
+ for EthereumBlobExporter<
+ UniversalLocation,
+ EthereumNetwork,
+ OutboundQueue,
+ AgentHashedDescription,
+ ConvertAssetId,
+ >
+where
+ UniversalLocation: Get,
+ EthereumNetwork: Get,
+ OutboundQueue: SendMessage,
+ AgentHashedDescription: ConvertLocation,
+ ConvertAssetId: MaybeEquivalence,
+{
+ type Ticket = (Vec, XcmHash);
+
+ fn validate(
+ network: NetworkId,
+ _channel: u32,
+ universal_source: &mut Option,
+ destination: &mut Option,
+ message: &mut Option>,
+ ) -> SendResult {
+ 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)
+ }
+
+ // Cloning destination to avoid modifying the value so subsequent exporters can use it.
+ let dest = destination.clone().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)
+ }
+
+ // Cloning universal_source to avoid modifying the value so subsequent exporters can use it.
+ let (local_net, local_sub) = universal_source.clone()
+ .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::NotApplicable
+ })?;
+
+ 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.as_slice() {
+ [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::NotApplicable)
+ },
+ };
+
+ let source_location = Location::new(1, local_sub.clone());
+
+ 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::NotApplicable)
+ },
+ };
+
+ 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, agent_id);
+ let (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 channel_id: ChannelId = ParaId::from(para_id).into();
+
+ let outbound_message = Message { id: Some(message_id.into()), channel_id, 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 Asset
+ let fee = Asset::from((Location::parent(), fee.total())).into();
+
+ Ok(((ticket.encode(), message_id), fee))
+ }
+
+ fn deliver(blob: (Vec, XcmHash)) -> Result {
+ 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,
+ ReserveAssetDepositedExpected,
+ InvalidAsset,
+ UnexpectedInstruction,
+}
+
+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, ConvertAssetId, Call> {
+ iter: Peekable>>,
+ ethereum_network: NetworkId,
+ agent_id: AgentId,
+ _marker: PhantomData,
+}
+impl<'a, ConvertAssetId, Call> XcmConverter<'a, ConvertAssetId, Call>
+where
+ ConvertAssetId: MaybeEquivalence,
+{
+ fn new(message: &'a Xcm, ethereum_network: NetworkId, agent_id: AgentId) -> Self {
+ Self {
+ iter: message.inner().iter().peekable(),
+ ethereum_network,
+ agent_id,
+ _marker: Default::default(),
+ }
+ }
+
+ fn convert(&mut self) -> Result<(Command, [u8; 32]), XcmConverterError> {
+ let result = match self.peek() {
+ Ok(ReserveAssetDeposited { .. }) => self.make_mint_foreign_token_command(),
+ // Get withdraw/deposit and make native tokens create message.
+ Ok(WithdrawAsset { .. }) => self.make_unlock_native_token_command(),
+ Err(e) => Err(e),
+ _ => return Err(XcmConverterError::UnexpectedInstruction),
+ }?;
+
+ // All xcm instructions must be consumed before exit.
+ if self.next().is_ok() {
+ return Err(XcmConverterError::EndOfXcmMessageExpected)
+ }
+
+ Ok(result)
+ }
+
+ fn make_unlock_native_token_command(
+ &mut self,
+ ) -> Result<(Command, [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.unpack(),
+ (0, [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)?;
+
+ // Fees are collected on AH, up front and directly from the user, to cover the
+ // complete cost of the transfer. Any additional fees provided in the XCM program are
+ // refunded to the beneficiary. We only validate the fee here if its provided to make sure
+ // the XCM program is well formed. Another way to think about this from an XCM perspective
+ // would be that the user offered to pay X amount in fees, but we charge 0 of that X amount
+ // (no fee) and refund X to the user.
+ 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 reserve_asset {
+ Asset { id: AssetId(inner_location), fun: Fungible(amount) } =>
+ match inner_location.unpack() {
+ // Get the ERC20 contract address of the token.
+ (0, [AccountKey20 { network, key }]) if self.network_matches(network) =>
+ Some((H160(*key), *amount)),
+ // If there is no ERC20 contract address in the location then signal to the
+ // gateway that is a native Ether transfer by using
+ // `0x0000000000000000000000000000000000000000` as the token address.
+ (0, []) => Some((H160([0; 20]), *amount)),
+ _ => None,
+ },
+ _ => None,
+ }
+ .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((
+ Command::AgentExecute {
+ agent_id: self.agent_id,
+ command: AgentExecuteCommand::TransferToken { token, recipient, amount },
+ },
+ *topic_id,
+ ))
+ }
+
+ fn next(&mut self) -> Result<&'a Instruction, XcmConverterError> {
+ self.iter.next().ok_or(XcmConverterError::UnexpectedEndOfXcm)
+ }
+
+ fn peek(&mut self) -> Result<&&'a Instruction, XcmConverterError> {
+ self.iter.peek().ok_or(XcmConverterError::UnexpectedEndOfXcm)
+ }
+
+ fn network_matches(&self, network: &Option) -> bool {
+ if let Some(network) = network {
+ *network == self.ethereum_network
+ } else {
+ true
+ }
+ }
+
+ /// Convert the xcm for Polkadot-native token from AH into the Command
+ /// To match transfers of Polkadot-native tokens, we expect an input of the form:
+ /// # ReserveAssetDeposited
+ /// # ClearOrigin
+ /// # BuyExecution
+ /// # DepositAsset
+ /// # SetTopic
+ fn make_mint_foreign_token_command(
+ &mut self,
+ ) -> Result<(Command, [u8; 32]), XcmConverterError> {
+ use XcmConverterError::*;
+
+ // Get the reserve assets.
+ let reserve_assets =
+ match_expression!(self.next()?, ReserveAssetDeposited(reserve_assets), reserve_assets)
+ .ok_or(ReserveAssetDepositedExpected)?;
+
+ // 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.unpack(),
+ (0, [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)?;
+
+ // Fees are collected on AH, up front and directly from the user, to cover the
+ // complete cost of the transfer. Any additional fees provided in the XCM program are
+ // refunded to the beneficiary. We only validate the fee here if its provided to make sure
+ // the XCM program is well formed. Another way to think about this from an XCM perspective
+ // would be that the user offered to pay X amount in fees, but we charge 0 of that X amount
+ // (no fee) and refund X to the user.
+ 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 (asset_id, amount) = match reserve_asset {
+ Asset { id: AssetId(inner_location), fun: Fungible(amount) } =>
+ Some((inner_location.clone(), *amount)),
+ _ => None,
+ }
+ .ok_or(AssetResolutionFailed)?;
+
+ // transfer amount must be greater than 0.
+ ensure!(amount > 0, ZeroAssetTransfer);
+
+ let token_id = TokenIdOf::convert_location(&asset_id).ok_or(InvalidAsset)?;
+
+ let expected_asset_id = ConvertAssetId::convert(&token_id).ok_or(InvalidAsset)?;
+
+ ensure!(asset_id == expected_asset_id, InvalidAsset);
+
+ // 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((Command::MintForeignToken { token_id, recipient, amount }, *topic_id))
+ }
+}
diff --git a/bridges/snowbridge/primitives/router/src/outbound/tests.rs b/bridges/snowbridge/primitives/router/src/outbound/tests.rs
new file mode 100644
index 0000000000000..2a60f9f3e0eab
--- /dev/null
+++ b/bridges/snowbridge/primitives/router/src/outbound/tests.rs
@@ -0,0 +1,1314 @@
+use frame_support::parameter_types;
+use hex_literal::hex;
+use snowbridge_core::{
+ outbound::{Fee, SendError, SendMessageFeeProvider},
+ AgentIdOf,
+};
+use sp_std::default::Default;
+use xcm::{
+ latest::{ROCOCO_GENESIS_HASH, WESTEND_GENESIS_HASH},
+ prelude::SendError as XcmSendError,
+};
+
+use super::*;
+
+parameter_types! {
+ const MaxMessageSize: u32 = u32::MAX;
+ const RelayNetwork: NetworkId = Polkadot;
+ UniversalLocation: InteriorLocation = [GlobalConsensus(RelayNetwork::get()), Parachain(1013)].into();
+ const BridgedNetwork: NetworkId = Ethereum{ chain_id: 1 };
+ const NonBridgedNetwork: NetworkId = Ethereum{ chain_id: 2 };
+}
+
+struct MockOkOutboundQueue;
+impl SendMessage for MockOkOutboundQueue {
+ type Ticket = ();
+
+ fn validate(_: &Message) -> Result<(Self::Ticket, Fee), SendError> {
+ Ok(((), Fee { local: 1, remote: 1 }))
+ }
+
+ fn deliver(_: Self::Ticket) -> Result {
+ Ok(H256::zero())
+ }
+}
+
+impl SendMessageFeeProvider for MockOkOutboundQueue {
+ type Balance = u128;
+
+ fn local_fee() -> Self::Balance {
+ 1
+ }
+}
+struct MockErrOutboundQueue;
+impl SendMessage for MockErrOutboundQueue {
+ type Ticket = ();
+
+ fn validate(_: &Message) -> Result<(Self::Ticket, Fee), SendError> {
+ Err(SendError::MessageTooLarge)
+ }
+
+ fn deliver(_: Self::Ticket) -> Result {
+ Err(SendError::MessageTooLarge)
+ }
+}
+
+impl SendMessageFeeProvider for MockErrOutboundQueue {
+ type Balance = u128;
+
+ fn local_fee() -> Self::Balance {
+ 1
+ }
+}
+
+pub struct MockTokenIdConvert;
+impl MaybeEquivalence for MockTokenIdConvert {
+ fn convert(_id: &TokenId) -> Option {
+ Some(Location::new(1, [GlobalConsensus(ByGenesis(WESTEND_GENESIS_HASH))]))
+ }
+ fn convert_back(_loc: &Location) -> Option {
+ None
+ }
+}
+
+#[test]
+fn exporter_validate_with_unknown_network_yields_not_applicable() {
+ let network = Ethereum { chain_id: 1337 };
+ let channel: u32 = 0;
+ let mut universal_source: Option = None;
+ let mut destination: Option = None;
+ let mut message: Option> = None;
+
+ let result =
+ EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockOkOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::validate(network, channel, &mut universal_source, &mut destination, &mut message);
+ assert_eq!(result, Err(XcmSendError::NotApplicable));
+}
+
+#[test]
+fn exporter_validate_with_invalid_destination_yields_missing_argument() {
+ let network = BridgedNetwork::get();
+ let channel: u32 = 0;
+ let mut universal_source: Option = None;
+ let mut destination: Option = None;
+ let mut message: Option> = None;
+
+ let result =
+ EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockOkOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::validate(network, channel, &mut universal_source, &mut destination, &mut message);
+ assert_eq!(result, Err(XcmSendError::MissingArgument));
+}
+
+#[test]
+fn exporter_validate_with_x8_destination_yields_not_applicable() {
+ let network = BridgedNetwork::get();
+ let channel: u32 = 0;
+ let mut universal_source: Option = None;
+ let mut destination: Option = Some(
+ [OnlyChild, OnlyChild, OnlyChild, OnlyChild, OnlyChild, OnlyChild, OnlyChild, OnlyChild]
+ .into(),
+ );
+ let mut message: Option> = None;
+
+ let result =
+ EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockOkOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::validate(network, channel, &mut universal_source, &mut destination, &mut message);
+ assert_eq!(result, Err(XcmSendError::NotApplicable));
+}
+
+#[test]
+fn exporter_validate_without_universal_source_yields_missing_argument() {
+ let network = BridgedNetwork::get();
+ let channel: u32 = 0;
+ let mut universal_source: Option = None;
+ let mut destination: Option = Here.into();
+ let mut message: Option> = None;
+
+ let result =
+ EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockOkOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::validate(network, channel, &mut universal_source, &mut destination, &mut message);
+ assert_eq!(result, Err(XcmSendError::MissingArgument));
+}
+
+#[test]
+fn exporter_validate_without_global_universal_location_yields_not_applicable() {
+ let network = BridgedNetwork::get();
+ let channel: u32 = 0;
+ let mut universal_source: Option = Here.into();
+ let mut destination: Option = Here.into();
+ let mut message: Option> = None;
+
+ let result =
+ EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockOkOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::validate(network, channel, &mut universal_source, &mut destination, &mut message);
+ assert_eq!(result, Err(XcmSendError::NotApplicable));
+}
+
+#[test]
+fn exporter_validate_without_global_bridge_location_yields_not_applicable() {
+ let network = NonBridgedNetwork::get();
+ let channel: u32 = 0;
+ let mut universal_source: Option = Here.into();
+ let mut destination: Option = Here.into();
+ let mut message: Option> = None;
+
+ let result =
+ EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockOkOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::validate(network, channel, &mut universal_source, &mut destination, &mut message);
+ assert_eq!(result, Err(XcmSendError::NotApplicable));
+}
+
+#[test]
+fn exporter_validate_with_remote_universal_source_yields_not_applicable() {
+ let network = BridgedNetwork::get();
+ let channel: u32 = 0;
+ let mut universal_source: Option =
+ Some([GlobalConsensus(Kusama), Parachain(1000)].into());
+ let mut destination: Option = Here.into();
+ let mut message: Option> = None;
+
+ let result =
+ EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockOkOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::validate(network, channel, &mut universal_source, &mut destination, &mut message);
+ assert_eq!(result, Err(XcmSendError::NotApplicable));
+}
+
+#[test]
+fn exporter_validate_without_para_id_in_source_yields_not_applicable() {
+ let network = BridgedNetwork::get();
+ let channel: u32 = 0;
+ let mut universal_source: Option = Some(GlobalConsensus(Polkadot).into());
+ let mut destination: Option = Here.into();
+ let mut message: Option> = None;
+
+ let result =
+ EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockOkOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::validate(network, channel, &mut universal_source, &mut destination, &mut message);
+ assert_eq!(result, Err(XcmSendError::NotApplicable));
+}
+
+#[test]
+fn exporter_validate_complex_para_id_in_source_yields_not_applicable() {
+ let network = BridgedNetwork::get();
+ let channel: u32 = 0;
+ let mut universal_source: Option =
+ Some([GlobalConsensus(Polkadot), Parachain(1000), PalletInstance(12)].into());
+ let mut destination: Option = Here.into();
+ let mut message: Option> = None;
+
+ let result =
+ EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockOkOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::validate(network, channel, &mut universal_source, &mut destination, &mut message);
+ assert_eq!(result, Err(XcmSendError::NotApplicable));
+}
+
+#[test]
+fn exporter_validate_without_xcm_message_yields_missing_argument() {
+ let network = BridgedNetwork::get();
+ let channel: u32 = 0;
+ let mut universal_source: Option =
+ Some([GlobalConsensus(Polkadot), Parachain(1000)].into());
+ let mut destination: Option = Here.into();
+ let mut message: Option> = None;
+
+ let result =
+ EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockOkOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::validate(network, channel, &mut universal_source, &mut destination, &mut message);
+ assert_eq!(result, Err(XcmSendError::MissingArgument));
+}
+
+#[test]
+fn exporter_validate_with_max_target_fee_yields_unroutable() {
+ let network = BridgedNetwork::get();
+ let mut destination: Option = Here.into();
+
+ let mut universal_source: Option =
+ Some([GlobalConsensus(Polkadot), Parachain(1000)].into());
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let channel: u32 = 0;
+ let fee = Asset { id: AssetId(Here.into()), fun: Fungible(1000) };
+ let fees: Assets = vec![fee.clone()].into();
+ let assets: Assets = vec![Asset {
+ id: AssetId(AccountKey20 { network: None, key: token_address }.into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let filter: AssetFilter = assets.clone().into();
+
+ let mut message: Option> = Some(
+ vec![
+ WithdrawAsset(fees),
+ BuyExecution { fees: fee, weight_limit: Unlimited },
+ WithdrawAsset(assets),
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: Some(network), key: beneficiary_address }
+ .into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into(),
+ );
+
+ let result =
+ EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockOkOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::validate(network, channel, &mut universal_source, &mut destination, &mut message);
+
+ assert_eq!(result, Err(XcmSendError::Unroutable));
+}
+
+#[test]
+fn exporter_validate_with_unparsable_xcm_yields_unroutable() {
+ let network = BridgedNetwork::get();
+ let mut destination: Option = Here.into();
+
+ let mut universal_source: Option =
+ Some([GlobalConsensus(Polkadot), Parachain(1000)].into());
+
+ let channel: u32 = 0;
+ let fee = Asset { id: AssetId(Here.into()), fun: Fungible(1000) };
+ let fees: Assets = vec![fee.clone()].into();
+
+ let mut message: Option> =
+ Some(vec![WithdrawAsset(fees), BuyExecution { fees: fee, weight_limit: Unlimited }].into());
+
+ let result =
+ EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockOkOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::validate(network, channel, &mut universal_source, &mut destination, &mut message);
+
+ assert_eq!(result, Err(XcmSendError::Unroutable));
+}
+
+#[test]
+fn exporter_validate_xcm_success_case_1() {
+ let network = BridgedNetwork::get();
+ let mut destination: Option = Here.into();
+
+ let mut universal_source: Option =
+ Some([GlobalConsensus(Polkadot), Parachain(1000)].into());
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let channel: u32 = 0;
+ let assets: Assets = vec![Asset {
+ id: AssetId([AccountKey20 { network: None, key: token_address }].into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let fee = assets.clone().get(0).unwrap().clone();
+ let filter: AssetFilter = assets.clone().into();
+
+ let mut message: Option> = Some(
+ vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: fee, weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into(),
+ );
+
+ let result =
+ EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockOkOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::validate(network, channel, &mut universal_source, &mut destination, &mut message);
+
+ assert!(result.is_ok());
+}
+
+#[test]
+fn exporter_deliver_with_submit_failure_yields_unroutable() {
+ let result = EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockErrOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::deliver((hex!("deadbeef").to_vec(), XcmHash::default()));
+ assert_eq!(result, Err(XcmSendError::Transport("other transport error")))
+}
+
+#[test]
+fn xcm_converter_convert_success() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![Asset {
+ id: AssetId([AccountKey20 { network: None, key: token_address }].into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let filter: AssetFilter = assets.clone().into();
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+ let expected_payload = Command::AgentExecute {
+ agent_id: Default::default(),
+ command: AgentExecuteCommand::TransferToken {
+ token: token_address.into(),
+ recipient: beneficiary_address.into(),
+ amount: 1000,
+ },
+ };
+ let result = converter.convert();
+ assert_eq!(result, Ok((expected_payload, [0; 32])));
+}
+
+#[test]
+fn xcm_converter_convert_without_buy_execution_yields_success() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![Asset {
+ id: AssetId([AccountKey20 { network: None, key: token_address }].into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let filter: AssetFilter = assets.clone().into();
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+ let expected_payload = Command::AgentExecute {
+ agent_id: Default::default(),
+ command: AgentExecuteCommand::TransferToken {
+ token: token_address.into(),
+ recipient: beneficiary_address.into(),
+ amount: 1000,
+ },
+ };
+ let result = converter.convert();
+ assert_eq!(result, Ok((expected_payload, [0; 32])));
+}
+
+#[test]
+fn xcm_converter_convert_with_wildcard_all_asset_filter_succeeds() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![Asset {
+ id: AssetId([AccountKey20 { network: None, key: token_address }].into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let filter: AssetFilter = Wild(All);
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+ let expected_payload = Command::AgentExecute {
+ agent_id: Default::default(),
+ command: AgentExecuteCommand::TransferToken {
+ token: token_address.into(),
+ recipient: beneficiary_address.into(),
+ amount: 1000,
+ },
+ };
+ let result = converter.convert();
+ assert_eq!(result, Ok((expected_payload, [0; 32])));
+}
+
+#[test]
+fn xcm_converter_convert_with_native_eth_succeeds() {
+ let network = BridgedNetwork::get();
+
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ // The asset is `{ parents: 0, interior: X1(Here) }` relative to ethereum.
+ let assets: Assets = vec![Asset { id: AssetId([].into()), fun: Fungible(1000) }].into();
+ let filter: AssetFilter = Wild(All);
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+
+ // The token address that is expected to be sent should be
+ // `0x0000000000000000000000000000000000000000`. The solidity will
+ // interpret this as a transfer of ETH.
+ let expected_payload = Command::AgentExecute {
+ agent_id: Default::default(),
+ command: AgentExecuteCommand::TransferToken {
+ token: H160([0; 20]),
+ recipient: beneficiary_address.into(),
+ amount: 1000,
+ },
+ };
+ let result = converter.convert();
+ assert_eq!(result, Ok((expected_payload, [0; 32])));
+}
+
+#[test]
+fn xcm_converter_convert_with_fees_less_than_reserve_yields_success() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let asset_location: Location = [AccountKey20 { network: None, key: token_address }].into();
+ let fee_asset = Asset { id: AssetId(asset_location.clone()), fun: Fungible(500) };
+
+ let assets: Assets = vec![Asset { id: AssetId(asset_location), fun: Fungible(1000) }].into();
+
+ let filter: AssetFilter = assets.clone().into();
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: fee_asset, weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+ let expected_payload = Command::AgentExecute {
+ agent_id: Default::default(),
+ command: AgentExecuteCommand::TransferToken {
+ token: token_address.into(),
+ recipient: beneficiary_address.into(),
+ amount: 1000,
+ },
+ };
+ let result = converter.convert();
+ assert_eq!(result, Ok((expected_payload, [0; 32])));
+}
+
+#[test]
+fn xcm_converter_convert_without_set_topic_yields_set_topic_expected() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![Asset {
+ id: AssetId([AccountKey20 { network: None, key: token_address }].into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let filter: AssetFilter = assets.clone().into();
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ ClearTopic,
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::SetTopicExpected));
+}
+
+#[test]
+fn xcm_converter_convert_with_partial_message_yields_unexpected_end_of_xcm() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let assets: Assets = vec![Asset {
+ id: AssetId([AccountKey20 { network: None, key: token_address }].into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let message: Xcm<()> = vec![WithdrawAsset(assets)].into();
+
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::UnexpectedEndOfXcm));
+}
+
+#[test]
+fn xcm_converter_with_different_fee_asset_fails() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let asset_location = [AccountKey20 { network: None, key: token_address }].into();
+ let fee_asset =
+ Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(1000) };
+
+ let assets: Assets = vec![Asset { id: AssetId(asset_location), fun: Fungible(1000) }].into();
+
+ let filter: AssetFilter = assets.clone().into();
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: fee_asset, weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::InvalidFeeAsset));
+}
+
+#[test]
+fn xcm_converter_with_fees_greater_than_reserve_fails() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let asset_location: Location = [AccountKey20 { network: None, key: token_address }].into();
+ let fee_asset = Asset { id: AssetId(asset_location.clone()), fun: Fungible(1001) };
+
+ let assets: Assets = vec![Asset { id: AssetId(asset_location), fun: Fungible(1000) }].into();
+
+ let filter: AssetFilter = assets.clone().into();
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: fee_asset, weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::InvalidFeeAsset));
+}
+
+#[test]
+fn xcm_converter_convert_with_empty_xcm_yields_unexpected_end_of_xcm() {
+ let network = BridgedNetwork::get();
+
+ let message: Xcm<()> = vec![].into();
+
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::UnexpectedEndOfXcm));
+}
+
+#[test]
+fn xcm_converter_convert_with_extra_instructions_yields_end_of_xcm_message_expected() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![Asset {
+ id: AssetId([AccountKey20 { network: None, key: token_address }].into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let filter: AssetFilter = assets.clone().into();
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ClearError,
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::EndOfXcmMessageExpected));
+}
+
+#[test]
+fn xcm_converter_convert_without_withdraw_asset_yields_withdraw_expected() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![Asset {
+ id: AssetId([AccountKey20 { network: None, key: token_address }].into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let filter: AssetFilter = assets.clone().into();
+
+ let message: Xcm<()> = vec![
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::UnexpectedInstruction));
+}
+
+#[test]
+fn xcm_converter_convert_without_withdraw_asset_yields_deposit_expected() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![Asset {
+ id: AssetId(AccountKey20 { network: None, key: token_address }.into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::DepositAssetExpected));
+}
+
+#[test]
+fn xcm_converter_convert_without_assets_yields_no_reserve_assets() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![].into();
+ let filter: AssetFilter = assets.clone().into();
+
+ let fee = Asset {
+ id: AssetId(AccountKey20 { network: None, key: token_address }.into()),
+ fun: Fungible(1000),
+ };
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: fee, weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::NoReserveAssets));
+}
+
+#[test]
+fn xcm_converter_convert_with_two_assets_yields_too_many_assets() {
+ let network = BridgedNetwork::get();
+
+ let token_address_1: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let token_address_2: [u8; 20] = hex!("1100000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![
+ Asset {
+ id: AssetId(AccountKey20 { network: None, key: token_address_1 }.into()),
+ fun: Fungible(1000),
+ },
+ Asset {
+ id: AssetId(AccountKey20 { network: None, key: token_address_2 }.into()),
+ fun: Fungible(500),
+ },
+ ]
+ .into();
+ let filter: AssetFilter = assets.clone().into();
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::TooManyAssets));
+}
+
+#[test]
+fn xcm_converter_convert_without_consuming_filter_yields_filter_does_not_consume_all_assets() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![Asset {
+ id: AssetId(AccountKey20 { network: None, key: token_address }.into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let filter: AssetFilter = Wild(WildAsset::AllCounted(0));
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::FilterDoesNotConsumeAllAssets));
+}
+
+#[test]
+fn xcm_converter_convert_with_zero_amount_asset_yields_zero_asset_transfer() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![Asset {
+ id: AssetId(AccountKey20 { network: None, key: token_address }.into()),
+ fun: Fungible(0),
+ }]
+ .into();
+ let filter: AssetFilter = Wild(WildAsset::AllCounted(1));
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::ZeroAssetTransfer));
+}
+
+#[test]
+fn xcm_converter_convert_non_ethereum_asset_yields_asset_resolution_failed() {
+ let network = BridgedNetwork::get();
+
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![Asset {
+ id: AssetId([GlobalConsensus(Polkadot), Parachain(1000), GeneralIndex(0)].into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let filter: AssetFilter = Wild(WildAsset::AllCounted(1));
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::AssetResolutionFailed));
+}
+
+#[test]
+fn xcm_converter_convert_non_ethereum_chain_asset_yields_asset_resolution_failed() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![Asset {
+ id: AssetId(
+ AccountKey20 { network: Some(Ethereum { chain_id: 2 }), key: token_address }.into(),
+ ),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let filter: AssetFilter = Wild(WildAsset::AllCounted(1));
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::AssetResolutionFailed));
+}
+
+#[test]
+fn xcm_converter_convert_non_ethereum_chain_yields_asset_resolution_failed() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![Asset {
+ id: AssetId(
+ [AccountKey20 { network: Some(NonBridgedNetwork::get()), key: token_address }].into(),
+ ),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let filter: AssetFilter = Wild(WildAsset::AllCounted(1));
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::AssetResolutionFailed));
+}
+
+#[test]
+fn xcm_converter_convert_with_non_ethereum_beneficiary_yields_beneficiary_resolution_failed() {
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+
+ let beneficiary_address: [u8; 32] =
+ hex!("2000000000000000000000000000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![Asset {
+ id: AssetId(AccountKey20 { network: None, key: token_address }.into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let filter: AssetFilter = Wild(WildAsset::AllCounted(1));
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: [
+ GlobalConsensus(Polkadot),
+ Parachain(1000),
+ AccountId32 { network: Some(Polkadot), id: beneficiary_address },
+ ]
+ .into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::BeneficiaryResolutionFailed));
+}
+
+#[test]
+fn xcm_converter_convert_with_non_ethereum_chain_beneficiary_yields_beneficiary_resolution_failed()
+{
+ let network = BridgedNetwork::get();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let assets: Assets = vec![Asset {
+ id: AssetId(AccountKey20 { network: None, key: token_address }.into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let filter: AssetFilter = Wild(WildAsset::AllCounted(1));
+
+ let message: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 {
+ network: Some(Ethereum { chain_id: 2 }),
+ key: beneficiary_address,
+ }
+ .into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::BeneficiaryResolutionFailed));
+}
+
+#[test]
+fn test_describe_asset_hub() {
+ let legacy_location: Location = Location::new(0, [Parachain(1000)]);
+ let legacy_agent_id = AgentIdOf::convert_location(&legacy_location).unwrap();
+ assert_eq!(
+ legacy_agent_id,
+ hex!("72456f48efed08af20e5b317abf8648ac66e86bb90a411d9b0b713f7364b75b4").into()
+ );
+ let location: Location = Location::new(1, [Parachain(1000)]);
+ let agent_id = AgentIdOf::convert_location(&location).unwrap();
+ assert_eq!(
+ agent_id,
+ hex!("81c5ab2571199e3188135178f3c2c8e2d268be1313d029b30f534fa579b69b79").into()
+ )
+}
+
+#[test]
+fn test_describe_here() {
+ let location: Location = Location::new(0, []);
+ let agent_id = AgentIdOf::convert_location(&location).unwrap();
+ assert_eq!(
+ agent_id,
+ hex!("03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314").into()
+ )
+}
+
+#[test]
+fn xcm_converter_transfer_native_token_success() {
+ let network = BridgedNetwork::get();
+
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let amount = 1000000;
+ let asset_location = Location::new(1, [GlobalConsensus(ByGenesis(WESTEND_GENESIS_HASH))]);
+ let token_id = TokenIdOf::convert_location(&asset_location).unwrap();
+
+ let assets: Assets = vec![Asset { id: AssetId(asset_location), fun: Fungible(amount) }].into();
+ let filter: AssetFilter = assets.clone().into();
+
+ let message: Xcm<()> = vec![
+ ReserveAssetDeposited(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+ let expected_payload =
+ Command::MintForeignToken { recipient: beneficiary_address.into(), amount, token_id };
+ let result = converter.convert();
+ assert_eq!(result, Ok((expected_payload, [0; 32])));
+}
+
+#[test]
+fn xcm_converter_transfer_native_token_with_invalid_location_will_fail() {
+ let network = BridgedNetwork::get();
+
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let amount = 1000000;
+ // Invalid asset location from a different consensus
+ let asset_location =
+ Location { parents: 2, interior: [GlobalConsensus(ByGenesis(ROCOCO_GENESIS_HASH))].into() };
+
+ let assets: Assets = vec![Asset { id: AssetId(asset_location), fun: Fungible(amount) }].into();
+ let filter: AssetFilter = assets.clone().into();
+
+ let message: Xcm<()> = vec![
+ ReserveAssetDeposited(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut converter =
+ XcmConverter::::new(&message, network, Default::default());
+ let result = converter.convert();
+ assert_eq!(result.err(), Some(XcmConverterError::InvalidAsset));
+}
+
+#[test]
+fn exporter_validate_with_invalid_dest_does_not_alter_destination() {
+ let network = BridgedNetwork::get();
+ let destination: InteriorLocation = Parachain(1000).into();
+
+ let universal_source: InteriorLocation = [GlobalConsensus(Polkadot), Parachain(1000)].into();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let channel: u32 = 0;
+ let assets: Assets = vec![Asset {
+ id: AssetId([AccountKey20 { network: None, key: token_address }].into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let fee = assets.clone().get(0).unwrap().clone();
+ let filter: AssetFilter = assets.clone().into();
+ let msg: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: fee, weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut msg_wrapper: Option> = Some(msg.clone());
+ let mut dest_wrapper = Some(destination.clone());
+ let mut universal_source_wrapper = Some(universal_source.clone());
+
+ let result =
+ EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockOkOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::validate(
+ network, channel, &mut universal_source_wrapper, &mut dest_wrapper, &mut msg_wrapper
+ );
+
+ assert_eq!(result, Err(XcmSendError::NotApplicable));
+
+ // ensure mutable variables are not changed
+ assert_eq!(Some(destination), dest_wrapper);
+ assert_eq!(Some(msg), msg_wrapper);
+ assert_eq!(Some(universal_source), universal_source_wrapper);
+}
+
+#[test]
+fn exporter_validate_with_invalid_universal_source_does_not_alter_universal_source() {
+ let network = BridgedNetwork::get();
+ let destination: InteriorLocation = Here.into();
+
+ let universal_source: InteriorLocation =
+ [GlobalConsensus(ByGenesis(WESTEND_GENESIS_HASH)), Parachain(1000)].into();
+
+ let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000");
+ let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+ let channel: u32 = 0;
+ let assets: Assets = vec![Asset {
+ id: AssetId([AccountKey20 { network: None, key: token_address }].into()),
+ fun: Fungible(1000),
+ }]
+ .into();
+ let fee = assets.clone().get(0).unwrap().clone();
+ let filter: AssetFilter = assets.clone().into();
+ let msg: Xcm<()> = vec![
+ WithdrawAsset(assets.clone()),
+ ClearOrigin,
+ BuyExecution { fees: fee, weight_limit: Unlimited },
+ DepositAsset {
+ assets: filter,
+ beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(),
+ },
+ SetTopic([0; 32]),
+ ]
+ .into();
+ let mut msg_wrapper: Option> = Some(msg.clone());
+ let mut dest_wrapper = Some(destination.clone());
+ let mut universal_source_wrapper = Some(universal_source.clone());
+
+ let result =
+ EthereumBlobExporter::<
+ UniversalLocation,
+ BridgedNetwork,
+ MockOkOutboundQueue,
+ AgentIdOf,
+ MockTokenIdConvert,
+ >::validate(
+ network, channel, &mut universal_source_wrapper, &mut dest_wrapper, &mut msg_wrapper
+ );
+
+ assert_eq!(result, Err(XcmSendError::NotApplicable));
+
+ // ensure mutable variables are not changed
+ assert_eq!(Some(destination), dest_wrapper);
+ assert_eq!(Some(msg), msg_wrapper);
+ assert_eq!(Some(universal_source), universal_source_wrapper);
+}
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
index caf269f7984cc..5416149501365 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
@@ -37,7 +37,6 @@ use assets_common::{
matching::{FromNetwork, FromSiblingParachain},
AssetIdForPoolAssets, AssetIdForPoolAssetsConvert, AssetIdForTrustBackedAssetsConvert,
};
-use bp_asset_hub_rococo::CreateForeignAssetDeposit;
use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases;
use cumulus_primitives_core::{AggregateMessageOrigin, ClaimQueueOffset, CoreSelector};
use sp_api::impl_runtime_apis;
@@ -462,7 +461,8 @@ impl pallet_asset_conversion_ops::Config for Runtime {
}
parameter_types! {
- pub const ForeignAssetsAssetDeposit: Balance = CreateForeignAssetDeposit::get();
+ // we just reuse the same deposits
+ pub const ForeignAssetsAssetDeposit: Balance = AssetDeposit::get();
pub const ForeignAssetsAssetAccountDeposit: Balance = AssetAccountDeposit::get();
pub const ForeignAssetsApprovalDeposit: Balance = ApprovalDeposit::get();
pub const ForeignAssetsAssetsStringLimit: u32 = AssetsStringLimit::get();
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
index d2fca4ac40d08..c3a68c98ad08b 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
@@ -36,7 +36,6 @@ use assets_common::{
local_and_foreign_assets::{LocalFromLeft, TargetFromLeft},
AssetIdForPoolAssets, AssetIdForPoolAssetsConvert, AssetIdForTrustBackedAssetsConvert,
};
-use bp_asset_hub_westend::CreateForeignAssetDeposit;
use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases;
use cumulus_primitives_core::{AggregateMessageOrigin, ClaimQueueOffset, CoreSelector, ParaId};
@@ -515,7 +514,8 @@ impl pallet_asset_conversion_ops::Config for Runtime {
}
parameter_types! {
- pub const ForeignAssetsAssetDeposit: Balance = CreateForeignAssetDeposit::get();
+ // we just reuse the same deposits
+ pub const ForeignAssetsAssetDeposit: Balance = AssetDeposit::get();
pub const ForeignAssetsAssetAccountDeposit: Balance = AssetAccountDeposit::get();
pub const ForeignAssetsApprovalDeposit: Balance = ApprovalDeposit::get();
pub const ForeignAssetsAssetsStringLimit: u32 = AssetsStringLimit::get();
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/Cargo.toml b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/Cargo.toml
index b18e8affb6541..33155ebab6512 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/Cargo.toml
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/Cargo.toml
@@ -118,6 +118,7 @@ snowbridge-pallet-ethereum-client = { workspace = true }
snowbridge-pallet-inbound-queue = { workspace = true }
snowbridge-pallet-outbound-queue = { workspace = true }
snowbridge-pallet-system = { workspace = true }
+snowbridge-router-primitives = { workspace = true }
snowbridge-runtime-common = { workspace = true }
snowbridge-system-runtime-api = { workspace = true }
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs
index eea61e217c6ba..ff4d860fcd092 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs
@@ -24,7 +24,7 @@ use crate::{
use parachains_common::{AccountId, Balance};
use snowbridge_beacon_primitives::{Fork, ForkVersions};
use snowbridge_core::{gwei, meth, AllowSiblingsOnly, PricingParameters, Rewards};
-use snowbridge_inbound_queue_primitives::v1::MessageToXcm;
+use snowbridge_router_primitives::inbound::MessageToXcm;
use snowbridge_outbound_queue_primitives::v1::EthereumBlobExporter;
use sp_core::H160;
@@ -37,7 +37,6 @@ use testnet_parachains_constants::rococo::{
use crate::xcm_config::RelayNetwork;
#[cfg(feature = "runtime-benchmarks")]
use benchmark_helpers::DoNothingRouter;
-use bp_asset_hub_rococo::CreateForeignAssetDeposit;
use frame_support::{parameter_types, weights::ConstantMultiplier};
use hex_literal::hex;
use pallet_xcm::EnsureXcm;
@@ -63,6 +62,7 @@ parameter_types! {
parameter_types! {
pub const CreateAssetCall: [u8;2] = [53, 0];
+ pub const CreateAssetDeposit: u128 = (UNITS / 10) + EXISTENTIAL_DEPOSIT;
pub Parameters: PricingParameters = PricingParameters {
exchange_rate: FixedU128::from_rational(1, 400),
fee_per_gas: gwei(20),
@@ -87,7 +87,7 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime {
type Helper = Runtime;
type MessageConverter = MessageToXcm<
CreateAssetCall,
- CreateForeignAssetDeposit,
+ CreateAssetDeposit,
ConstU8,
AccountId,
Balance,
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml
index 62b4b487e1c07..278562c38d9b1 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml
@@ -123,6 +123,7 @@ snowbridge-pallet-outbound-queue = { workspace = true }
snowbridge-pallet-outbound-queue-v2 = { workspace = true }
snowbridge-pallet-system = { workspace = true }
snowbridge-pallet-system-v2 = { workspace = true }
+snowbridge-router-primitives = { workspace = true }
snowbridge-runtime-common = { workspace = true }
snowbridge-system-runtime-api = { workspace = true }
snowbridge-system-v2-runtime-api = { workspace = true }
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs
index 6428b08598ae4..869e08917fae3 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs
@@ -21,12 +21,12 @@ use crate::{
Balances, BridgeRelayers, EthereumInboundQueue, EthereumOutboundQueue, EthereumOutboundQueueV2,
EthereumSystem, EthereumSystemV2, MessageQueue, Runtime, RuntimeEvent, TransactionByteFee,
};
-use bp_asset_hub_westend::CreateForeignAssetDeposit;
use frame_support::{parameter_types, traits::Contains, weights::ConstantMultiplier};
use frame_system::EnsureRootWithSuccess;
use pallet_xcm::EnsureXcm;
use parachains_common::{AccountId, Balance};
use snowbridge_beacon_primitives::{Fork, ForkVersions};
+use snowbridge_router_primitives::inbound::MessageToXcm;
use snowbridge_core::{gwei, meth, AllowSiblingsOnly, PricingParameters, Rewards};
use snowbridge_outbound_queue_primitives::{
v1::{ConstantGasMeter, EthereumBlobExporter},
@@ -77,6 +77,7 @@ parameter_types! {
parameter_types! {
pub const CreateAssetCall: [u8;2] = [53, 0];
+ pub const CreateAssetDeposit: u128 = (UNITS / 10) + EXISTENTIAL_DEPOSIT;
pub Parameters: PricingParameters = PricingParameters {
exchange_rate: FixedU128::from_rational(1, 400),
fee_per_gas: gwei(20),
@@ -104,9 +105,9 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime {
type GatewayAddress = EthereumGatewayAddress;
#[cfg(feature = "runtime-benchmarks")]
type Helper = Runtime;
- type MessageConverter = snowbridge_inbound_queue_primitives::v1::MessageToXcm<
+ type MessageConverter = MessageToXcm<
CreateAssetCall,
- CreateForeignAssetDeposit,
+ CreateAssetDeposit,
ConstU8,
AccountId,
Balance,
@@ -139,9 +140,9 @@ impl snowbridge_pallet_inbound_queue_v2::Config for Runtime {
type Token = Balances;
type Balance = Balance;
type WeightToFee = WeightToFee;
- type MessageConverter = snowbridge_inbound_queue_primitives::v2::MessageToXcm<
+ type MessageConverter = MessageToXcm<
CreateAssetCall,
- CreateForeignAssetDeposit,
+ CreateAssetDeposit,
EthereumNetwork,
InboundQueueV2Location,
EthereumSystem,