referrerpolicy=no-referrer-when-downgrade

pallet_nfts/features/
atomic_swap.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! This module contains helper functions for performing atomic swaps implemented in the NFTs
19//! pallet.
20//! The bitflag [`PalletFeature::Swaps`] needs to be set in [`Config::Features`] for NFTs
21//! to have the functionality defined in this module.
22
23use crate::*;
24use frame_support::{
25	pallet_prelude::*,
26	traits::{Currency, ExistenceRequirement::KeepAlive},
27};
28
29impl<T: Config<I>, I: 'static> Pallet<T, I> {
30	/// Creates a new swap offer for the specified item.
31	///
32	/// This function is used to create a new swap offer for the specified item. The `caller`
33	/// account must be the owner of the item. The swap offer specifies the `offered_collection`,
34	/// `offered_item`, `desired_collection`, `maybe_desired_item`, `maybe_price`, and `duration`.
35	/// The `duration` specifies the deadline by which the swap must be claimed. If
36	/// `maybe_desired_item` is `Some`, the specified item is expected in return for the swap. If
37	/// `maybe_desired_item` is `None`, it indicates that any item from the `desired_collection` can
38	/// be offered in return. The `maybe_price` specifies an optional price for the swap. If
39	/// specified, the other party must offer the specified `price` or higher for the swap. After
40	/// creating the swap, the function emits the `SwapCreated` event.
41	///
42	/// - `caller`: The account creating the swap offer, which must be the owner of the item.
43	/// - `offered_collection_id`: The collection ID containing the offered item.
44	/// - `offered_item_id`: The item ID offered for the swap.
45	/// - `desired_collection_id`: The collection ID containing the desired item (if any).
46	/// - `maybe_desired_item_id`: The ID of the desired item (if any).
47	/// - `maybe_price`: The optional price for the swap.
48	/// - `duration`: The duration (in block numbers) specifying the deadline for the swap claim.
49	pub(crate) fn do_create_swap(
50		caller: T::AccountId,
51		offered_collection_id: T::CollectionId,
52		offered_item_id: T::ItemId,
53		desired_collection_id: T::CollectionId,
54		maybe_desired_item_id: Option<T::ItemId>,
55		maybe_price: Option<PriceWithDirection<ItemPrice<T, I>>>,
56		duration: BlockNumberFor<T, I>,
57	) -> DispatchResult {
58		ensure!(
59			Self::is_pallet_feature_enabled(PalletFeature::Swaps),
60			Error::<T, I>::MethodDisabled
61		);
62		ensure!(duration <= T::MaxDeadlineDuration::get(), Error::<T, I>::WrongDuration);
63
64		let item = Item::<T, I>::get(&offered_collection_id, &offered_item_id)
65			.ok_or(Error::<T, I>::UnknownItem)?;
66		ensure!(item.owner == caller, Error::<T, I>::NoPermission);
67
68		match maybe_desired_item_id {
69			Some(desired_item_id) => ensure!(
70				Item::<T, I>::contains_key(&desired_collection_id, &desired_item_id),
71				Error::<T, I>::UnknownItem
72			),
73			None => ensure!(
74				Collection::<T, I>::contains_key(&desired_collection_id),
75				Error::<T, I>::UnknownCollection
76			),
77		};
78
79		let now = T::BlockNumberProvider::current_block_number();
80		let deadline = duration.saturating_add(now);
81
82		PendingSwapOf::<T, I>::insert(
83			&offered_collection_id,
84			&offered_item_id,
85			PendingSwap {
86				desired_collection: desired_collection_id,
87				desired_item: maybe_desired_item_id,
88				price: maybe_price.clone(),
89				deadline,
90			},
91		);
92
93		Self::deposit_event(Event::SwapCreated {
94			offered_collection: offered_collection_id,
95			offered_item: offered_item_id,
96			desired_collection: desired_collection_id,
97			desired_item: maybe_desired_item_id,
98			price: maybe_price,
99			deadline,
100		});
101
102		Ok(())
103	}
104	/// Cancels the specified swap offer.
105	///
106	/// This function is used to cancel the specified swap offer created by the `caller` account. If
107	/// the swap offer's deadline has not yet passed, the `caller` must be the owner of the offered
108	/// item; otherwise, anyone can cancel an expired offer.
109	/// After canceling the swap offer, the function emits the `SwapCancelled` event.
110	///
111	/// - `caller`: The account canceling the swap offer.
112	/// - `offered_collection_id`: The collection ID containing the offered item.
113	/// - `offered_item_id`: The item ID offered for the swap.
114	pub(crate) fn do_cancel_swap(
115		caller: T::AccountId,
116		offered_collection_id: T::CollectionId,
117		offered_item_id: T::ItemId,
118	) -> DispatchResult {
119		let swap = PendingSwapOf::<T, I>::get(&offered_collection_id, &offered_item_id)
120			.ok_or(Error::<T, I>::UnknownSwap)?;
121
122		let now = T::BlockNumberProvider::current_block_number();
123		if swap.deadline > now {
124			let item = Item::<T, I>::get(&offered_collection_id, &offered_item_id)
125				.ok_or(Error::<T, I>::UnknownItem)?;
126			ensure!(item.owner == caller, Error::<T, I>::NoPermission);
127		}
128
129		PendingSwapOf::<T, I>::remove(&offered_collection_id, &offered_item_id);
130
131		Self::deposit_event(Event::SwapCancelled {
132			offered_collection: offered_collection_id,
133			offered_item: offered_item_id,
134			desired_collection: swap.desired_collection,
135			desired_item: swap.desired_item,
136			price: swap.price,
137			deadline: swap.deadline,
138		});
139
140		Ok(())
141	}
142
143	/// Claims the specified swap offer.
144	///
145	/// This function is used to claim a swap offer specified by the `send_collection_id`,
146	/// `send_item_id`, `receive_collection_id`, and `receive_item_id`. The `caller` account must be
147	/// the owner of the item specified by `send_collection_id` and `send_item_id`. If the claimed
148	/// swap has an associated `price`, it will be transferred between the owners of the two items
149	/// based on the `price.direction`. After the swap is completed, the function emits the
150	/// `SwapClaimed` event.
151	///
152	/// - `caller`: The account claiming the swap offer, which must be the owner of the sent item.
153	/// - `send_collection_id`: The identifier of the collection containing the item being sent.
154	/// - `send_item_id`: The identifier of the item being sent for the swap.
155	/// - `receive_collection_id`: The identifier of the collection containing the item being
156	///   received.
157	/// - `receive_item_id`: The identifier of the item being received in the swap.
158	/// - `witness_price`: The optional witness price for the swap (price that was offered in the
159	///   swap).
160	pub(crate) fn do_claim_swap(
161		caller: T::AccountId,
162		send_collection_id: T::CollectionId,
163		send_item_id: T::ItemId,
164		receive_collection_id: T::CollectionId,
165		receive_item_id: T::ItemId,
166		witness_price: Option<PriceWithDirection<ItemPrice<T, I>>>,
167	) -> DispatchResult {
168		ensure!(
169			Self::is_pallet_feature_enabled(PalletFeature::Swaps),
170			Error::<T, I>::MethodDisabled
171		);
172
173		let send_item = Item::<T, I>::get(&send_collection_id, &send_item_id)
174			.ok_or(Error::<T, I>::UnknownItem)?;
175		let receive_item = Item::<T, I>::get(&receive_collection_id, &receive_item_id)
176			.ok_or(Error::<T, I>::UnknownItem)?;
177		let swap = PendingSwapOf::<T, I>::get(&receive_collection_id, &receive_item_id)
178			.ok_or(Error::<T, I>::UnknownSwap)?;
179
180		ensure!(send_item.owner == caller, Error::<T, I>::NoPermission);
181		ensure!(
182			swap.desired_collection == send_collection_id && swap.price == witness_price,
183			Error::<T, I>::UnknownSwap
184		);
185
186		if let Some(desired_item) = swap.desired_item {
187			ensure!(desired_item == send_item_id, Error::<T, I>::UnknownSwap);
188		}
189
190		let now = T::BlockNumberProvider::current_block_number();
191		ensure!(now <= swap.deadline, Error::<T, I>::DeadlineExpired);
192
193		if let Some(ref price) = swap.price {
194			match price.direction {
195				PriceDirection::Send => T::Currency::transfer(
196					&receive_item.owner,
197					&send_item.owner,
198					price.amount,
199					KeepAlive,
200				)?,
201				PriceDirection::Receive => T::Currency::transfer(
202					&send_item.owner,
203					&receive_item.owner,
204					price.amount,
205					KeepAlive,
206				)?,
207			};
208		}
209
210		// This also removes the swap.
211		Self::do_transfer(send_collection_id, send_item_id, receive_item.owner.clone(), |_, _| {
212			Ok(())
213		})?;
214		Self::do_transfer(
215			receive_collection_id,
216			receive_item_id,
217			send_item.owner.clone(),
218			|_, _| Ok(()),
219		)?;
220
221		Self::deposit_event(Event::SwapClaimed {
222			sent_collection: send_collection_id,
223			sent_item: send_item_id,
224			sent_item_owner: send_item.owner,
225			received_collection: receive_collection_id,
226			received_item: receive_item_id,
227			received_item_owner: receive_item.owner,
228			price: swap.price,
229			deadline: swap.deadline,
230		});
231
232		Ok(())
233	}
234}