referrerpolicy=no-referrer-when-downgrade

staging_xcm_builder/
nonfungible_adapter.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Polkadot is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Adapters to work with [`frame_support::traits::tokens::nonfungible`] through XCM.
18
19use crate::MintLocation;
20use core::{fmt::Debug, marker::PhantomData};
21use frame_support::{
22	defensive_assert, ensure,
23	traits::{tokens::nonfungible, Get},
24};
25use xcm::latest::prelude::*;
26use xcm_executor::{
27	traits::{ConvertLocation, Error as MatchError, MatchesNonFungible, TransactAsset},
28	AssetsInHolding,
29};
30
31const LOG_TARGET: &str = "xcm::nonfungible_adapter";
32
33/// [`TransactAsset`] implementation that allows the use of a [`nonfungible`] implementation for
34/// handling an asset in the XCM executor.
35/// Only works for transfers.
36pub struct NonFungibleTransferAdapter<NonFungible, Matcher, AccountIdConverter, AccountId>(
37	PhantomData<(NonFungible, Matcher, AccountIdConverter, AccountId)>,
38)
39where
40	NonFungible: nonfungible::Transfer<AccountId>;
41impl<
42		NonFungible: nonfungible::Transfer<AccountId>,
43		Matcher: MatchesNonFungible<NonFungible::ItemId>,
44		AccountIdConverter: ConvertLocation<AccountId>,
45		AccountId: Clone + Debug, // can't get away without it since Currency is generic over it.
46	> TransactAsset for NonFungibleTransferAdapter<NonFungible, Matcher, AccountIdConverter, AccountId>
47where
48	NonFungible::ItemId: Debug,
49{
50	fn transfer_asset(
51		what: &Asset,
52		from: &Location,
53		to: &Location,
54		context: &XcmContext,
55	) -> Result<Asset, XcmError> {
56		tracing::trace!(
57			target: LOG_TARGET,
58			?what,
59			?from,
60			?to,
61			?context,
62			"transfer_asset",
63		);
64		// Check we handle this asset.
65		let instance = Matcher::matches_nonfungible(what).ok_or(MatchError::AssetNotHandled)?;
66		let destination = AccountIdConverter::convert_location(to)
67			.ok_or(MatchError::AccountIdConversionFailed)?;
68		NonFungible::transfer(&instance, &destination).map_err(|e| {
69			tracing::debug!(target: LOG_TARGET, ?e, ?instance, ?destination, "Failed to transfer non-fungible asset");
70			XcmError::FailedToTransactAsset(e.into())
71		})?;
72		Ok(what.clone())
73	}
74}
75
76/// [`TransactAsset`] implementation that allows the use of a [`nonfungible`] implementation for
77/// handling an asset in the XCM executor.
78/// Works for teleport bookkeeping.
79pub struct NonFungibleMutateAdapter<
80	NonFungible,
81	Matcher,
82	AccountIdConverter,
83	AccountId,
84	CheckingAccount,
85>(PhantomData<(NonFungible, Matcher, AccountIdConverter, AccountId, CheckingAccount)>)
86where
87	NonFungible: nonfungible::Mutate<AccountId>,
88	NonFungible::ItemId: Debug;
89
90impl<
91		NonFungible: nonfungible::Mutate<AccountId>,
92		Matcher: MatchesNonFungible<NonFungible::ItemId>,
93		AccountIdConverter: ConvertLocation<AccountId>,
94		AccountId: Clone + Eq + Debug, /* can't get away without it since Currency is generic
95		                                * over it. */
96		CheckingAccount: Get<Option<(AccountId, MintLocation)>>,
97	> NonFungibleMutateAdapter<NonFungible, Matcher, AccountIdConverter, AccountId, CheckingAccount>
98where
99	NonFungible::ItemId: Debug,
100{
101	fn can_accrue_checked(instance: NonFungible::ItemId) -> XcmResult {
102		ensure!(NonFungible::owner(&instance).is_none(), XcmError::NotDepositable);
103		Ok(())
104	}
105	fn can_reduce_checked(checking_account: AccountId, instance: NonFungible::ItemId) -> XcmResult {
106		// This is an asset whose teleports we track.
107		let owner = NonFungible::owner(&instance);
108		ensure!(owner == Some(checking_account), XcmError::NotWithdrawable);
109		ensure!(NonFungible::can_transfer(&instance), XcmError::NotWithdrawable);
110		Ok(())
111	}
112	fn accrue_checked(checking_account: AccountId, instance: NonFungible::ItemId) {
113		let ok = NonFungible::mint_into(&instance, &checking_account).is_ok();
114		debug_assert!(ok, "`mint_into` cannot generally fail; qed");
115	}
116	fn reduce_checked(instance: NonFungible::ItemId) {
117		let ok = NonFungible::burn(&instance, None).is_ok();
118		debug_assert!(ok, "`can_check_in` must have returned `true` immediately prior; qed");
119	}
120}
121
122impl<
123		NonFungible: nonfungible::Mutate<AccountId>,
124		Matcher: MatchesNonFungible<NonFungible::ItemId>,
125		AccountIdConverter: ConvertLocation<AccountId>,
126		AccountId: Clone + Eq + Debug, /* can't get away without it since Currency is generic
127		                                * over it. */
128		CheckingAccount: Get<Option<(AccountId, MintLocation)>>,
129	> TransactAsset
130	for NonFungibleMutateAdapter<NonFungible, Matcher, AccountIdConverter, AccountId, CheckingAccount>
131where
132	NonFungible::ItemId: Debug,
133{
134	fn can_check_in(origin: &Location, what: &Asset, context: &XcmContext) -> XcmResult {
135		tracing::trace!(
136			target: LOG_TARGET,
137			?origin,
138			?what,
139			?context,
140			"can_check_in",
141		);
142		// Check we handle this asset.
143		let instance = Matcher::matches_nonfungible(what).ok_or(MatchError::AssetNotHandled)?;
144		match CheckingAccount::get() {
145			// We track this asset's teleports to ensure no more come in than have gone out.
146			Some((checking_account, MintLocation::Local)) => {
147				Self::can_reduce_checked(checking_account, instance)
148			},
149			// We track this asset's teleports to ensure no more go out than have come in.
150			Some((_, MintLocation::NonLocal)) => Self::can_accrue_checked(instance),
151			_ => Ok(()),
152		}
153	}
154
155	fn check_in(origin: &Location, what: &Asset, context: &XcmContext) {
156		tracing::trace!(
157			target: LOG_TARGET,
158			?origin,
159			?what,
160			?context,
161			"check_in",
162		);
163		if let Some(instance) = Matcher::matches_nonfungible(what) {
164			match CheckingAccount::get() {
165				// We track this asset's teleports to ensure no more come in than have gone out.
166				Some((_, MintLocation::Local)) => Self::reduce_checked(instance),
167				// We track this asset's teleports to ensure no more go out than have come in.
168				Some((checking_account, MintLocation::NonLocal)) => {
169					Self::accrue_checked(checking_account, instance)
170				},
171				_ => (),
172			}
173		}
174	}
175
176	fn can_check_out(dest: &Location, what: &Asset, context: &XcmContext) -> XcmResult {
177		tracing::trace!(
178			target: LOG_TARGET,
179			?dest,
180			?what,
181			?context,
182			"can_check_out",
183		);
184		// Check we handle this asset.
185		let instance = Matcher::matches_nonfungible(what).ok_or(MatchError::AssetNotHandled)?;
186		match CheckingAccount::get() {
187			// We track this asset's teleports to ensure no more come in than have gone out.
188			Some((_, MintLocation::Local)) => Self::can_accrue_checked(instance),
189			// We track this asset's teleports to ensure no more go out than have come in.
190			Some((checking_account, MintLocation::NonLocal)) => {
191				Self::can_reduce_checked(checking_account, instance)
192			},
193			_ => Ok(()),
194		}
195	}
196
197	fn check_out(dest: &Location, what: &Asset, context: &XcmContext) {
198		tracing::trace!(
199			target: LOG_TARGET,
200			?dest,
201			?what,
202			?context,
203			"check_out",
204		);
205		if let Some(instance) = Matcher::matches_nonfungible(what) {
206			match CheckingAccount::get() {
207				// We track this asset's teleports to ensure no more come in than have gone out.
208				Some((checking_account, MintLocation::Local)) => {
209					Self::accrue_checked(checking_account, instance)
210				},
211				// We track this asset's teleports to ensure no more go out than have come in.
212				Some((_, MintLocation::NonLocal)) => Self::reduce_checked(instance),
213				_ => (),
214			}
215		}
216	}
217
218	fn deposit_asset(
219		what: AssetsInHolding,
220		who: &Location,
221		context: Option<&XcmContext>,
222	) -> Result<(), (AssetsInHolding, XcmError)> {
223		tracing::trace!(
224			target: LOG_TARGET,
225			?what,
226			?who,
227			?context,
228			"deposit_asset",
229		);
230		defensive_assert!(what.len() == 1, "Trying to deposit more than one asset!");
231		// Check we handle this asset.
232		let maybe = what
233			.non_fungible_assets_iter()
234			.next()
235			.and_then(|asset| Matcher::matches_nonfungible(&asset));
236		let Some(instance) = maybe else { return Err((what, MatchError::AssetNotHandled.into())) };
237		let Some(who) = AccountIdConverter::convert_location(who) else {
238			return Err((what, MatchError::AccountIdConversionFailed.into()));
239		};
240		NonFungible::mint_into(&instance, &who).map_err(|e| {
241			tracing::debug!(target: LOG_TARGET, ?e, ?instance, ?who, "Failed to mint asset");
242			(what, XcmError::FailedToTransactAsset(e.into()))
243		})
244	}
245
246	fn withdraw_asset(
247		what: &Asset,
248		who: &Location,
249		maybe_context: Option<&XcmContext>,
250	) -> Result<AssetsInHolding, XcmError> {
251		tracing::trace!(
252			target: LOG_TARGET,
253			?what,
254			?who,
255			?maybe_context,
256			"withdraw_asset"
257		);
258		// Check we handle this asset.
259		let who = AccountIdConverter::convert_location(who)
260			.ok_or(MatchError::AccountIdConversionFailed)?;
261		let instance = Matcher::matches_nonfungible(what).ok_or(MatchError::AssetNotHandled)?;
262		let asset_instance = match what.fun {
263			NonFungible(instance) => instance,
264			_ => return Err(MatchError::AssetNotHandled.into()),
265		};
266		NonFungible::burn(&instance, Some(&who)).map_err(|e| {
267			tracing::debug!(target: LOG_TARGET, ?e, ?instance, ?who, "Failed to burn asset");
268			XcmError::FailedToTransactAsset(e.into())
269		})?;
270		Ok(AssetsInHolding::new_from_non_fungible(what.id.clone(), asset_instance))
271	}
272
273	fn mint_asset(what: &Asset, context: &XcmContext) -> Result<AssetsInHolding, XcmError> {
274		tracing::trace!(
275			target: LOG_TARGET,
276			?what, ?context,
277			"mint_asset",
278		);
279		let asset_instance = match what.fun {
280			NonFungible(instance) => instance,
281			_ => return Err(MatchError::AssetNotHandled.into()),
282		};
283		let _instance = Matcher::matches_nonfungible(what).ok_or(MatchError::AssetNotHandled)?;
284		Ok(AssetsInHolding::new_from_non_fungible(what.id.clone(), asset_instance))
285	}
286}
287
288/// [`TransactAsset`] implementation that allows the use of a [`nonfungible`] implementation for
289/// handling an asset in the XCM executor.
290/// Works for everything.
291pub struct NonFungibleAdapter<NonFungible, Matcher, AccountIdConverter, AccountId, CheckingAccount>(
292	PhantomData<(NonFungible, Matcher, AccountIdConverter, AccountId, CheckingAccount)>,
293)
294where
295	NonFungible: nonfungible::Mutate<AccountId> + nonfungible::Transfer<AccountId>,
296	NonFungible::ItemId: Debug;
297impl<
298		NonFungible: nonfungible::Mutate<AccountId> + nonfungible::Transfer<AccountId>,
299		Matcher: MatchesNonFungible<NonFungible::ItemId>,
300		AccountIdConverter: ConvertLocation<AccountId>,
301		AccountId: Clone + Eq + Debug, /* can't get away without it since Currency is generic
302		                                * over it. */
303		CheckingAccount: Get<Option<(AccountId, MintLocation)>>,
304	> TransactAsset
305	for NonFungibleAdapter<NonFungible, Matcher, AccountIdConverter, AccountId, CheckingAccount>
306where
307	NonFungible::ItemId: Debug,
308{
309	fn can_check_in(origin: &Location, what: &Asset, context: &XcmContext) -> XcmResult {
310		NonFungibleMutateAdapter::<
311			NonFungible,
312			Matcher,
313			AccountIdConverter,
314			AccountId,
315			CheckingAccount,
316		>::can_check_in(origin, what, context)
317	}
318
319	fn check_in(origin: &Location, what: &Asset, context: &XcmContext) {
320		NonFungibleMutateAdapter::<
321			NonFungible,
322			Matcher,
323			AccountIdConverter,
324			AccountId,
325			CheckingAccount,
326		>::check_in(origin, what, context)
327	}
328
329	fn can_check_out(dest: &Location, what: &Asset, context: &XcmContext) -> XcmResult {
330		NonFungibleMutateAdapter::<
331			NonFungible,
332			Matcher,
333			AccountIdConverter,
334			AccountId,
335			CheckingAccount,
336		>::can_check_out(dest, what, context)
337	}
338
339	fn check_out(dest: &Location, what: &Asset, context: &XcmContext) {
340		NonFungibleMutateAdapter::<
341			NonFungible,
342			Matcher,
343			AccountIdConverter,
344			AccountId,
345			CheckingAccount,
346		>::check_out(dest, what, context)
347	}
348
349	fn deposit_asset(
350		what: AssetsInHolding,
351		who: &Location,
352		context: Option<&XcmContext>,
353	) -> Result<(), (AssetsInHolding, XcmError)> {
354		NonFungibleMutateAdapter::<
355			NonFungible,
356			Matcher,
357			AccountIdConverter,
358			AccountId,
359			CheckingAccount,
360		>::deposit_asset(what, who, context)
361	}
362
363	fn withdraw_asset(
364		what: &Asset,
365		who: &Location,
366		maybe_context: Option<&XcmContext>,
367	) -> Result<AssetsInHolding, XcmError> {
368		NonFungibleMutateAdapter::<
369			NonFungible,
370			Matcher,
371			AccountIdConverter,
372			AccountId,
373			CheckingAccount,
374		>::withdraw_asset(what, who, maybe_context)
375	}
376
377	fn transfer_asset(
378		what: &Asset,
379		from: &Location,
380		to: &Location,
381		context: &XcmContext,
382	) -> Result<Asset, XcmError> {
383		NonFungibleTransferAdapter::<NonFungible, Matcher, AccountIdConverter, AccountId>::transfer_asset(
384			what, from, to, context,
385		)
386	}
387
388	fn mint_asset(what: &Asset, context: &XcmContext) -> Result<AssetsInHolding, XcmError> {
389		NonFungibleMutateAdapter::<
390			NonFungible,
391			Matcher,
392			AccountIdConverter,
393			AccountId,
394			CheckingAccount,
395		>::mint_asset(what, context)
396	}
397}