1#![cfg(feature = "runtime-benchmarks")]
21
22extern crate alloc;
23
24use alloc::{vec, vec::Vec};
25use frame_benchmarking::v2::*;
26use frame_support::{
27 dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo},
28 traits::{Consideration, Get},
29};
30use frame_system::RawOrigin;
31use sp_runtime::{
32 traits::{Bounded, Dispatchable, TransactionExtension, TxBaseImplication},
33 transaction_validity::TransactionSource,
34 DispatchError,
35};
36
37use super::*;
38use crate::extension::{AsScarcity, AsScarcityInfo};
39
40fn metadata_key<T: Config>(seed: u32) -> MetadataKeyOf<T> {
41 let mut bytes = vec![0; T::MaxKeyLen::get() as usize];
42 for (destination, source) in bytes.iter_mut().zip(seed.to_le_bytes()) {
43 *destination = source;
44 }
45 bytes.try_into().ok().expect("key uses the configured maximum length")
46}
47
48fn metadata_value<T: Config>(byte: u8, len: u32) -> MetadataValueOf<T> {
49 vec![byte; len as usize]
50 .try_into()
51 .ok()
52 .expect("value length is benchmark-bounded")
53}
54
55fn fund<T: Config>(account: &T::AccountId) {
56 T::Consideration::ensure_successful(account, BalanceOf::<T>::max_value() / 4u32.into());
57}
58
59fn create_definition<T: Config>(
60 owner: &T::AccountId,
61) -> Result<(CollectionId, ItemIndex), BenchmarkError> {
62 fund::<T>(owner);
63 let collection = Pallet::<T>::do_create_collection(owner.clone())?;
64 let item = Pallet::<T>::do_define_item(owner.clone(), collection, Vec::new())?;
65 Ok((collection, item))
66}
67
68#[benchmarks(
69 where
70 T::RuntimeCall: From<Call<T>>
71 + Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>
72 + GetDispatchInfo,
73)]
74mod benchmarks {
75 use super::*;
76
77 #[benchmark]
78 fn create_collection() {
79 let caller: T::AccountId = whitelisted_caller();
80 fund::<T>(&caller);
81
82 #[extrinsic_call]
83 _(RawOrigin::Signed(caller.clone()));
84
85 assert_eq!(NextCollectionId::<T>::get(), 1);
86 assert_eq!(Collections::<T>::get(0).map(|info| info.owner), Some(caller));
87 }
88
89 #[benchmark]
90 fn define_item(m: Linear<0, 100>) -> Result<(), BenchmarkError> {
91 let caller: T::AccountId = whitelisted_caller();
92 fund::<T>(&caller);
93 let collection = Pallet::<T>::do_create_collection(caller.clone())?;
94 let metadata = (0..m)
95 .map(|index| {
96 let key = metadata_key::<T>(index);
97 let value = metadata_value::<T>(0x42, T::MaxValueLen::get());
98 (key, value)
99 })
100 .collect::<Vec<_>>();
101
102 #[extrinsic_call]
103 _(RawOrigin::Signed(caller), collection, metadata);
104
105 assert!(ItemDefs::<T>::contains_key(collection, 0));
106 assert_eq!(ItemMetadata::<T>::iter_prefix((collection, 0)).count(), m as usize);
107 Ok(())
108 }
109
110 #[benchmark]
111 fn mint(m: Linear<0, { T::MaxInstanceMetadata::get() }>) -> Result<(), BenchmarkError> {
112 let caller: T::AccountId = whitelisted_caller();
113 let destination: T::AccountId = account("destination", 0, 0);
114 NftsByOwner::<T>::remove(&destination);
115 let (collection, item) = create_definition::<T>(&caller)?;
116 let metadata = (0..m)
117 .map(|index| {
118 let key = metadata_key::<T>(index);
119 let value = metadata_value::<T>(0x44, T::MaxValueLen::get());
120 (key, value)
121 })
122 .collect::<Vec<_>>();
123
124 #[extrinsic_call]
125 _(RawOrigin::Signed(caller), collection, item, destination.clone(), metadata);
126
127 let nft = NftsByOwner::<T>::get(&destination).expect("destination owns the minted NFT");
128 assert_eq!(Instances::<T>::get(nft.instance), Some(destination));
129 assert_eq!(InstanceMetadataCount::<T>::get(nft.instance), m);
130 Ok(())
131 }
132
133 #[benchmark]
134 fn transfer() -> Result<(), BenchmarkError> {
135 let owner: T::AccountId = whitelisted_caller();
136 let destination: T::AccountId = account("destination", 0, 0);
137 NftsByOwner::<T>::remove(&destination);
138 let (collection, item) = create_definition::<T>(&owner)?;
139 Pallet::<T>::do_mint(owner.clone(), collection, item, owner.clone(), Vec::new())?;
140 let nft = NftsByOwner::<T>::take(&owner).expect("owner has the minted NFT");
141 let instance = nft.instance;
142 let origin: T::RuntimeOrigin = Origin::<T>::Nft { owner, nft }.into();
143
144 #[extrinsic_call]
145 _(origin, destination.clone());
146
147 assert_eq!(NftsByOwner::<T>::get(&destination).map(|nft| nft.instance), Some(instance));
148 assert_eq!(Instances::<T>::get(instance), Some(destination));
149 Ok(())
150 }
151
152 #[benchmark]
153 fn burn(m: Linear<0, { T::MaxInstanceMetadata::get() }>) -> Result<(), BenchmarkError> {
154 let owner: T::AccountId = whitelisted_caller();
155 let (collection, item) = create_definition::<T>(&owner)?;
156 let metadata = (0..m)
157 .map(|index| {
158 let key = metadata_key::<T>(index);
159 let value = metadata_value::<T>(0x44, T::MaxValueLen::get());
160 (key, value)
161 })
162 .collect::<Vec<_>>();
163 Pallet::<T>::do_mint(owner.clone(), collection, item, owner.clone(), metadata)?;
164 let nft = NftsByOwner::<T>::take(&owner).expect("owner has the minted NFT");
165 let instance = nft.instance;
166 assert!(InstanceDeposits::<T>::contains_key(instance));
167 assert_eq!(InstanceMetadataCount::<T>::get(instance), m);
168 let origin: T::RuntimeOrigin = Origin::<T>::Nft { owner, nft }.into();
169
170 #[extrinsic_call]
171 _(origin);
172
173 assert!(!Instances::<T>::contains_key(instance));
174 assert!(!InstanceDeposits::<T>::contains_key(instance));
175 assert!(!InstanceMetadataCount::<T>::contains_key(instance));
176 assert_eq!(InstanceMetadata::<T>::iter_prefix(instance).count(), 0);
177 Ok(())
178 }
179
180 #[benchmark]
181 fn nominate_collection_owner() -> Result<(), BenchmarkError> {
182 let caller: T::AccountId = whitelisted_caller();
183 let nominee: T::AccountId = account("nominee", 0, 0);
184 fund::<T>(&caller);
185 let collection = Pallet::<T>::do_create_collection(caller.clone())?;
186
187 #[extrinsic_call]
188 _(RawOrigin::Signed(caller), collection, Some(nominee.clone()));
189
190 assert_eq!(
191 Collections::<T>::get(collection).and_then(|info| info.pending_owner),
192 Some(nominee),
193 );
194 Ok(())
195 }
196
197 #[benchmark]
198 fn set_collection_metadata() -> Result<(), BenchmarkError> {
199 let caller: T::AccountId = whitelisted_caller();
200 fund::<T>(&caller);
201 let collection = Pallet::<T>::do_create_collection(caller.clone())?;
202 let key = metadata_key::<T>(0);
203 let value = metadata_value::<T>(0x22, T::MaxValueLen::get());
204
205 #[extrinsic_call]
206 _(RawOrigin::Signed(caller), collection, key.clone(), Some(value.clone()));
207
208 assert_eq!(Pallet::<T>::collection_metadata_of(collection, &key), Some(value));
209 frame_system::Pallet::<T>::assert_last_event(
210 Event::<T>::CollectionMetadataSet { collection, key }.into(),
211 );
212 Ok(())
213 }
214
215 #[benchmark]
216 fn set_item_metadata() -> Result<(), BenchmarkError> {
217 let caller: T::AccountId = whitelisted_caller();
218 let (collection, item) = create_definition::<T>(&caller)?;
219 let key = metadata_key::<T>(0);
220 let value = metadata_value::<T>(0x22, T::MaxValueLen::get());
221
222 #[extrinsic_call]
223 _(RawOrigin::Signed(caller), collection, item, key.clone(), Some(value.clone()));
224
225 assert_eq!(Pallet::<T>::item_metadata_of(collection, item, &key), Some(value));
226 frame_system::Pallet::<T>::assert_last_event(
227 Event::<T>::ItemMetadataSet { collection, item, key }.into(),
228 );
229 Ok(())
230 }
231
232 #[benchmark]
233 fn set_instance_metadata() -> Result<(), BenchmarkError> {
234 let caller: T::AccountId = whitelisted_caller();
235 let destination: T::AccountId = account("destination", 0, 0);
236 let (collection, item) = create_definition::<T>(&caller)?;
237 let instance =
238 Pallet::<T>::do_mint(caller.clone(), collection, item, destination, Vec::new())?;
239 let key = metadata_key::<T>(0);
240 let value = metadata_value::<T>(0x66, T::MaxValueLen::get());
241
242 #[extrinsic_call]
243 _(RawOrigin::Signed(caller), instance, key.clone(), Some(value.clone()));
244
245 assert_eq!(Pallet::<T>::instance_metadata_of(instance, &key), Some(value));
246 frame_system::Pallet::<T>::assert_last_event(
247 Event::<T>::InstanceMetadataSet { instance, key }.into(),
248 );
249 Ok(())
250 }
251
252 #[benchmark]
253 fn force_burn(m: Linear<0, { T::MaxInstanceMetadata::get() }>) -> Result<(), BenchmarkError> {
254 let caller: T::AccountId = whitelisted_caller();
255 let destination: T::AccountId = account("destination", 0, 0);
256 let (collection, item) = create_definition::<T>(&caller)?;
257 let metadata = (0..m)
258 .map(|index| {
259 let key = metadata_key::<T>(index);
260 let value = metadata_value::<T>(0x55, T::MaxValueLen::get());
261 (key, value)
262 })
263 .collect::<Vec<_>>();
264 let instance =
265 Pallet::<T>::do_mint(caller.clone(), collection, item, destination.clone(), metadata)?;
266
267 #[extrinsic_call]
268 _(RawOrigin::Signed(caller), instance);
269
270 assert!(!NftsByOwner::<T>::contains_key(destination));
271 assert!(!Instances::<T>::contains_key(instance));
272 assert_eq!(InstanceMetadata::<T>::iter_prefix(instance).count(), 0);
273 Ok(())
274 }
275
276 #[benchmark]
277 fn force_transfer() -> Result<(), BenchmarkError> {
278 let caller: T::AccountId = whitelisted_caller();
279 let from: T::AccountId = account("from", 0, 0);
280 let to: T::AccountId = account("to", 0, 0);
281 NftsByOwner::<T>::remove(&to);
282 let (collection, item) = create_definition::<T>(&caller)?;
283 let instance =
284 Pallet::<T>::do_mint(caller.clone(), collection, item, from.clone(), Vec::new())?;
285 Locked::<T>::insert(&from, LockInfo { retries: u8::MAX, until: u64::MAX });
286
287 #[extrinsic_call]
288 _(RawOrigin::Signed(caller), instance, to.clone());
289
290 let nft = NftsByOwner::<T>::get(&to).expect("destination owns the transferred NFT");
291 assert_eq!(nft.instance, instance);
292 assert_eq!(nft.state_nonce, 1);
293 assert_eq!(Instances::<T>::get(instance), Some(to));
294 assert!(!NftsByOwner::<T>::contains_key(from.clone()));
295 assert!(!Locked::<T>::contains_key(from));
296 Ok(())
297 }
298
299 #[benchmark]
300 fn delete_item() -> Result<(), BenchmarkError> {
301 let caller: T::AccountId = whitelisted_caller();
302 let (collection, item) = create_definition::<T>(&caller)?;
303
304 #[extrinsic_call]
305 _(RawOrigin::Signed(caller), collection, item);
306
307 assert!(!ItemDefs::<T>::contains_key(collection, item));
308 assert_eq!(Collections::<T>::get(collection).map(|info| info.item_count), Some(0));
309 Ok(())
310 }
311
312 #[benchmark]
313 fn delete_collection() -> Result<(), BenchmarkError> {
314 let caller: T::AccountId = whitelisted_caller();
315 fund::<T>(&caller);
316 let collection = Pallet::<T>::do_create_collection(caller.clone())?;
317
318 #[extrinsic_call]
319 _(RawOrigin::Signed(caller), collection);
320
321 assert!(!Collections::<T>::contains_key(collection));
322 Ok(())
323 }
324
325 #[benchmark]
326 fn claim_collection_ownership() -> Result<(), BenchmarkError> {
327 let owner: T::AccountId = whitelisted_caller();
328 let new_owner: T::AccountId = account("new_owner", 0, 0);
329 fund::<T>(&owner);
330 fund::<T>(&new_owner);
331 let collection = Pallet::<T>::do_create_collection(owner.clone())?;
332 Pallet::<T>::nominate_collection_owner(
333 RawOrigin::Signed(owner.clone()).into(),
334 collection,
335 Some(new_owner.clone()),
336 )?;
337
338 #[extrinsic_call]
339 _(RawOrigin::Signed(new_owner.clone()), collection);
340
341 let info = Collections::<T>::get(collection).expect("collection remains after claim");
342 assert_eq!(info.owner, new_owner);
343 assert_eq!(info.pending_owner, None);
344 Ok(())
345 }
346
347 #[benchmark]
348 fn as_scarcity_pipeline() -> Result<(), BenchmarkError> {
349 let owner: T::AccountId = whitelisted_caller();
350 let destination: T::AccountId = account("destination", 0, 0);
351 NftsByOwner::<T>::remove(&destination);
352 let (collection, item) = create_definition::<T>(&owner)?;
353 Pallet::<T>::do_mint(owner.clone(), collection, item, owner.clone(), Vec::new())?;
354 Locked::<T>::insert(&owner, LockInfo { retries: u8::MAX, until: 0 });
355 let nft = NftsByOwner::<T>::get(&owner).expect("owner has the minted NFT");
356
357 let call: T::RuntimeCall = Call::<T>::transfer { to: destination }.into();
358 let info = call.get_dispatch_info();
359 let extension = AsScarcity::<T>::new(Some(AsScarcityInfo::AsNft {
360 instance: nft.instance,
361 state_nonce: nft.state_nonce,
362 }));
363 let origin: T::RuntimeOrigin = RawOrigin::Signed(owner.clone()).into();
364 let failed_dispatch = Err(DispatchError::Other("benchmark failure"));
365
366 #[block]
367 {
368 let (_, val, origin) = extension
369 .validate(
370 origin,
371 &call,
372 &info,
373 0,
374 (),
375 &TxBaseImplication(()),
376 TransactionSource::External,
377 )
378 .expect("worst-case scarcity transfer validates");
379 let pre = extension
380 .prepare(val, &origin, &call, &info, 0)
381 .expect("validated NFT is prepared");
382 AsScarcity::<T>::post_dispatch_details(
383 pre,
384 &info,
385 &Default::default(),
386 0,
387 &failed_dispatch,
388 )
389 .expect("failed dispatch is restored and locked");
390 }
391
392 assert!(NftsByOwner::<T>::contains_key(&owner));
393 assert_eq!(Locked::<T>::get(&owner).map(|lock| lock.retries), Some(u8::MAX));
394 Ok(())
395 }
396
397 impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test);
398}