1use crate::{pallet::*, weights::WeightInfo, Config, Nft};
59use codec::{Decode, DecodeWithMemTracking, Encode};
60use core::marker::PhantomData;
61use frame_support::{
62 pallet_prelude::{Get, TransactionSource},
63 traits::{IsSubType, OriginTrait, UnixTime},
64 weights::Weight,
65 CloneNoBound, DebugNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound,
66};
67use scale_info::TypeInfo;
68use sp_runtime::{
69 traits::{
70 DispatchInfoOf, Implication, PostDispatchInfoOf, TransactionExtension, ValidateResult,
71 },
72 transaction_validity::{InvalidTransaction, TransactionValidityError, ValidTransaction},
73 DispatchResult,
74};
75
76#[derive(
78 Encode,
79 Decode,
80 TypeInfo,
81 EqNoBound,
82 CloneNoBound,
83 PartialEqNoBound,
84 DecodeWithMemTracking,
85 DebugNoBound,
86)]
87pub enum AsScarcityInfo {
88 AsNft {
90 instance: InstanceId,
92 state_nonce: u64,
94 },
95}
96
97#[repr(u8)]
99pub enum CustomInvalidity {
100 OriginToAsNftMustBeSigned = 0,
102 NftTemporarilyLocked = 1,
104 NoNft = 2,
106 TransferToSelf = 3,
108 DestinationOccupied = 4,
110 NftStateMismatch = 5,
112}
113
114impl From<CustomInvalidity> for TransactionValidityError {
115 fn from(value: CustomInvalidity) -> Self {
116 InvalidTransaction::Custom(value as u8).into()
117 }
118}
119
120pub enum Val<T: Config + Send + Sync> {
122 NotUsing,
123 UsingNft { owner: T::AccountId, instance: InstanceId, state_nonce: u64 },
124}
125
126pub enum Pre<T: Config + Send + Sync> {
128 NotUsing,
129 UsingNft { owner: T::AccountId, nft: Nft },
130}
131
132#[derive(
144 Encode,
145 Decode,
146 TypeInfo,
147 EqNoBound,
148 CloneNoBound,
149 PartialEqNoBound,
150 DefaultNoBound,
151 DecodeWithMemTracking,
152 DebugNoBound,
153)]
154#[scale_info(skip_type_params(T))]
155pub struct AsScarcity<T: Config + Send + Sync>(Option<AsScarcityInfo>, PhantomData<T>);
156
157impl<T: Config + Send + Sync> AsScarcity<T> {
158 pub fn new(explicit: Option<AsScarcityInfo>) -> Self {
160 Self(explicit, PhantomData)
161 }
162
163 fn failed_dispatch_lock(previous: Option<LockInfo>) -> LockInfo {
164 let retries = previous.map(|lock| lock.retries.saturating_add(1)).unwrap_or(1);
165 let exponent = retries.saturating_sub(1);
166 let multiplier = 2u64.saturating_pow(u32::from(exponent).min(63));
167 LockInfo {
168 retries,
169 until: T::UnixTime::now()
170 .as_secs()
171 .saturating_add(multiplier.saturating_mul(T::LockPeriod::get())),
172 }
173 }
174}
175
176impl<T: Config + Send + Sync> TransactionExtension<<T as frame_system::Config>::RuntimeCall>
177 for AsScarcity<T>
178{
179 const IDENTIFIER: &'static str = "AsScarcity";
180 type Implicit = ();
181 type Val = Val<T>;
182 type Pre = Pre<T>;
183
184 fn weight(&self, call: &<T as frame_system::Config>::RuntimeCall) -> Weight {
185 if matches!(self.0.as_ref(), Some(AsScarcityInfo::AsNft { .. })) &&
186 matches!(
187 call.is_sub_type(),
188 Some(Call::<T>::transfer { .. }) | Some(Call::<T>::burn {})
189 ) {
190 T::WeightInfo::as_scarcity_pipeline()
191 } else {
192 Weight::zero()
193 }
194 }
195
196 fn validate(
197 &self,
198 mut origin: <T as frame_system::Config>::RuntimeOrigin,
199 call: &<T as frame_system::Config>::RuntimeCall,
200 _info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
201 _len: usize,
202 _self_implicit: Self::Implicit,
203 _inherited_implication: &impl Implication,
204 _source: TransactionSource,
205 ) -> ValidateResult<Self::Val, <T as frame_system::Config>::RuntimeCall> {
206 let transfer_to = match call.is_sub_type() {
207 Some(Call::<T>::transfer { to }) => Some(to),
208 Some(Call::<T>::burn {}) => None,
209 _ => return Ok((ValidTransaction::default(), Val::NotUsing, origin)),
210 };
211 let Some(AsScarcityInfo::AsNft { instance, state_nonce }) = self.0.as_ref() else {
212 return Ok((ValidTransaction::default(), Val::NotUsing, origin));
213 };
214
215 let Some(frame_system::Origin::<T>::Signed(owner)) = origin.as_system_ref() else {
216 return Err(CustomInvalidity::OriginToAsNftMustBeSigned.into());
217 };
218 let owner = owner.clone();
219 let now = T::UnixTime::now().as_secs();
220 if let Some(lock) = Locked::<T>::get(&owner) {
221 if lock.until > now {
222 return Err(CustomInvalidity::NftTemporarilyLocked.into());
223 }
224 }
225 let nft = NftsByOwner::<T>::get(&owner).ok_or(CustomInvalidity::NoNft)?;
226 if nft.instance != *instance || nft.state_nonce != *state_nonce {
227 return Err(CustomInvalidity::NftStateMismatch.into());
228 }
229 if let Some(to) = transfer_to {
230 if to == &owner {
235 return Err(CustomInvalidity::TransferToSelf.into());
236 }
237 if NftsByOwner::<T>::contains_key(to) {
238 return Err(CustomInvalidity::DestinationOccupied.into());
239 }
240 }
241 let priority = now.saturating_sub(nft.last_moved).min(T::MaxTransferPriority::get());
242 let validity = ValidTransaction::with_tag_prefix("Scarcity")
243 .and_provides((nft.instance, nft.state_nonce))
244 .priority(priority)
245 .into();
246 origin.set_caller_from(Origin::Nft { owner: owner.clone(), nft });
247 Ok((
248 validity,
249 Val::UsingNft { owner, instance: *instance, state_nonce: *state_nonce },
250 origin,
251 ))
252 }
253
254 fn prepare(
255 self,
256 val: Self::Val,
257 _origin: &<T as frame_system::Config>::RuntimeOrigin,
258 _call: &<T as frame_system::Config>::RuntimeCall,
259 _info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
260 _len: usize,
261 ) -> Result<Self::Pre, TransactionValidityError> {
262 match val {
263 Val::NotUsing => Ok(Pre::NotUsing),
264 Val::UsingNft { owner, instance, state_nonce } => {
265 let nft = NftsByOwner::<T>::try_mutate_exists(
266 &owner,
267 |maybe_nft| -> Result<Nft, TransactionValidityError> {
268 let nft = maybe_nft.as_ref().ok_or(CustomInvalidity::NoNft)?;
269 if nft.instance != instance || nft.state_nonce != state_nonce {
270 return Err(CustomInvalidity::NftStateMismatch.into());
271 }
272 Ok(maybe_nft.take().expect("NFT existence checked above; qed"))
276 },
277 )?;
278 Ok(Pre::UsingNft { owner, nft })
279 },
280 }
281 }
282
283 fn post_dispatch_details(
284 pre: Self::Pre,
285 _info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
286 _post_info: &PostDispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
287 _len: usize,
288 result: &DispatchResult,
289 ) -> Result<Weight, TransactionValidityError> {
290 if let Pre::UsingNft { owner, nft } = pre {
291 if result.is_err() {
292 NftsByOwner::<T>::insert(&owner, nft);
293 Locked::<T>::insert(&owner, Self::failed_dispatch_lock(Locked::<T>::get(&owner)));
294 } else {
295 Locked::<T>::remove(&owner);
296 }
297 }
298 Ok(Weight::zero())
299 }
300}