diff --git a/Cargo.lock b/Cargo.lock
index 0cf0541fa5f7..959f6b0968c3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6566,6 +6566,24 @@ dependencies = [
"sp-std",
]
+[[package]]
+name = "pallet-salary"
+version = "4.0.0-dev"
+source = "git+https://github.com/paritytech/substrate?branch=master#96f8c97dff7f419a349ee01d02613445ba4a41f8"
+dependencies = [
+ "frame-benchmarking",
+ "frame-support",
+ "frame-system",
+ "log",
+ "parity-scale-codec",
+ "scale-info",
+ "sp-arithmetic",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-std",
+]
+
[[package]]
name = "pallet-scheduler"
version = "4.0.0-dev"
@@ -15052,11 +15070,14 @@ dependencies = [
"frame-system",
"impl-trait-for-tuples",
"log",
+ "pallet-assets",
"pallet-balances",
+ "pallet-salary",
"pallet-transaction-payment",
"pallet-xcm",
"parity-scale-codec",
"polkadot-parachain",
+ "polkadot-primitives",
"polkadot-runtime-parachains",
"polkadot-test-runtime",
"primitive-types",
diff --git a/xcm/xcm-builder/Cargo.toml b/xcm/xcm-builder/Cargo.toml
index fec354d0caea..702d5bd7fa06 100644
--- a/xcm/xcm-builder/Cargo.toml
+++ b/xcm/xcm-builder/Cargo.toml
@@ -29,6 +29,9 @@ polkadot-parachain = { path = "../../parachain", default-features = false }
primitive-types = "0.12.1"
pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "master" }
pallet-xcm = { path = "../pallet-xcm" }
+pallet-salary = { git = "https://github.com/paritytech/substrate", branch = "master" }
+pallet-assets = { git = "https://github.com/paritytech/substrate", branch = "master" }
+primitives = { package = "polkadot-primitives", path = "../../primitives" }
polkadot-runtime-parachains = { path = "../../runtime/parachains" }
assert_matches = "1.5.0"
polkadot-test-runtime = { path = "../../runtime/test-runtime" }
diff --git a/xcm/xcm-builder/src/tests/mock.rs b/xcm/xcm-builder/src/tests/mock.rs
index 9be034596f43..0e7b748106ef 100644
--- a/xcm/xcm-builder/src/tests/mock.rs
+++ b/xcm/xcm-builder/src/tests/mock.rs
@@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see .
+//! Mock implementations to test XCM builder configuration types.
+
use crate::{
barriers::{AllowSubscriptionsFrom, RespectSuspension, TrailingSetTopicAsId},
test_utils::*,
@@ -42,7 +44,7 @@ pub use sp_std::{
marker::PhantomData,
};
pub use xcm::latest::{prelude::*, Weight};
-use xcm_executor::traits::Properties;
+use xcm_executor::traits::{Properties, QueryHandler, QueryResponseStatus};
pub use xcm_executor::{
traits::{
AssetExchange, AssetLock, CheckSuspension, ConvertOrigin, Enact, ExportXcm, FeeManager,
@@ -410,6 +412,63 @@ pub fn response(query_id: u64) -> Option {
})
}
+/// Mock implementation of the [`QueryHandler`] trait for creating XCM success queries and expecting
+/// responses.
+pub struct TestQueryHandler(core::marker::PhantomData<(T, BlockNumber)>);
+impl QueryHandler
+ for TestQueryHandler
+{
+ type QueryId = u64;
+ type BlockNumber = BlockNumber;
+ type Error = XcmError;
+ type UniversalLocation = T::UniversalLocation;
+
+ fn new_query(
+ responder: impl Into,
+ _timeout: Self::BlockNumber,
+ _match_querier: impl Into,
+ ) -> Self::QueryId {
+ let query_id = 1;
+ expect_response(query_id, responder.into());
+ query_id
+ }
+
+ fn report_outcome(
+ message: &mut Xcm<()>,
+ responder: impl Into,
+ timeout: Self::BlockNumber,
+ ) -> Result {
+ let responder = responder.into();
+ let destination = Self::UniversalLocation::get()
+ .invert_target(&responder)
+ .map_err(|()| XcmError::LocationNotInvertible)?;
+ let query_id = Self::new_query(responder, timeout, Here);
+ let response_info = QueryResponseInfo { destination, query_id, max_weight: Weight::zero() };
+ let report_error = Xcm(vec![ReportError(response_info)]);
+ message.0.insert(0, SetAppendix(report_error));
+ Ok(query_id)
+ }
+
+ fn take_response(query_id: Self::QueryId) -> QueryResponseStatus {
+ QUERIES
+ .with(|q| {
+ q.borrow().get(&query_id).and_then(|v| match v {
+ ResponseSlot::Received(r) => Some(QueryResponseStatus::Ready {
+ response: r.clone(),
+ at: Self::BlockNumber::zero(),
+ }),
+ _ => Some(QueryResponseStatus::NotFound),
+ })
+ })
+ .unwrap_or(QueryResponseStatus::NotFound)
+ }
+
+ #[cfg(feature = "runtime-benchmarks")]
+ fn expect_response(_id: Self::QueryId, _response: xcm::latest::Response) {
+ // Unnecessary since it's only a test implementation
+ }
+}
+
parameter_types! {
pub static ExecutorUniversalLocation: InteriorMultiLocation
= (ByGenesis([0; 32]), Parachain(42)).into();
diff --git a/xcm/xcm-builder/src/tests/mod.rs b/xcm/xcm-builder/src/tests/mod.rs
index 6daf1872f055..e11caf6282be 100644
--- a/xcm/xcm-builder/src/tests/mod.rs
+++ b/xcm/xcm-builder/src/tests/mod.rs
@@ -34,6 +34,7 @@ mod bridging;
mod expecting;
mod locking;
mod origins;
+mod pay;
mod querying;
mod transacting;
mod version_subscriptions;
diff --git a/xcm/xcm-builder/src/tests/pay/mock.rs b/xcm/xcm-builder/src/tests/pay/mock.rs
new file mode 100644
index 000000000000..3231611d3dd8
--- /dev/null
+++ b/xcm/xcm-builder/src/tests/pay/mock.rs
@@ -0,0 +1,334 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot. If not, see .
+
+use super::*;
+
+use frame_support::traits::{AsEnsureOriginWithArg, Nothing};
+
+use frame_support::derive_impl;
+
+use frame_support::{
+ construct_runtime, parameter_types,
+ traits::{ConstU32, Everything},
+};
+use frame_system::{EnsureRoot, EnsureSigned};
+use polkadot_test_runtime::SignedExtra;
+use primitives::{AccountIndex, BlakeTwo256, Signature};
+use sp_runtime::{generic, traits::MaybeEquivalence, AccountId32, BuildStorage};
+use xcm_executor::{traits::ConvertLocation, XcmExecutor};
+
+pub type Address = sp_runtime::MultiAddress;
+pub type UncheckedExtrinsic =
+ generic::UncheckedExtrinsic;
+pub type Header = generic::Header;
+pub type Block = generic::Block;
+
+pub type BlockNumber = u32;
+pub type AccountId = AccountId32;
+
+construct_runtime!(
+ pub struct Test {
+ System: frame_system,
+ Balances: pallet_balances,
+ Assets: pallet_assets,
+ Salary: pallet_salary,
+ XcmPallet: pallet_xcm,
+ }
+);
+
+parameter_types! {
+ pub const BlockHashCount: BlockNumber = 250;
+}
+
+#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)]
+impl frame_system::Config for Test {
+ type Block = Block;
+ type BlockHashCount = BlockHashCount;
+ type BaseCallFilter = frame_support::traits::Everything;
+ type RuntimeOrigin = RuntimeOrigin;
+ type RuntimeCall = RuntimeCall;
+ type RuntimeEvent = RuntimeEvent;
+ type PalletInfo = PalletInfo;
+ type OnSetCode = ();
+ type AccountData = pallet_balances::AccountData;
+ type AccountId = AccountId;
+ type Lookup = sp_runtime::traits::IdentityLookup;
+}
+
+pub type Balance = u128;
+
+parameter_types! {
+ pub const ExistentialDeposit: Balance = 1;
+}
+
+impl pallet_balances::Config for Test {
+ type MaxLocks = ConstU32<0>;
+ type Balance = Balance;
+ type RuntimeEvent = RuntimeEvent;
+ type DustRemoval = ();
+ type ExistentialDeposit = ExistentialDeposit;
+ type AccountStore = System;
+ type WeightInfo = ();
+ type MaxReserves = ();
+ type ReserveIdentifier = [u8; 8];
+ type RuntimeHoldReason = RuntimeHoldReason;
+ type FreezeIdentifier = ();
+ type MaxHolds = ConstU32<0>;
+ type MaxFreezes = ConstU32<0>;
+}
+
+parameter_types! {
+ pub const AssetDeposit: u128 = 1_000_000;
+ pub const MetadataDepositBase: u128 = 1_000_000;
+ pub const MetadataDepositPerByte: u128 = 100_000;
+ pub const AssetAccountDeposit: u128 = 1_000_000;
+ pub const ApprovalDeposit: u128 = 1_000_000;
+ pub const AssetsStringLimit: u32 = 50;
+ pub const RemoveItemsLimit: u32 = 50;
+}
+
+impl pallet_assets::Config for Test {
+ type RuntimeEvent = RuntimeEvent;
+ type Balance = Balance;
+ type AssetId = AssetIdForAssets;
+ type Currency = Balances;
+ type CreateOrigin = AsEnsureOriginWithArg>;
+ type ForceOrigin = EnsureRoot;
+ type AssetDeposit = AssetDeposit;
+ type MetadataDepositBase = MetadataDepositBase;
+ type MetadataDepositPerByte = MetadataDepositPerByte;
+ type AssetAccountDeposit = AssetAccountDeposit;
+ type ApprovalDeposit = ApprovalDeposit;
+ type StringLimit = AssetsStringLimit;
+ type Freezer = ();
+ type Extra = ();
+ type WeightInfo = ();
+ type RemoveItemsLimit = RemoveItemsLimit;
+ type AssetIdParameter = AssetIdForAssets;
+ type CallbackHandle = ();
+}
+
+parameter_types! {
+ pub const RelayLocation: MultiLocation = Here.into_location();
+ pub const AnyNetwork: Option = None;
+ pub UniversalLocation: InteriorMultiLocation = (ByGenesis([0; 32]), Parachain(42)).into();
+ pub UnitWeightCost: u64 = 1_000;
+ pub static AdvertisedXcmVersion: u32 = 3;
+ pub const BaseXcmWeight: Weight = Weight::from_parts(1_000, 1_000);
+ pub CurrencyPerSecondPerByte: (AssetId, u128, u128) = (Concrete(RelayLocation::get()), 1, 1);
+ pub TrustedAssets: (MultiAssetFilter, MultiLocation) = (All.into(), Here.into());
+ pub const MaxInstructions: u32 = 100;
+ pub const MaxAssetsIntoHolding: u32 = 64;
+ pub CheckingAccount: AccountId = XcmPallet::check_account();
+}
+
+type AssetIdForAssets = u128;
+
+pub struct FromMultiLocationToAsset(
+ core::marker::PhantomData<(MultiLocation, AssetId)>,
+);
+impl MaybeEquivalence
+ for FromMultiLocationToAsset
+{
+ fn convert(value: &MultiLocation) -> Option {
+ match value {
+ MultiLocation { parents: 0, interior: Here } => Some(0 as AssetIdForAssets),
+ MultiLocation { parents: 1, interior: Here } => Some(1 as AssetIdForAssets),
+ MultiLocation { parents: 0, interior: X2(PalletInstance(1), GeneralIndex(index)) }
+ if ![0, 1].contains(index) =>
+ Some(*index as AssetIdForAssets),
+ _ => None,
+ }
+ }
+
+ fn convert_back(value: &AssetIdForAssets) -> Option {
+ match value {
+ 0u128 => Some(MultiLocation { parents: 1, interior: Here }),
+ para_id @ 1..=1000 =>
+ Some(MultiLocation { parents: 1, interior: X1(Parachain(*para_id as u32)) }),
+ _ => None,
+ }
+ }
+}
+
+pub type LocalOriginToLocation = SignedToAccountId32;
+pub type LocalAssetsTransactor = FungiblesAdapter<
+ Assets,
+ ConvertedConcreteId<
+ AssetIdForAssets,
+ Balance,
+ FromMultiLocationToAsset,
+ JustTry,
+ >,
+ SovereignAccountOf,
+ AccountId,
+ NoChecking,
+ CheckingAccount,
+>;
+
+type OriginConverter = (
+ pallet_xcm::XcmPassthrough,
+ SignedAccountId32AsNative,
+);
+type Barrier = AllowUnpaidExecutionFrom;
+
+pub struct DummyWeightTrader;
+impl WeightTrader for DummyWeightTrader {
+ fn new() -> Self {
+ DummyWeightTrader
+ }
+
+ fn buy_weight(
+ &mut self,
+ _weight: Weight,
+ _payment: xcm_executor::Assets,
+ _context: &XcmContext,
+ ) -> Result {
+ Ok(xcm_executor::Assets::default())
+ }
+}
+
+pub struct XcmConfig;
+impl xcm_executor::Config for XcmConfig {
+ type RuntimeCall = RuntimeCall;
+ type XcmSender = TestMessageSender;
+ type AssetTransactor = LocalAssetsTransactor;
+ type OriginConverter = OriginConverter;
+ type IsReserve = ();
+ type IsTeleporter = ();
+ type UniversalLocation = UniversalLocation;
+ type Barrier = Barrier;
+ type Weigher = FixedWeightBounds;
+ type Trader = DummyWeightTrader;
+ type ResponseHandler = XcmPallet;
+ type AssetTrap = XcmPallet;
+ type AssetLocker = ();
+ type AssetExchanger = ();
+ type AssetClaims = XcmPallet;
+ type SubscriptionService = XcmPallet;
+ type PalletInstancesInfo = ();
+ type MaxAssetsIntoHolding = MaxAssetsIntoHolding;
+ type FeeManager = ();
+ type MessageExporter = ();
+ type UniversalAliases = Nothing;
+ type CallDispatcher = RuntimeCall;
+ type SafeCallFilter = Everything;
+ type Aliasers = Nothing;
+}
+
+parameter_types! {
+ pub TreasuryAccountId: AccountId = AccountId::new([42u8; 32]);
+}
+
+pub struct TreasuryToAccount;
+impl ConvertLocation for TreasuryToAccount {
+ fn convert_location(location: &MultiLocation) -> Option {
+ match location {
+ MultiLocation {
+ parents: 1,
+ interior:
+ X2(Parachain(42), Plurality { id: BodyId::Treasury, part: BodyPart::Voice }),
+ } => Some(TreasuryAccountId::get()), // Hardcoded test treasury account id
+ _ => None,
+ }
+ }
+}
+
+type SovereignAccountOf = (
+ AccountId32Aliases,
+ TreasuryToAccount,
+ HashedDescription>,
+);
+
+#[cfg(feature = "runtime-benchmarks")]
+parameter_types! {
+ pub ReachableDest: Option = Some(Parachain(1000).into());
+}
+
+impl pallet_xcm::Config for Test {
+ type RuntimeEvent = RuntimeEvent;
+ type SendXcmOrigin = EnsureXcmOrigin;
+ type XcmRouter = TestMessageSender;
+ type ExecuteXcmOrigin = EnsureXcmOrigin;
+ type XcmExecuteFilter = Everything;
+ type XcmExecutor = XcmExecutor;
+ type XcmTeleportFilter = Everything;
+ type XcmReserveTransferFilter = Everything;
+ type Weigher = FixedWeightBounds;
+ type UniversalLocation = UniversalLocation;
+ type RuntimeOrigin = RuntimeOrigin;
+ type RuntimeCall = RuntimeCall;
+ const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100;
+ type AdvertisedXcmVersion = AdvertisedXcmVersion;
+ type TrustedLockers = ();
+ type SovereignAccountOf = SovereignAccountOf;
+ type Currency = Balances;
+ type CurrencyMatcher = IsConcrete;
+ type MaxLockers = frame_support::traits::ConstU32<8>;
+ type MaxRemoteLockConsumers = frame_support::traits::ConstU32<0>;
+ type RemoteLockConsumerIdentifier = ();
+ type WeightInfo = pallet_xcm::TestWeightInfo;
+ #[cfg(feature = "runtime-benchmarks")]
+ type ReachableDest = ReachableDest;
+ type AdminOrigin = EnsureRoot;
+}
+
+pub const UNITS: Balance = 1_000_000_000_000;
+pub const INITIAL_BALANCE: Balance = 100 * UNITS;
+pub const MINIMUM_BALANCE: Balance = 1 * UNITS;
+
+pub fn sibling_chain_account_id(para_id: u32, account: [u8; 32]) -> AccountId {
+ let location: MultiLocation =
+ (Parent, Parachain(para_id), Junction::AccountId32 { id: account, network: None }).into();
+ SovereignAccountOf::convert_location(&location).unwrap()
+}
+
+pub fn new_test_ext() -> sp_io::TestExternalities {
+ let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap();
+ let admin_account: AccountId = AccountId::new([0u8; 32]);
+ pallet_assets::GenesisConfig:: {
+ assets: vec![
+ (0, admin_account.clone(), true, MINIMUM_BALANCE),
+ (1, admin_account.clone(), true, MINIMUM_BALANCE),
+ (100, admin_account.clone(), true, MINIMUM_BALANCE),
+ ],
+ metadata: vec![
+ (0, "Native token".encode(), "NTV".encode(), 12),
+ (1, "Relay token".encode(), "RLY".encode(), 12),
+ (100, "Test token".encode(), "TST".encode(), 12),
+ ],
+ accounts: vec![
+ (0, sibling_chain_account_id(42, [3u8; 32]), INITIAL_BALANCE),
+ (1, TreasuryAccountId::get(), INITIAL_BALANCE),
+ (100, TreasuryAccountId::get(), INITIAL_BALANCE),
+ ],
+ }
+ .assimilate_storage(&mut t)
+ .unwrap();
+ let mut ext = sp_io::TestExternalities::new(t);
+ ext.execute_with(|| System::set_block_number(1));
+ ext
+}
+
+pub fn next_block() {
+ System::set_block_number(System::block_number() + 1);
+}
+
+pub fn run_to(block_number: BlockNumber) {
+ while System::block_number() < block_number {
+ next_block();
+ }
+}
diff --git a/xcm/xcm-builder/src/tests/pay/mod.rs b/xcm/xcm-builder/src/tests/pay/mod.rs
new file mode 100644
index 000000000000..8adf1ad2f5e1
--- /dev/null
+++ b/xcm/xcm-builder/src/tests/pay/mod.rs
@@ -0,0 +1,21 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot. If not, see .
+
+use super::*;
+
+mod mock;
+mod pay;
+mod salary;
diff --git a/xcm/xcm-builder/src/tests/pay/pay.rs b/xcm/xcm-builder/src/tests/pay/pay.rs
new file mode 100644
index 000000000000..28b2feec0c23
--- /dev/null
+++ b/xcm/xcm-builder/src/tests/pay/pay.rs
@@ -0,0 +1,158 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot. If not, see .
+
+//! Tests for making sure `PayOverXcm::pay` generates the correct message and sends it to the
+//! correct destination
+
+use super::{mock::*, *};
+use frame_support::{assert_ok, traits::tokens::Pay};
+
+/// Type representing both a location and an asset that is held at that location.
+/// The id of the held asset is relative to the location where it is being held.
+#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq)]
+pub struct AssetKind {
+ destination: MultiLocation,
+ asset_id: AssetId,
+}
+
+pub struct LocatableAssetKindConverter;
+impl sp_runtime::traits::Convert for LocatableAssetKindConverter {
+ fn convert(value: AssetKind) -> LocatableAssetId {
+ LocatableAssetId { asset_id: value.asset_id, location: value.destination }
+ }
+}
+
+parameter_types! {
+ pub SenderAccount: AccountId = AccountId::new([3u8; 32]);
+ pub InteriorAccount: InteriorMultiLocation = AccountId32 { id: SenderAccount::get().into(), network: None }.into();
+ pub InteriorBody: InteriorMultiLocation = Plurality { id: BodyId::Treasury, part: BodyPart::Voice }.into();
+ pub Timeout: BlockNumber = 5; // 5 blocks
+}
+
+/// Scenario:
+/// Account #3 on the local chain, parachain 42, controls an amount of funds on parachain 2.
+/// [`PayOverXcm::pay`] creates the correct message for account #3 to pay another account, account
+/// #5, on parachain 2, remotely, in its native token.
+#[test]
+fn pay_over_xcm_works() {
+ let recipient = AccountId::new([5u8; 32]);
+ let asset_kind =
+ AssetKind { destination: (Parent, Parachain(2)).into(), asset_id: Here.into() };
+ let amount = 10 * UNITS;
+
+ new_test_ext().execute_with(|| {
+ // Check starting balance
+ assert_eq!(mock::Assets::balance(0, &recipient), 0);
+
+ assert_ok!(PayOverXcm::<
+ InteriorAccount,
+ TestMessageSender,
+ TestQueryHandler,
+ Timeout,
+ AccountId,
+ AssetKind,
+ LocatableAssetKindConverter,
+ AliasesIntoAccountId32,
+ >::pay(&recipient, asset_kind, amount));
+
+ let expected_message = Xcm(vec![
+ DescendOrigin(AccountId32 { id: SenderAccount::get().into(), network: None }.into()),
+ UnpaidExecution { weight_limit: Unlimited, check_origin: None },
+ SetAppendix(Xcm(vec![ReportError(QueryResponseInfo {
+ destination: (Parent, Parachain(42)).into(),
+ query_id: 1,
+ max_weight: Weight::zero(),
+ })])),
+ TransferAsset {
+ assets: (Here, amount).into(),
+ beneficiary: AccountId32 { id: recipient.clone().into(), network: None }.into(),
+ },
+ ]);
+ let expected_hash = fake_message_hash(&expected_message);
+
+ assert_eq!(
+ sent_xcm(),
+ vec![((Parent, Parachain(2)).into(), expected_message, expected_hash)]
+ );
+
+ let (_, message, hash) = sent_xcm()[0].clone();
+ let message =
+ Xcm::<::RuntimeCall>::from(message.clone());
+
+ // Execute message in parachain 2 with parachain 42's origin
+ let origin = (Parent, Parachain(42));
+ XcmExecutor::::execute_xcm(origin, message, hash, Weight::MAX);
+ assert_eq!(mock::Assets::balance(0, &recipient), amount);
+ });
+}
+
+/// Scenario:
+/// A pluralistic body, a Treasury, on the local chain, parachain 42, controls an amount of funds
+/// on parachain 2. [`PayOverXcm::pay`] creates the correct message for the treasury to pay
+/// another account, account #7, on parachain 2, remotely, in the relay's token.
+#[test]
+fn pay_over_xcm_governance_body() {
+ let recipient = AccountId::new([7u8; 32]);
+ let asset_kind =
+ AssetKind { destination: (Parent, Parachain(2)).into(), asset_id: Parent.into() };
+ let amount = 10 * UNITS;
+
+ let relay_asset_index = 1;
+
+ new_test_ext().execute_with(|| {
+ // Check starting balance
+ assert_eq!(mock::Assets::balance(relay_asset_index, &recipient), 0);
+
+ assert_ok!(PayOverXcm::<
+ InteriorBody,
+ TestMessageSender,
+ TestQueryHandler,
+ Timeout,
+ AccountId,
+ AssetKind,
+ LocatableAssetKindConverter,
+ AliasesIntoAccountId32,
+ >::pay(&recipient, asset_kind, amount));
+
+ let expected_message = Xcm(vec![
+ DescendOrigin(Plurality { id: BodyId::Treasury, part: BodyPart::Voice }.into()),
+ UnpaidExecution { weight_limit: Unlimited, check_origin: None },
+ SetAppendix(Xcm(vec![ReportError(QueryResponseInfo {
+ destination: (Parent, Parachain(42)).into(),
+ query_id: 1,
+ max_weight: Weight::zero(),
+ })])),
+ TransferAsset {
+ assets: (Parent, amount).into(),
+ beneficiary: AccountId32 { id: recipient.clone().into(), network: None }.into(),
+ },
+ ]);
+ let expected_hash = fake_message_hash(&expected_message);
+ assert_eq!(
+ sent_xcm(),
+ vec![((Parent, Parachain(2)).into(), expected_message, expected_hash)]
+ );
+
+ let (_, message, hash) = sent_xcm()[0].clone();
+ let message =
+ Xcm::<::RuntimeCall>::from(message.clone());
+
+ // Execute message in parachain 2 with parachain 42's origin
+ let origin = (Parent, Parachain(42));
+ XcmExecutor::::execute_xcm(origin, message, hash, Weight::MAX);
+ assert_eq!(mock::Assets::balance(relay_asset_index, &recipient), amount);
+ });
+}
diff --git a/xcm/xcm-builder/src/tests/pay/salary.rs b/xcm/xcm-builder/src/tests/pay/salary.rs
new file mode 100644
index 000000000000..1d40345f302a
--- /dev/null
+++ b/xcm/xcm-builder/src/tests/pay/salary.rs
@@ -0,0 +1,172 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot. If not, see .
+
+//! Tests for the integration between `PayOverXcm` and the salary pallet
+
+use super::{mock::*, *};
+
+use frame_support::{
+ assert_ok,
+ traits::{tokens::GetSalary, RankedMembers},
+};
+use sp_runtime::{traits::ConvertToValue, DispatchResult};
+
+parameter_types! {
+ pub Interior: InteriorMultiLocation = Plurality { id: BodyId::Treasury, part: BodyPart::Voice }.into();
+ pub Timeout: BlockNumber = 5;
+ pub AssetHub: MultiLocation = (Parent, Parachain(1)).into();
+ pub AssetIdGeneralIndex: u128 = 100;
+ pub AssetHubAssetId: AssetId = (PalletInstance(1), GeneralIndex(AssetIdGeneralIndex::get())).into();
+ pub LocatableAsset: LocatableAssetId = LocatableAssetId { asset_id: AssetHubAssetId::get(), location: AssetHub::get() };
+}
+
+type SalaryPayOverXcm = PayOverXcm<
+ Interior,
+ TestMessageSender,
+ TestQueryHandler,
+ Timeout,
+ AccountId,
+ (),
+ ConvertToValue,
+ AliasesIntoAccountId32,
+>;
+
+type Rank = u128;
+
+thread_local! {
+ pub static CLUB: RefCell> = RefCell::new(BTreeMap::new());
+}
+
+pub struct TestClub;
+impl RankedMembers for TestClub {
+ type AccountId = AccountId;
+ type Rank = Rank;
+
+ fn min_rank() -> Self::Rank {
+ 0
+ }
+ fn rank_of(who: &Self::AccountId) -> Option {
+ CLUB.with(|club| club.borrow().get(who).cloned())
+ }
+ fn induct(who: &Self::AccountId) -> DispatchResult {
+ CLUB.with(|club| club.borrow_mut().insert(who.clone(), 0));
+ Ok(())
+ }
+ fn promote(who: &Self::AccountId) -> DispatchResult {
+ CLUB.with(|club| {
+ club.borrow_mut().entry(who.clone()).and_modify(|rank| *rank += 1);
+ });
+ Ok(())
+ }
+ fn demote(who: &Self::AccountId) -> DispatchResult {
+ CLUB.with(|club| match club.borrow().get(who) {
+ None => Err(sp_runtime::DispatchError::Unavailable),
+ Some(&0) => {
+ club.borrow_mut().remove(&who);
+ Ok(())
+ },
+ Some(_) => {
+ club.borrow_mut().entry(who.clone()).and_modify(|rank| *rank += 1);
+ Ok(())
+ },
+ })
+ }
+}
+
+fn set_rank(who: AccountId, rank: u128) {
+ CLUB.with(|club| club.borrow_mut().insert(who, rank));
+}
+
+parameter_types! {
+ pub const RegistrationPeriod: BlockNumber = 2;
+ pub const PayoutPeriod: BlockNumber = 2;
+ pub const FixedSalaryAmount: Balance = 10 * UNITS;
+ pub static Budget: Balance = FixedSalaryAmount::get();
+}
+
+pub struct FixedSalary;
+impl GetSalary for FixedSalary {
+ fn get_salary(_rank: Rank, _who: &AccountId) -> Balance {
+ FixedSalaryAmount::get()
+ }
+}
+
+impl pallet_salary::Config for Test {
+ type WeightInfo = ();
+ type RuntimeEvent = RuntimeEvent;
+ type Paymaster = SalaryPayOverXcm;
+ type Members = TestClub;
+ type Salary = FixedSalary;
+ type RegistrationPeriod = RegistrationPeriod;
+ type PayoutPeriod = PayoutPeriod;
+ type Budget = Budget;
+}
+
+/// Scenario:
+/// The salary pallet is used to pay a member over XCM.
+/// The correct XCM message is generated and when executed in the remote chain,
+/// the member receives the salary.
+#[test]
+fn salary_pay_over_xcm_works() {
+ let recipient = AccountId::new([1u8; 32]);
+
+ new_test_ext().execute_with(|| {
+ // Set the recipient as a member of a ranked collective
+ set_rank(recipient.clone(), 1);
+
+ // Check starting balance
+ assert_eq!(mock::Assets::balance(AssetIdGeneralIndex::get(), &recipient.clone()), 0);
+
+ // Use salary pallet to call `PayOverXcm::pay`
+ assert_ok!(Salary::init(RuntimeOrigin::signed(recipient.clone())));
+ run_to(5);
+ assert_ok!(Salary::induct(RuntimeOrigin::signed(recipient.clone())));
+ assert_ok!(Salary::bump(RuntimeOrigin::signed(recipient.clone())));
+ assert_ok!(Salary::register(RuntimeOrigin::signed(recipient.clone())));
+ run_to(7);
+ assert_ok!(Salary::payout(RuntimeOrigin::signed(recipient.clone())));
+
+ // Get message from mock transport layer
+ let (_, message, hash) = sent_xcm()[0].clone();
+ // Change type from `Xcm<()>` to `Xcm` to be able to execute later
+ let message =
+ Xcm::<::RuntimeCall>::from(message.clone());
+
+ let expected_message: Xcm = Xcm::(vec![
+ DescendOrigin(Plurality { id: BodyId::Treasury, part: BodyPart::Voice }.into()),
+ UnpaidExecution { weight_limit: Unlimited, check_origin: None },
+ SetAppendix(Xcm(vec![ReportError(QueryResponseInfo {
+ destination: (Parent, Parachain(42)).into(),
+ query_id: 1,
+ max_weight: Weight::zero(),
+ })])),
+ TransferAsset {
+ assets: (AssetHubAssetId::get(), FixedSalaryAmount::get()).into(),
+ beneficiary: AccountId32 { id: recipient.clone().into(), network: None }.into(),
+ },
+ ]);
+ assert_eq!(message, expected_message);
+
+ // Execute message as the asset hub
+ XcmExecutor::::execute_xcm((Parent, Parachain(42)), message, hash, Weight::MAX);
+
+ // Recipient receives the payment
+ assert_eq!(
+ mock::Assets::balance(AssetIdGeneralIndex::get(), &recipient),
+ FixedSalaryAmount::get()
+ );
+ });
+}