Skip to main content

parachains_common/
pay.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Cumulus.
3// SPDX-License-Identifier: Apache-2.0
4
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// 	http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
18use frame_support::traits::{
19	fungibles,
20	tokens::{PayWithSource, PaymentStatus, Preservation},
21};
22use polkadot_runtime_common::impls::VersionedLocatableAsset;
23use sp_runtime::{traits::TypedGet, DispatchError};
24use xcm::latest::prelude::*;
25use xcm_executor::traits::ConvertLocation;
26
27/// Versioned locatable account type which contains both an XCM `location` and `account_id` to
28/// identify an account which exists on some chain.
29#[derive(
30	Encode,
31	Decode,
32	Eq,
33	PartialEq,
34	Clone,
35	Debug,
36	scale_info::TypeInfo,
37	MaxEncodedLen,
38	DecodeWithMemTracking,
39)]
40pub enum VersionedLocatableAccount {
41	#[codec(index = 4)]
42	V4 { location: xcm::v4::Location, account_id: xcm::v4::Location },
43	#[codec(index = 5)]
44	V5 { location: xcm::v5::Location, account_id: xcm::v5::Location },
45}
46
47// Implement Convert trait for use with pallet_multi_asset_bounties
48/// Converter from `AccountId32` to `VersionedLocatableAccount` for use with
49/// `pallet_multi_asset_bounties`.
50///
51/// # Example
52///
53/// ,ignore
54/// type FundingSource = PalletIdAsFundingSource<
55///     TreasuryPalletId,
56///     Runtime,
57///     AccountIdToLocalLocation
58/// >;
59/// ///
60/// # Warning
61/// This conversion fills in default values (location = "here", network = None) which may
62/// be incorrect if the account is from another chain or network.
63pub struct AccountIdToLocalLocation;
64
65impl sp_runtime::traits::Convert<sp_runtime::AccountId32, VersionedLocatableAccount>
66	for AccountIdToLocalLocation
67{
68	/// Convert a local account ID into a `VersionedLocatableAccount`.
69	///
70	/// This assumes the account is on the local chain (`Location::here()`) and has no network
71	/// specification (`network: None`). Only use this when you're certain the account is local.
72	///
73	/// # Warning
74	/// This conversion fills in default values (location = "here", network = None) which may
75	/// be incorrect if the account is from another chain or network.
76	fn convert(account_id: sp_runtime::AccountId32) -> VersionedLocatableAccount {
77		VersionedLocatableAccount::V5 {
78			location: Location::here(),
79			account_id: Location::new(
80				0,
81				[xcm::v5::Junction::AccountId32 { network: None, id: account_id.into() }],
82			),
83		}
84	}
85}
86
87/// Pay on the local chain with `fungibles` implementation if the beneficiary and the asset are both
88/// local.
89pub struct LocalPay<F, A, C>(core::marker::PhantomData<(F, A, C)>);
90impl<A, F, C> frame_support::traits::tokens::Pay for LocalPay<F, A, C>
91where
92	A: TypedGet,
93	F: fungibles::Mutate<A::Type, AssetId = xcm::v5::Location> + fungibles::Create<A::Type>,
94	C: ConvertLocation<A::Type>,
95	A::Type: Eq + Clone,
96{
97	type Balance = F::Balance;
98	type Beneficiary = VersionedLocatableAccount;
99	type AssetKind = VersionedLocatableAsset;
100	type Id = QueryId;
101	type Error = DispatchError;
102	fn pay(
103		who: &Self::Beneficiary,
104		asset: Self::AssetKind,
105		amount: Self::Balance,
106	) -> Result<Self::Id, Self::Error> {
107		let who = Self::match_location::<A::Type>(who).map_err(|_| DispatchError::Unavailable)?;
108		let asset = Self::match_asset(&asset).map_err(|_| DispatchError::Unavailable)?;
109		<F as fungibles::Mutate<_>>::transfer(
110			asset,
111			&A::get(),
112			&who,
113			amount,
114			Preservation::Expendable,
115		)?;
116		// We use `QueryId::MAX` as a constant identifier for these payments since they are always
117		// processed immediately and successfully on the local chain. The `QueryId` type is used to
118		// maintain compatibility with XCM payment implementations.
119		Ok(Self::Id::MAX) // Always returns the same ID, breaks the expectation that payment IDs should be
120		            // unique. See Issue #10450.
121	}
122	fn check_payment(_: Self::Id) -> PaymentStatus {
123		PaymentStatus::Success
124	}
125	#[cfg(feature = "runtime-benchmarks")]
126	fn ensure_successful(_: &Self::Beneficiary, asset: Self::AssetKind, amount: Self::Balance) {
127		let asset = Self::match_asset(&asset).expect("invalid asset");
128		<F as fungibles::Create<_>>::create(asset.clone(), A::get(), true, amount).unwrap();
129		<F as fungibles::Mutate<_>>::mint_into(asset, &A::get(), amount).unwrap();
130	}
131	#[cfg(feature = "runtime-benchmarks")]
132	fn ensure_concluded(_: Self::Id) {}
133}
134
135impl<A, F, C> LocalPay<F, A, C> {
136	fn match_location<T>(who: &VersionedLocatableAccount) -> Result<T, ()>
137	where
138		T: Eq + Clone,
139		C: ConvertLocation<T>,
140	{
141		// only applicable for the local accounts
142		let account_id = match who {
143			VersionedLocatableAccount::V4 { location, account_id } if location.is_here() => {
144				&account_id.clone().try_into().map_err(|_| ())?
145			},
146			VersionedLocatableAccount::V5 { location, account_id } if location.is_here() => {
147				account_id
148			},
149			_ => return Err(()),
150		};
151		C::convert_location(account_id).ok_or(())
152	}
153
154	fn match_asset(asset: &VersionedLocatableAsset) -> Result<xcm::v5::Location, ()> {
155		match asset {
156			VersionedLocatableAsset::V3 { location, asset_id } if location.is_here() => {
157				// Convert V3 asset_id to V5 Location (must go through V4)
158				let v4_asset_id: xcm::v4::AssetId = (*asset_id).try_into().map_err(|_| ())?;
159				let v5_asset_id: xcm::v5::AssetId = v4_asset_id.try_into().map_err(|_| ())?;
160				Ok(v5_asset_id.0)
161			},
162			VersionedLocatableAsset::V4 { location, asset_id } if location.is_here() => {
163				asset_id.clone().try_into().map(|a: xcm::v5::AssetId| a.0).map_err(|_| ())
164			},
165			VersionedLocatableAsset::V5 { location, asset_id } if location.is_here() => {
166				Ok(asset_id.clone().0)
167			},
168			_ => Err(()),
169		}
170	}
171}
172
173// Implement PayWithSource for LocalPay
174impl<A, F, C> PayWithSource for LocalPay<F, A, C>
175where
176	A: Eq + Clone,
177	F: fungibles::Mutate<A, AssetId = xcm::v5::Location> + fungibles::Create<A>,
178	C: ConvertLocation<A>,
179{
180	type Balance = F::Balance;
181	type Source = VersionedLocatableAccount;
182	type Beneficiary = VersionedLocatableAccount;
183	type AssetKind = VersionedLocatableAsset;
184	type Id = QueryId;
185	type Error = DispatchError;
186	fn pay(
187		source: &Self::Source,
188		who: &Self::Beneficiary,
189		asset: Self::AssetKind,
190		amount: Self::Balance,
191	) -> Result<Self::Id, Self::Error> {
192		let source = Self::match_location::<A>(source).map_err(|_| DispatchError::Unavailable)?;
193		let who = Self::match_location::<A>(who).map_err(|_| DispatchError::Unavailable)?;
194		let asset = Self::match_asset(&asset).map_err(|_| DispatchError::Unavailable)?;
195		<F as fungibles::Mutate<_>>::transfer(
196			asset,
197			&source,
198			&who,
199			amount,
200			Preservation::Expendable,
201		)?;
202		// We use `QueryId::MAX` as a constant identifier for these payments since they are always
203		// processed immediately and successfully on the local chain. The `QueryId` type is used to
204		// maintain compatibility with XCM payment implementations.
205		Ok(Self::Id::MAX)
206	}
207	fn check_payment(_: Self::Id) -> PaymentStatus {
208		PaymentStatus::Success
209	}
210	#[cfg(feature = "runtime-benchmarks")]
211	fn ensure_successful(
212		source: &Self::Source,
213		_: &Self::Beneficiary,
214		asset: Self::AssetKind,
215		amount: Self::Balance,
216	) {
217		use sp_runtime::traits::Zero;
218
219		let source = Self::match_location::<A>(source).expect("invalid source");
220		let asset = Self::match_asset(&asset).expect("invalid asset");
221		if F::total_issuance(asset.clone()).is_zero() {
222			<F as fungibles::Create<_>>::create(asset.clone(), source.clone(), true, 1u32.into())
223				.unwrap();
224		}
225		<F as fungibles::Mutate<_>>::mint_into(asset, &source, amount).unwrap();
226	}
227	#[cfg(feature = "runtime-benchmarks")]
228	fn ensure_concluded(_: Self::Id) {}
229}
230
231#[cfg(feature = "runtime-benchmarks")]
232pub mod benchmarks {
233	use super::*;
234	use core::marker::PhantomData;
235	use frame_support::traits::Get;
236	use pallet_multi_asset_bounties::ArgumentsFactory as MultiAssetBountiesArgumentsFactory;
237	use pallet_treasury::ArgumentsFactory as TreasuryArgumentsFactory;
238	use sp_core::ConstU8;
239
240	/// Provides factory methods for the `AssetKind` and the `Beneficiary` that are applicable for
241	/// the payout made by [`LocalPay`].
242	///
243	/// ### Parameters:
244	/// - `PalletId`: The ID of the assets registry pallet.
245	pub struct LocalPayArguments<PalletId = ConstU8<0>>(PhantomData<PalletId>);
246	impl<PalletId: Get<u8>>
247		TreasuryArgumentsFactory<VersionedLocatableAsset, VersionedLocatableAccount>
248		for LocalPayArguments<PalletId>
249	{
250		fn create_asset_kind(seed: u32) -> VersionedLocatableAsset {
251			VersionedLocatableAsset::V5 {
252				location: Location::new(0, []),
253				asset_id: Location::new(
254					0,
255					[PalletInstance(PalletId::get()), GeneralIndex(seed.into())],
256				)
257				.into(),
258			}
259		}
260		fn create_beneficiary(seed: [u8; 32]) -> VersionedLocatableAccount {
261			VersionedLocatableAccount::V5 {
262				location: Location::new(0, []),
263				account_id: Location::new(0, [AccountId32 { network: None, id: seed }]),
264			}
265		}
266	}
267
268	/// Provides factory methods for the `AssetKind`, `Source`, and `Beneficiary` that are
269	/// applicable for the payout made by [`LocalPay`] when used with `PayWithSource`.
270	///
271	/// ### Parameters:
272	/// - `PalletId`: The ID of the assets registry pallet.
273	pub struct LocalPayWithSourceArguments<PalletId = ConstU8<0>>(PhantomData<PalletId>);
274	impl<PalletId: Get<u8>, Balance>
275		MultiAssetBountiesArgumentsFactory<
276			VersionedLocatableAsset,
277			VersionedLocatableAccount,
278			Balance,
279		> for LocalPayWithSourceArguments<PalletId>
280	{
281		fn create_asset_kind(seed: u32) -> VersionedLocatableAsset {
282			VersionedLocatableAsset::V5 {
283				location: Location::new(0, []),
284				asset_id: Location::new(
285					0,
286					[PalletInstance(PalletId::get()), GeneralIndex(seed.into())],
287				)
288				.into(),
289			}
290		}
291		fn create_beneficiary(seed: [u8; 32]) -> VersionedLocatableAccount {
292			VersionedLocatableAccount::V5 {
293				location: Location::new(0, []),
294				account_id: Location::new(0, [AccountId32 { network: None, id: seed }]),
295			}
296		}
297	}
298}