referrerpolicy=no-referrer-when-downgrade

staging_xcm_builder/
nonfungibles_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::nonfungibles`] through XCM.
18
19use crate::{AssetChecking, MintLocation};
20use core::{fmt::Debug, marker::PhantomData, result};
21use frame_support::{
22	defensive_assert, ensure,
23	traits::{tokens::nonfungibles, Get},
24};
25use xcm::latest::prelude::*;
26use xcm_executor::{
27	traits::{ConvertLocation, Error as MatchError, MatchesNonFungibles, TransactAsset},
28	AssetsInHolding,
29};
30
31const LOG_TARGET: &str = "xcm::nonfungibles_adapter";
32
33/// [`TransactAsset`] implementation that allows the use of a [`nonfungibles`] implementation for
34/// handling an asset in the XCM executor.
35/// Only works for transfers.
36pub struct NonFungiblesTransferAdapter<Assets, Matcher, AccountIdConverter, AccountId>(
37	PhantomData<(Assets, Matcher, AccountIdConverter, AccountId)>,
38)
39where
40	Assets: nonfungibles::Transfer<AccountId>,
41	Assets::CollectionId: Debug,
42	Assets::ItemId: Debug;
43impl<
44		Assets: nonfungibles::Transfer<AccountId>,
45		Matcher: MatchesNonFungibles<Assets::CollectionId, Assets::ItemId>,
46		AccountIdConverter: ConvertLocation<AccountId>,
47		AccountId: Clone + Debug, // can't get away without it since Currency is generic over it.
48	> TransactAsset for NonFungiblesTransferAdapter<Assets, Matcher, AccountIdConverter, AccountId>
49where
50	Assets::CollectionId: Debug,
51	Assets::ItemId: Debug,
52{
53	fn transfer_asset(
54		what: &Asset,
55		from: &Location,
56		to: &Location,
57		context: &XcmContext,
58	) -> Result<Asset, XcmError> {
59		tracing::trace!(
60			target: LOG_TARGET,
61			?what,
62			?from,
63			?to,
64			?context,
65			"transfer_asset",
66		);
67		// Check we handle this asset.
68		let (class, instance) = Matcher::matches_nonfungibles(what)?;
69		let destination = AccountIdConverter::convert_location(to)
70			.ok_or(MatchError::AccountIdConversionFailed)?;
71		Assets::transfer(&class, &instance, &destination).map_err(|e| {
72			tracing::debug!(target: LOG_TARGET, ?e, ?class, ?instance, ?destination, "Failed to transfer asset");
73			XcmError::FailedToTransactAsset(e.into())
74		})?;
75		Ok(what.clone())
76	}
77}
78
79/// [`TransactAsset`] implementation that allows the use of a [`nonfungibles`] implementation for
80/// handling an asset in the XCM executor.
81/// Only works for teleport bookkeeping.
82pub struct NonFungiblesMutateAdapter<
83	Assets,
84	Matcher,
85	AccountIdConverter,
86	AccountId,
87	CheckAsset,
88	CheckingAccount,
89>(PhantomData<(Assets, Matcher, AccountIdConverter, AccountId, CheckAsset, CheckingAccount)>);
90
91impl<
92		Assets: nonfungibles::Mutate<AccountId>,
93		Matcher: MatchesNonFungibles<Assets::CollectionId, Assets::ItemId>,
94		AccountIdConverter: ConvertLocation<AccountId>,
95		AccountId: Clone + Eq, // can't get away without it since Currency is generic over it.
96		CheckAsset: AssetChecking<Assets::CollectionId>,
97		CheckingAccount: Get<Option<AccountId>>,
98	>
99	NonFungiblesMutateAdapter<
100		Assets,
101		Matcher,
102		AccountIdConverter,
103		AccountId,
104		CheckAsset,
105		CheckingAccount,
106	>
107{
108	fn can_accrue_checked(class: Assets::CollectionId, instance: Assets::ItemId) -> XcmResult {
109		ensure!(Assets::owner(&class, &instance).is_none(), XcmError::NotDepositable);
110		Ok(())
111	}
112	fn can_reduce_checked(class: Assets::CollectionId, instance: Assets::ItemId) -> XcmResult {
113		if let Some(checking_account) = CheckingAccount::get() {
114			// This is an asset whose teleports we track.
115			let owner = Assets::owner(&class, &instance);
116			ensure!(owner == Some(checking_account), XcmError::NotWithdrawable);
117			ensure!(Assets::can_transfer(&class, &instance), XcmError::NotWithdrawable);
118		}
119		Ok(())
120	}
121	fn accrue_checked(class: Assets::CollectionId, instance: Assets::ItemId) {
122		if let Some(checking_account) = CheckingAccount::get() {
123			let ok = Assets::mint_into(&class, &instance, &checking_account).is_ok();
124			debug_assert!(ok, "`mint_into` cannot generally fail; qed");
125		}
126	}
127	fn reduce_checked(class: Assets::CollectionId, instance: Assets::ItemId) {
128		let ok = Assets::burn(&class, &instance, None).is_ok();
129		debug_assert!(ok, "`can_check_in` must have returned `true` immediately prior; qed");
130	}
131}
132
133impl<
134		Assets: nonfungibles::Mutate<AccountId>,
135		Matcher: MatchesNonFungibles<Assets::CollectionId, Assets::ItemId>,
136		AccountIdConverter: ConvertLocation<AccountId>,
137		AccountId: Clone + Eq + Debug, /* can't get away without it since Currency is generic
138		                                * over it. */
139		CheckAsset: AssetChecking<Assets::CollectionId>,
140		CheckingAccount: Get<Option<AccountId>>,
141	> TransactAsset
142	for NonFungiblesMutateAdapter<
143		Assets,
144		Matcher,
145		AccountIdConverter,
146		AccountId,
147		CheckAsset,
148		CheckingAccount,
149	>
150where
151	Assets::CollectionId: Debug,
152	Assets::ItemId: Debug,
153{
154	fn can_check_in(origin: &Location, what: &Asset, context: &XcmContext) -> XcmResult {
155		tracing::trace!(
156			target: LOG_TARGET,
157			?origin,
158			?what,
159			?context,
160			"can_check_in",
161		);
162		// Check we handle this asset.
163		let (class, instance) = Matcher::matches_nonfungibles(what)?;
164		match CheckAsset::asset_checking(&class) {
165			// We track this asset's teleports to ensure no more come in than have gone out.
166			Some(MintLocation::Local) => Self::can_reduce_checked(class, instance),
167			// We track this asset's teleports to ensure no more go out than have come in.
168			Some(MintLocation::NonLocal) => Self::can_accrue_checked(class, instance),
169			_ => Ok(()),
170		}
171	}
172
173	fn check_in(origin: &Location, what: &Asset, context: &XcmContext) {
174		tracing::trace!(
175			target: LOG_TARGET,
176			?origin,
177			?what,
178			?context,
179			"check_in",
180		);
181		if let Ok((class, instance)) = Matcher::matches_nonfungibles(what) {
182			match CheckAsset::asset_checking(&class) {
183				// We track this asset's teleports to ensure no more come in than have gone out.
184				Some(MintLocation::Local) => Self::reduce_checked(class, instance),
185				// We track this asset's teleports to ensure no more go out than have come in.
186				Some(MintLocation::NonLocal) => Self::accrue_checked(class, instance),
187				_ => (),
188			}
189		}
190	}
191
192	fn can_check_out(dest: &Location, what: &Asset, context: &XcmContext) -> XcmResult {
193		tracing::trace!(
194			target: LOG_TARGET,
195			?dest,
196			?what,
197			?context,
198			"can_check_out",
199		);
200		// Check we handle this asset.
201		let (class, instance) = Matcher::matches_nonfungibles(what)?;
202		match CheckAsset::asset_checking(&class) {
203			// We track this asset's teleports to ensure no more come in than have gone out.
204			Some(MintLocation::Local) => Self::can_accrue_checked(class, instance),
205			// We track this asset's teleports to ensure no more go out than have come in.
206			Some(MintLocation::NonLocal) => Self::can_reduce_checked(class, instance),
207			_ => Ok(()),
208		}
209	}
210
211	fn check_out(dest: &Location, what: &Asset, context: &XcmContext) {
212		tracing::trace!(
213			target: LOG_TARGET,
214			?dest,
215			?what,
216			?context,
217			"check_out",
218		);
219		if let Ok((class, instance)) = Matcher::matches_nonfungibles(what) {
220			match CheckAsset::asset_checking(&class) {
221				// We track this asset's teleports to ensure no more come in than have gone out.
222				Some(MintLocation::Local) => Self::accrue_checked(class, instance),
223				// We track this asset's teleports to ensure no more go out than have come in.
224				Some(MintLocation::NonLocal) => Self::reduce_checked(class, instance),
225				_ => (),
226			}
227		}
228	}
229
230	fn deposit_asset(
231		what: AssetsInHolding,
232		who: &Location,
233		context: Option<&XcmContext>,
234	) -> Result<(), (AssetsInHolding, XcmError)> {
235		tracing::trace!(
236			target: LOG_TARGET,
237			?what,
238			?who,
239			?context,
240			"deposit_asset",
241		);
242		defensive_assert!(what.len() == 1, "Trying to deposit more than one asset!");
243		// Check we handle this asset.
244		let maybe = what
245			.non_fungible_assets_iter()
246			.next()
247			.and_then(|asset| Matcher::matches_nonfungibles(&asset).ok());
248		let Some((class, instance)) = maybe else {
249			return Err((what, MatchError::AssetNotHandled.into()));
250		};
251		let Some(who) = AccountIdConverter::convert_location(who) else {
252			return Err((what, MatchError::AccountIdConversionFailed.into()));
253		};
254		Assets::mint_into(&class, &instance, &who).map_err(|e| {
255			tracing::debug!(target: LOG_TARGET, ?e, ?class, ?instance, ?who, "Failed to mint asset");
256			(what, XcmError::FailedToTransactAsset(e.into()))
257		})
258	}
259
260	fn withdraw_asset(
261		what: &Asset,
262		who: &Location,
263		maybe_context: Option<&XcmContext>,
264	) -> Result<AssetsInHolding, XcmError> {
265		tracing::trace!(
266			target: LOG_TARGET,
267			?what,
268			?who,
269			?maybe_context,
270			"withdraw_asset",
271		);
272		// Check we handle this asset.
273		let who = AccountIdConverter::convert_location(who)
274			.ok_or(MatchError::AccountIdConversionFailed)?;
275		let asset_instance = match what.fun {
276			NonFungible(instance) => instance,
277			_ => return Err(MatchError::AssetNotHandled.into()),
278		};
279		let (class, instance) = Matcher::matches_nonfungibles(what)?;
280		Assets::burn(&class, &instance, Some(&who)).map_err(|e| {
281			tracing::debug!(target: LOG_TARGET, ?e, ?class, ?instance, ?who, "Failed to burn asset");
282			XcmError::FailedToTransactAsset(e.into())
283		})?;
284		Ok(AssetsInHolding::new_from_non_fungible(what.id.clone(), asset_instance))
285	}
286
287	fn mint_asset(what: &Asset, context: &XcmContext) -> Result<AssetsInHolding, XcmError> {
288		tracing::trace!(
289			target: LOG_TARGET,
290			?what, ?context,
291			"mint_asset",
292		);
293		let asset_instance = match what.fun {
294			NonFungible(instance) => instance,
295			_ => return Err(MatchError::AssetNotHandled.into()),
296		};
297		Matcher::matches_nonfungibles(what)?;
298		Ok(AssetsInHolding::new_from_non_fungible(what.id.clone(), asset_instance))
299	}
300}
301
302/// [`TransactAsset`] implementation that allows the use of a [`nonfungibles`] implementation for
303/// handling an asset in the XCM executor.
304/// Works for everything.
305pub struct NonFungiblesAdapter<
306	Assets,
307	Matcher,
308	AccountIdConverter,
309	AccountId,
310	CheckAsset,
311	CheckingAccount,
312>(PhantomData<(Assets, Matcher, AccountIdConverter, AccountId, CheckAsset, CheckingAccount)>)
313where
314	Assets: nonfungibles::Transfer<AccountId>,
315	Assets::CollectionId: Debug,
316	Assets::ItemId: Debug;
317impl<
318		Assets: nonfungibles::Mutate<AccountId> + nonfungibles::Transfer<AccountId>,
319		Matcher: MatchesNonFungibles<Assets::CollectionId, Assets::ItemId>,
320		AccountIdConverter: ConvertLocation<AccountId>,
321		AccountId: Clone + Eq + Debug, /* can't get away without it since Currency is generic
322		                                * over it. */
323		CheckAsset: AssetChecking<Assets::CollectionId>,
324		CheckingAccount: Get<Option<AccountId>>,
325	> TransactAsset
326	for NonFungiblesAdapter<
327		Assets,
328		Matcher,
329		AccountIdConverter,
330		AccountId,
331		CheckAsset,
332		CheckingAccount,
333	>
334where
335	Assets::CollectionId: Debug,
336	Assets::ItemId: Debug,
337{
338	fn can_check_in(origin: &Location, what: &Asset, context: &XcmContext) -> XcmResult {
339		NonFungiblesMutateAdapter::<
340			Assets,
341			Matcher,
342			AccountIdConverter,
343			AccountId,
344			CheckAsset,
345			CheckingAccount,
346		>::can_check_in(origin, what, context)
347	}
348
349	fn check_in(origin: &Location, what: &Asset, context: &XcmContext) {
350		NonFungiblesMutateAdapter::<
351			Assets,
352			Matcher,
353			AccountIdConverter,
354			AccountId,
355			CheckAsset,
356			CheckingAccount,
357		>::check_in(origin, what, context)
358	}
359
360	fn can_check_out(dest: &Location, what: &Asset, context: &XcmContext) -> XcmResult {
361		NonFungiblesMutateAdapter::<
362			Assets,
363			Matcher,
364			AccountIdConverter,
365			AccountId,
366			CheckAsset,
367			CheckingAccount,
368		>::can_check_out(dest, what, context)
369	}
370
371	fn check_out(dest: &Location, what: &Asset, context: &XcmContext) {
372		NonFungiblesMutateAdapter::<
373			Assets,
374			Matcher,
375			AccountIdConverter,
376			AccountId,
377			CheckAsset,
378			CheckingAccount,
379		>::check_out(dest, what, context)
380	}
381
382	fn deposit_asset(
383		what: AssetsInHolding,
384		who: &Location,
385		context: Option<&XcmContext>,
386	) -> Result<(), (AssetsInHolding, XcmError)> {
387		NonFungiblesMutateAdapter::<
388			Assets,
389			Matcher,
390			AccountIdConverter,
391			AccountId,
392			CheckAsset,
393			CheckingAccount,
394		>::deposit_asset(what, who, context)
395	}
396
397	fn withdraw_asset(
398		what: &Asset,
399		who: &Location,
400		maybe_context: Option<&XcmContext>,
401	) -> result::Result<AssetsInHolding, XcmError> {
402		NonFungiblesMutateAdapter::<
403			Assets,
404			Matcher,
405			AccountIdConverter,
406			AccountId,
407			CheckAsset,
408			CheckingAccount,
409		>::withdraw_asset(what, who, maybe_context)
410	}
411
412	fn transfer_asset(
413		what: &Asset,
414		from: &Location,
415		to: &Location,
416		context: &XcmContext,
417	) -> Result<Asset, XcmError> {
418		NonFungiblesTransferAdapter::<Assets, Matcher, AccountIdConverter, AccountId>::transfer_asset(
419			what, from, to, context,
420		)
421	}
422
423	fn mint_asset(what: &Asset, context: &XcmContext) -> Result<AssetsInHolding, XcmError> {
424		NonFungiblesMutateAdapter::<
425			Assets,
426			Matcher,
427			AccountIdConverter,
428			AccountId,
429			CheckAsset,
430			CheckingAccount,
431		>::mint_asset(what, context)
432	}
433}