referrerpolicy=no-referrer-when-downgrade

pallet_scarcity/
lib.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//! # Scarcity Pallet
19//!
20//! `pallet-scarcity` defines NFT collections and item definitions, then mints instances using a
21//! coinage-style ownership model: each purse key can hold at most one NFT. The pallet knows
22//! ownership, supply, metadata, and deposits; it knows nothing about what an item means. Item
23//! semantics and access-control policy can live in a higher-level runtime pallet or collection
24//! contract. At this storage layer the collection owner has full control over its definitions,
25//! metadata, and live instances, including force-transfer and force-burn. A runtime must
26//! separately expose Scarcity calls to its chosen contract environment; this crate does not
27//! provide a contract adapter.
28//!
29//! Purse keys are coinage-style receiving addresses, not identities: the pallet applies no
30//! destination consent. Any collection owner can mint into โ€” or force-transfer an instance to โ€”
31//! any empty purse key, and because each key holds at most one NFT, an unsolicited instance
32//! blocks that key from receiving anything else until its holder burns it or transfers it away.
33//! Holders should treat purse keys as disposable, minting to fresh keys they control, and
34//! runtimes or contracts that need receive-consent or long-lived well-known destinations must
35//! enforce that policy above this storage layer.
36//!
37//! The current collection owner backs all collection state with one aggregate consideration
38//! ticket. To transfer that responsibility safely, the owner first nominates a successor and the
39//! successor claims the collection. Claiming atomically creates an equivalent ticket for the
40//! successor, drops the former owner's ticket, and transfers collection authority. This lets the
41//! successor reject an unwanted or unaffordable collection while allowing the runtime to choose
42//! how storage consideration is implemented.
43//!
44//! Footprints count logical records and their encoded payloads rather than exact trie keys and
45//! hash prefixes. A runtime can account for backend overhead in its per-record base price and
46//! calibrate its byte price to the desired storage policy.
47//!
48//! Cleanup proceeds from leaves to roots so every call remains bounded. The collection owner
49//! force-burns live instances (or holders burn their own), removes item metadata, deletes empty
50//! item definitions, removes collection metadata, and finally deletes the empty collection.
51//! Instance metadata is bounded and removed automatically on burn. Allocated identifiers are
52//! never reused.
53//!
54//! Collection metadata supplies defaults inherited by every item definition, and item metadata
55//! supplies defaults shared by every instance minted from that definition. A minted instance may
56//! override either scope without affecting other instances. [`Pallet::instance_metadata_of`]
57//! resolves one key in instance, item, then collection order. [`Pallet::item_metadata_of`]
58//! resolves the item and collection scopes, while [`Pallet::collection_metadata_of`] reads only
59//! the collection scope.
60//!
61//! Keys and values are bounded raw bytes. Numeric metadata is a convention rather than a pallet
62//! type: games should use SCALE-encoded `u128` values when they need a shared numeric convention,
63//! and must decode and validate those bytes themselves.
64//!
65//! Transfers are feeless when authorized through the [`AsScarcity`](extension::AsScarcity)
66//! transaction extension. Their transaction priority is the time since the NFT last moved,
67//! capped by the runtime. Moving an NFT consumes it from the old purse key and places it at the
68//! new one. Each authorization names the permanent instance and its current state nonce. The state
69//! nonce invalidates an authorization whenever that instance moves, including collection-owner
70//! force-transfers away from and back to the same purse. Following Coinage's purse model,
71//! [`AsScarcity`](extension::AsScarcity) replaces the signed origin before ordinary account checks,
72//! so an NFT-only purse does not need a System account. Failed dispatch restores the NFT and
73//! temporarily locks the purse key; after the lock expires, the same signed transaction may be
74//! submitted again if its NFT state is still current. Callers must sign mortal transactions with
75//! an era shorter than [`Config::LockPeriod`] so that retrying is always a fresh signing
76//! decision; see the [replay and mortality rules](extension#replay-and-mortality).
77
78#![cfg_attr(not(feature = "std"), no_std)]
79
80extern crate alloc;
81
82pub use pallet::*;
83
84/// Runtime-only integration for minting an NFT without instance-scoped storage deposits.
85///
86/// Implementations still enforce all collection, item, supply, and ownership invariants. The
87/// calling runtime pallet is responsible for bounding depositless state growth through some
88/// other scarce resource or protocol rule.
89pub trait MintWithoutDeposit<AccountId> {
90	/// Bounded metadata key accepted by the implementation.
91	type MetadataKey;
92	/// Bounded metadata value accepted by the implementation.
93	type MetadataValue;
94
95	/// Mint an instance and its initial metadata without storage deposits.
96	fn mint_without_deposit(
97		collection: CollectionId,
98		item: ItemIndex,
99		to: AccountId,
100		metadata: alloc::vec::Vec<(Self::MetadataKey, Self::MetadataValue)>,
101	) -> Result<InstanceId, sp_runtime::DispatchError>;
102}
103
104#[cfg(feature = "runtime-benchmarks")]
105mod benchmarking;
106
107#[cfg(test)]
108mod mock;
109
110#[cfg(test)]
111mod tests;
112
113pub mod extension;
114pub mod weights;
115
116#[frame_support::pallet]
117pub mod pallet {
118	use crate::weights::WeightInfo;
119	#[cfg(any(test, feature = "try-runtime"))]
120	use alloc::collections::BTreeMap;
121	use alloc::vec::Vec;
122	use frame_support::{
123		pallet_prelude::*,
124		traits::{tokens::Balance, Consideration, Footprint, IsSubType, UnixTime},
125		transactional,
126	};
127	use frame_system::pallet_prelude::*;
128	#[cfg(any(test, feature = "try-runtime"))]
129	use sp_runtime::TryRuntimeError;
130	use sp_runtime::{
131		traits::{CheckedAdd, CheckedSub, Convert, Zero},
132		transaction_validity::TransactionPriority,
133		ArithmeticError, DispatchError,
134	};
135
136	pub type CollectionId = u32;
137	/// Index within a collection; `(CollectionId, ItemIndex)` names an item definition.
138	pub type ItemIndex = u32;
139	/// Permanent global serial, assigned at mint.
140	pub type InstanceId = u64;
141	pub type BalanceOf<T> = <T as Config>::Balance;
142	pub type CollectionInfoOf<T> = CollectionInfo<
143		<T as frame_system::Config>::AccountId,
144		BalanceOf<T>,
145		<T as Config>::Consideration,
146	>;
147	pub type MetadataKeyOf<T> = BoundedVec<u8, <T as Config>::MaxKeyLen>;
148	pub type MetadataValueOf<T> = BoundedVec<u8, <T as Config>::MaxValueLen>;
149
150	/// One stored metadata entry and the deposit backing it.
151	#[derive(
152		Clone, PartialEq, Eq, Debug, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen,
153	)]
154	pub struct MetadataEntry<Value, Balance> {
155		pub value: Value,
156		/// Exact contribution to the collection's aggregate consideration.
157		pub deposit: Balance,
158	}
159
160	/// Collection owner, ownership handoff, item allocation, and deposit accounting.
161	#[derive(
162		CloneNoBound,
163		PartialEqNoBound,
164		EqNoBound,
165		DebugNoBound,
166		Encode,
167		Decode,
168		DecodeWithMemTracking,
169		TypeInfo,
170		MaxEncodedLen,
171	)]
172	#[scale_info(skip_type_params(StorageConsideration))]
173	pub struct CollectionInfo<AccountId: Member, Balance: Member, StorageConsideration: Member> {
174		/// Only this account may perform collection-owner-authorized operations.
175		pub owner: AccountId,
176		/// Account nominated by `owner` to claim the collection.
177		pub pending_owner: Option<AccountId>,
178		/// The next item index to allocate, beginning at zero.
179		pub next_item_index: ItemIndex,
180		/// Number of item definitions which have not been deleted.
181		pub item_count: u32,
182		/// Number of collection-level metadata entries.
183		pub metadata_count: u32,
184		/// Exact deposit for the collection record itself.
185		pub collection_deposit: Balance,
186		/// Exact aggregate deposit represented by `consideration`.
187		pub owner_deposit: Balance,
188		/// Opaque consideration ticket backing `owner_deposit` on behalf of `owner`.
189		pub consideration: StorageConsideration,
190	}
191
192	/// Immutable shared definition for every minted copy of an item.
193	#[derive(
194		CloneNoBound,
195		PartialEqNoBound,
196		EqNoBound,
197		DebugNoBound,
198		Encode,
199		Decode,
200		DecodeWithMemTracking,
201		TypeInfo,
202		MaxEncodedLen,
203	)]
204	pub struct ItemDefinition<Balance: Member> {
205		/// Number of instances ever minted from this definition; burns do not decrement it.
206		pub supply: u32,
207		/// Number of currently live instances; every burn decrements it.
208		pub live_supply: u32,
209		/// Number of item-level metadata entries.
210		pub metadata_count: u32,
211		/// Exact storage deposit included in the collection's aggregate consideration.
212		pub deposit: Balance,
213	}
214
215	/// A minted Scarcity instance.
216	#[derive(
217		Clone, PartialEq, Eq, Debug, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen,
218	)]
219	pub struct Nft {
220		pub instance: InstanceId,
221		pub collection: CollectionId,
222		pub item: ItemIndex,
223		/// Unix seconds at mint.
224		pub minted_at: u64,
225		/// Unix seconds; equal to `minted_at` until the first transfer.
226		pub last_moved: u64,
227		/// Monotonic ownership-state revision, incremented by every successful transfer.
228		///
229		/// Purse-key authorizations bind to this value so moving an instance away and back cannot
230		/// revive an authorization created for its earlier ownership state.
231		pub state_nonce: u64,
232	}
233
234	/// Post-failure backoff lock for an NFT purse key.
235	#[derive(
236		Clone,
237		Copy,
238		PartialEq,
239		Eq,
240		Debug,
241		Encode,
242		Decode,
243		DecodeWithMemTracking,
244		TypeInfo,
245		MaxEncodedLen,
246	)]
247	pub struct LockInfo {
248		/// Number of consecutive failed purse-key dispatches.
249		pub retries: u8,
250		/// Unix timestamp (seconds) at which this lock expires.
251		pub until: u64,
252	}
253
254	/// The next collection identifier to allocate.
255	#[pallet::storage]
256	pub type NextCollectionId<T> = StorageValue<_, CollectionId, ValueQuery>;
257
258	/// Immutable item catalogues, grouped by their collection.
259	#[pallet::storage]
260	pub type Collections<T: Config> = StorageMap<
261		_,
262		Twox64Concat,
263		CollectionId,
264		CollectionInfo<T::AccountId, BalanceOf<T>, T::Consideration>,
265	>;
266
267	/// Immutable item definitions, indexed within their collection.
268	#[pallet::storage]
269	pub type ItemDefs<T: Config> = StorageDoubleMap<
270		_,
271		Twox64Concat,
272		CollectionId,
273		Twox64Concat,
274		ItemIndex,
275		ItemDefinition<BalanceOf<T>>,
276	>;
277
278	/// Collection-wide defaults inherited by every item in a collection.
279	///
280	/// Entries must be removed individually before the collection can be deleted.
281	#[pallet::storage]
282	pub type CollectionMetadata<T: Config> = StorageDoubleMap<
283		_,
284		Twox64Concat,
285		CollectionId,
286		Blake2_128Concat,
287		MetadataKeyOf<T>,
288		MetadataEntry<MetadataValueOf<T>, BalanceOf<T>>,
289	>;
290
291	/// Per-item defaults which override collection defaults for the same key.
292	///
293	/// These entries are shared by every instance minted from the item definition. Item
294	/// definitions outlive minted instances, so burning an instance never removes them. Entries
295	/// must be removed individually before the item definition can be deleted.
296	#[pallet::storage]
297	pub type ItemMetadata<T: Config> = StorageNMap<
298		_,
299		(
300			NMapKey<Twox64Concat, CollectionId>,
301			NMapKey<Twox64Concat, ItemIndex>,
302			NMapKey<Blake2_128Concat, MetadataKeyOf<T>>,
303		),
304		MetadataEntry<MetadataValueOf<T>, BalanceOf<T>>,
305	>;
306
307	/// The next permanent instance identifier to allocate.
308	#[pallet::storage]
309	pub type NextInstanceId<T> = StorageValue<_, InstanceId, ValueQuery>;
310
311	/// One NFT per owner key โ€” the coinage model.
312	#[pallet::storage]
313	pub type NftsByOwner<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, Nft>;
314
315	/// Stable reverse index from instance identifier to its current owner key.
316	#[pallet::storage]
317	pub type Instances<T: Config> = StorageMap<_, Twox64Concat, InstanceId, T::AccountId>;
318
319	/// Exact per-instance storage deposits, present only for deposit-paying mints.
320	#[pallet::storage]
321	pub type InstanceDeposits<T: Config> = StorageMap<_, Twox64Concat, InstanceId, BalanceOf<T>>;
322
323	/// Number of metadata overrides stored for each live instance.
324	///
325	/// This entry exists even when the count is zero so burn cleanup remains bounded and
326	/// `try_state` can verify that every live instance has explicit metadata accounting.
327	#[pallet::storage]
328	pub type InstanceMetadataCount<T: Config> =
329		StorageMap<_, Twox64Concat, InstanceId, u32, ValueQuery>;
330
331	/// Per-instance entries which override item and collection metadata for the same key.
332	#[pallet::storage]
333	pub type InstanceMetadata<T: Config> = StorageDoubleMap<
334		_,
335		Twox64Concat,
336		InstanceId,
337		Blake2_128Concat,
338		MetadataKeyOf<T>,
339		MetadataEntry<MetadataValueOf<T>, BalanceOf<T>>,
340	>;
341
342	/// Post-failure backoff locks for NFT purse keys.
343	///
344	/// A separate lock explicitly rejects transactions during backoff. Updating `last_moved`
345	/// would only lower transaction priority and would not enforce a delay.
346	#[pallet::storage]
347	pub type Locked<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, LockInfo>;
348
349	#[pallet::event]
350	#[pallet::generate_deposit(pub(super) fn deposit_event)]
351	pub enum Event<T: Config> {
352		/// A collection was created and assigned to its initial owner.
353		CollectionCreated { collection: CollectionId, owner: T::AccountId },
354		/// The collection owner nominated or cleared a prospective owner.
355		CollectionOwnerNominated { collection: CollectionId, pending_owner: Option<T::AccountId> },
356		/// A nominated account claimed the collection and assumed its aggregate storage deposit.
357		CollectionOwnerChanged {
358			collection: CollectionId,
359			old_owner: T::AccountId,
360			new_owner: T::AccountId,
361		},
362		/// An empty collection was deleted and its remaining deposit released.
363		CollectionDeleted { collection: CollectionId },
364		/// An immutable item definition was assigned within a collection.
365		ItemDefined { collection: CollectionId, item: ItemIndex },
366		/// An unused item definition was deleted and its deposit released.
367		ItemDeleted { collection: CollectionId, item: ItemIndex },
368		/// An instance was minted into an empty purse key.
369		Minted {
370			instance: InstanceId,
371			collection: CollectionId,
372			item: ItemIndex,
373			owner: T::AccountId,
374		},
375		/// A holder moved an instance from one purse key to another.
376		Transferred { instance: InstanceId, from: T::AccountId, to: T::AccountId },
377		/// A collection owner forced an instance from one purse key to another.
378		ForceTransferred { instance: InstanceId, from: T::AccountId, to: T::AccountId },
379		/// An instance was permanently removed.
380		Burned { instance: InstanceId },
381		/// A collection-level metadata default was inserted or overwritten.
382		CollectionMetadataSet { collection: CollectionId, key: MetadataKeyOf<T> },
383		/// A collection-level metadata default was removed.
384		CollectionMetadataRemoved { collection: CollectionId, key: MetadataKeyOf<T> },
385		/// An item-level metadata override was inserted or overwritten.
386		ItemMetadataSet { collection: CollectionId, item: ItemIndex, key: MetadataKeyOf<T> },
387		/// An item-level metadata override was removed.
388		ItemMetadataRemoved { collection: CollectionId, item: ItemIndex, key: MetadataKeyOf<T> },
389		/// An instance-level metadata override was inserted or overwritten.
390		InstanceMetadataSet { instance: InstanceId, key: MetadataKeyOf<T> },
391		/// An instance-level metadata override was removed.
392		InstanceMetadataRemoved { instance: InstanceId, key: MetadataKeyOf<T> },
393	}
394
395	#[pallet::error]
396	pub enum Error<T> {
397		/// The collection identifier space is exhausted.
398		TooManyCollections,
399		/// The collection does not exist.
400		UnknownCollection,
401		/// Only the current collection owner may perform this operation.
402		NoPermission,
403		/// The current collection owner cannot be nominated as its replacement.
404		AlreadyCollectionOwner,
405		/// The signer is not the account currently nominated to claim this collection.
406		NotPendingCollectionOwner,
407		/// The per-collection item index space is exhausted.
408		TooManyItems,
409		/// The requested item definition does not exist.
410		UnknownItem,
411		/// An item with live instances cannot be deleted.
412		ItemInUse,
413		/// Item metadata must be removed before deleting the item.
414		ItemMetadataNotEmpty,
415		/// Item definitions must be deleted before deleting the collection.
416		CollectionItemsNotEmpty,
417		/// Collection metadata must be removed before deleting the collection.
418		CollectionMetadataNotEmpty,
419		/// Stored dependency counters or deposits do not permit deletion.
420		DeletionInvariant,
421		/// The destination purse key already holds an NFT.
422		AddressOccupied,
423		/// The permanent instance identifier space is exhausted.
424		TooManyInstances,
425		/// An item definition's minted supply is exhausted.
426		SupplyOverflow,
427		/// The requested live instance does not exist.
428		UnknownInstance,
429		/// The configured per-instance metadata entry limit was reached.
430		TooManyInstanceMetadata,
431		/// An NFT cannot be transferred to its current purse key.
432		SelfTransfer,
433		/// An NFT has exhausted its ownership-state nonce.
434		StateNonceOverflow,
435	}
436
437	/// Hold reason available to runtimes which back [`Config::Consideration`] with fungible holds.
438	#[pallet::composite_enum]
439	pub enum HoldReason {
440		/// Funds held for a collection's aggregate storage consideration.
441		StorageDeposit,
442	}
443
444	#[pallet::pallet]
445	pub struct Pallet<T>(_);
446
447	#[pallet::hooks]
448	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
449		#[cfg(feature = "try-runtime")]
450		fn try_state(_n: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
451			Self::do_try_state()
452		}
453	}
454
455	/// A Scarcity NFT held by the custom dispatch origin.
456	#[pallet::origin]
457	#[derive(
458		CloneNoBound,
459		PartialEqNoBound,
460		EqNoBound,
461		DebugNoBound,
462		Encode,
463		Decode,
464		DecodeWithMemTracking,
465		TypeInfo,
466		MaxEncodedLen,
467	)]
468	pub enum Origin<T: Config> {
469		/// The NFT is removed from storage by `AsScarcity` before dispatch and held by this
470		/// origin.
471		Nft { owner: T::AccountId, nft: Nft },
472	}
473
474	#[pallet::config]
475	pub trait Config:
476		frame_system::Config<
477			RuntimeOrigin: Into<Result<Origin<Self>, Self::RuntimeOrigin>> + From<Origin<Self>>,
478			RuntimeCall: IsSubType<Call<Self>>,
479		> + Send
480		+ Sync
481	{
482		#[allow(deprecated)]
483		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
484
485		/// A type representing the weights required by the dispatchables of this pallet.
486		type WeightInfo: crate::weights::WeightInfo;
487
488		/// Unix time source for `minted_at`, `last_moved`, rest-time priority, and failure locks.
489		type UnixTime: UnixTime;
490
491		/// Unit in which the pallet calculates aggregate storage deposits.
492		type Balance: Balance;
493
494		/// Cost mechanism backing one collection's aggregate storage deposit.
495		///
496		/// The input is the exact sum produced by the deposit converters below. Keeping one
497		/// ticket per collection makes ownership handoff constant-time without constraining the
498		/// runtime to fungible balance holds.
499		type Consideration: Consideration<Self::AccountId, BalanceOf<Self>>;
500
501		/// Price of a collection record.
502		type CollectionDeposit: Convert<Footprint, BalanceOf<Self>>;
503
504		/// Price of an item definition.
505		type ItemDeposit: Convert<Footprint, BalanceOf<Self>>;
506
507		/// Price of one ordinary minted instance.
508		type InstanceDeposit: Convert<Footprint, BalanceOf<Self>>;
509
510		/// Price of one metadata entry.
511		///
512		/// The footprint contains one logical record plus the encoded key and value. Trie-key and
513		/// hash-prefix overhead can be priced through the converter's per-record base.
514		type MetadataDeposit: Convert<Footprint, BalanceOf<Self>>;
515
516		/// Maximum byte length of one metadata key.
517		#[pallet::constant]
518		type MaxKeyLen: Get<u32>;
519
520		/// Maximum byte length of one metadata value.
521		#[pallet::constant]
522		type MaxValueLen: Get<u32>;
523
524		/// Maximum number of metadata overrides stored on one live instance.
525		///
526		/// This bounds burn cleanup independently of how many mutation calls are submitted.
527		#[pallet::constant]
528		type MaxInstanceMetadata: Get<u32>;
529
530		/// Base lock period after a failed purse-key dispatch, in seconds.
531		#[pallet::constant]
532		type LockPeriod: Get<u64>;
533
534		/// Priority ceiling for rested NFT transfers.
535		#[pallet::constant]
536		type MaxTransferPriority: Get<TransactionPriority>;
537	}
538
539	#[pallet::call]
540	impl<T: Config> Pallet<T> {
541		/// Create a collection owned by the signer.
542		#[pallet::call_index(0)]
543		#[pallet::weight(T::WeightInfo::create_collection())]
544		pub fn create_collection(origin: OriginFor<T>) -> DispatchResult {
545			let owner = ensure_signed(origin)?;
546			Self::do_create_collection(owner).map(|_| ())
547		}
548
549		/// Define one immutable item and its shared metadata defaults.
550		///
551		/// The supplied metadata is inherited by every instance minted from this definition.
552		/// Instance-specific values belong on [`Self::mint`] or [`Self::set_instance_metadata`].
553		#[pallet::call_index(1)]
554		#[pallet::weight(T::WeightInfo::define_item(metadata.len() as u32))]
555		pub fn define_item(
556			origin: OriginFor<T>,
557			collection: CollectionId,
558			metadata: Vec<(MetadataKeyOf<T>, MetadataValueOf<T>)>,
559		) -> DispatchResult {
560			let owner = ensure_signed(origin)?;
561			Self::do_define_item(owner, collection, metadata).map(|_| ())
562		}
563
564		/// Mint an instance of an immutable item definition into an empty purse key.
565		///
566		/// The destination gives no consent; any empty key is a valid target. See the module
567		/// documentation on purse-key occupancy.
568		///
569		/// `metadata` contains instance-specific overrides. Item metadata remains the shared
570		/// default for every instance minted from the definition.
571		#[pallet::call_index(2)]
572		#[pallet::weight(T::WeightInfo::mint(metadata.len() as u32))]
573		pub fn mint(
574			origin: OriginFor<T>,
575			collection: CollectionId,
576			item: ItemIndex,
577			to: T::AccountId,
578			metadata: Vec<(MetadataKeyOf<T>, MetadataValueOf<T>)>,
579		) -> DispatchResult {
580			let owner = ensure_signed(origin)?;
581			Self::do_mint(owner, collection, item, to, metadata).map(|_| ())
582		}
583
584		/// Transfer an NFT held by the `Origin::Nft` purse-key origin.
585		#[pallet::call_index(3)]
586		#[pallet::weight(T::WeightInfo::transfer())]
587		#[pallet::feeless_if(|origin: &OriginFor<T>, _to: &T::AccountId| -> bool {
588			Pallet::<T>::is_nft_origin(origin)
589		})]
590		pub fn transfer(origin: OriginFor<T>, to: T::AccountId) -> DispatchResultWithPostInfo {
591			let Ok(Origin::Nft { owner, nft }) = origin.into() else {
592				return Err(DispatchError::BadOrigin.into());
593			};
594			ensure!(to != owner, Error::<T>::SelfTransfer);
595			ensure!(!NftsByOwner::<T>::contains_key(&to), Error::<T>::AddressOccupied);
596
597			let from = owner;
598			let state_nonce =
599				nft.state_nonce.checked_add(1).ok_or(Error::<T>::StateNonceOverflow)?;
600			let nft = Nft { last_moved: T::UnixTime::now().as_secs(), state_nonce, ..nft };
601			NftsByOwner::<T>::insert(&to, nft.clone());
602			Instances::<T>::insert(nft.instance, &to);
603			Self::deposit_event(Event::Transferred { instance: nft.instance, from, to });
604			Ok(Pays::No.into())
605		}
606
607		/// Burn an NFT held by the `Origin::Nft` purse-key origin.
608		///
609		/// Item-definition supply counts minted-ever instances and is deliberately monotonic.
610		/// Item metadata belongs to the definition and remains untouched. Instance metadata is
611		/// removed and its exact deposits are released.
612		#[pallet::call_index(4)]
613		#[pallet::weight(T::WeightInfo::burn(T::MaxInstanceMetadata::get()))]
614		#[pallet::feeless_if(|origin: &OriginFor<T>| -> bool {
615			Pallet::<T>::is_nft_origin(origin)
616		})]
617		#[transactional]
618		pub fn burn(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
619			let Ok(Origin::Nft { owner: _, nft }) = origin.into() else {
620				return Err(DispatchError::BadOrigin.into());
621			};
622			let metadata_count = Self::do_burn(nft)?;
623			Ok((Some(T::WeightInfo::burn(metadata_count)), Pays::No).into())
624		}
625
626		/// Nominate an account to claim ownership of a collection.
627		///
628		/// Only the current owner may nominate or clear a prospective owner. Nomination does not
629		/// change authority or move deposits.
630		#[pallet::call_index(5)]
631		#[pallet::weight(T::WeightInfo::nominate_collection_owner())]
632		pub fn nominate_collection_owner(
633			origin: OriginFor<T>,
634			collection: CollectionId,
635			pending_owner: Option<T::AccountId>,
636		) -> DispatchResult {
637			let owner = ensure_signed(origin)?;
638			Collections::<T>::try_mutate(collection, |maybe_info| {
639				let info = maybe_info.as_mut().ok_or(Error::<T>::UnknownCollection)?;
640				ensure!(info.owner == owner, Error::<T>::NoPermission);
641				ensure!(
642					pending_owner.as_ref() != Some(&info.owner),
643					Error::<T>::AlreadyCollectionOwner
644				);
645				info.pending_owner = pending_owner.clone();
646				Ok::<_, DispatchError>(())
647			})?;
648			Self::deposit_event(Event::CollectionOwnerNominated { collection, pending_owner });
649			Ok(())
650		}
651
652		/// Set or remove a collection-level metadata default.
653		///
654		/// Only the collection owner may mutate metadata. `None` releases an existing entry's
655		/// deposit; when the key is absent it is a successful no-op.
656		#[pallet::call_index(6)]
657		#[pallet::weight(T::WeightInfo::set_collection_metadata())]
658		#[transactional]
659		pub fn set_collection_metadata(
660			origin: OriginFor<T>,
661			collection: CollectionId,
662			key: MetadataKeyOf<T>,
663			value: Option<MetadataValueOf<T>>,
664		) -> DispatchResult {
665			let owner = ensure_signed(origin)?;
666			Self::do_set_collection_metadata(&owner, collection, key, value)
667		}
668
669		/// Set or remove a metadata default shared by every instance of an item.
670		///
671		/// This scope overrides the collection default without changing any instance-specific
672		/// override. Only the collection owner may mutate metadata. `None` releases an existing
673		/// entry's deposit; when the key is absent it is a successful no-op.
674		#[pallet::call_index(7)]
675		#[pallet::weight(T::WeightInfo::set_item_metadata())]
676		#[transactional]
677		pub fn set_item_metadata(
678			origin: OriginFor<T>,
679			collection: CollectionId,
680			item: ItemIndex,
681			key: MetadataKeyOf<T>,
682			value: Option<MetadataValueOf<T>>,
683		) -> DispatchResult {
684			let owner = ensure_signed(origin)?;
685			Self::do_set_item_metadata(&owner, collection, item, key, value)
686		}
687
688		/// Claim a collection after nomination by its current owner.
689		///
690		/// An equivalent aggregate consideration is first created for the claimant and then the
691		/// previous owner's ticket is dropped. The operation is atomic: failure to establish the
692		/// claimant's consideration leaves ownership and both tickets unchanged.
693		#[pallet::call_index(8)]
694		#[pallet::weight(T::WeightInfo::claim_collection_ownership())]
695		#[transactional]
696		pub fn claim_collection_ownership(
697			origin: OriginFor<T>,
698			collection: CollectionId,
699		) -> DispatchResult {
700			let new_owner = ensure_signed(origin)?;
701			let info = Collections::<T>::get(collection).ok_or(Error::<T>::UnknownCollection)?;
702			ensure!(
703				info.pending_owner.as_ref() == Some(&new_owner),
704				Error::<T>::NotPendingCollectionOwner
705			);
706
707			let old_owner = info.owner.clone();
708			let info = Self::change_collection_owner(info, new_owner.clone())?;
709			Collections::<T>::insert(collection, info);
710			Self::deposit_event(Event::CollectionOwnerChanged { collection, old_owner, new_owner });
711			Ok(())
712		}
713
714		/// Set or remove an instance-specific metadata override.
715		///
716		/// Only the collection owner may mutate metadata. `None` releases an existing entry's
717		/// deposit; when the key is absent it is a successful no-op.
718		#[pallet::call_index(9)]
719		#[pallet::weight(T::WeightInfo::set_instance_metadata())]
720		#[transactional]
721		pub fn set_instance_metadata(
722			origin: OriginFor<T>,
723			instance: InstanceId,
724			key: MetadataKeyOf<T>,
725			value: Option<MetadataValueOf<T>>,
726		) -> DispatchResult {
727			let owner = ensure_signed(origin)?;
728			Self::do_set_instance_metadata(&owner, instance, key, value, true)
729		}
730
731		/// Force-burn one live instance as its collection owner.
732		///
733		/// The collection layer intentionally applies no holder-level ACL. When a runtime exposes
734		/// this call to its contract environment, a contract-owned collection can enforce its own
735		/// consent and game rules before calling it.
736		#[pallet::call_index(10)]
737		#[pallet::weight(T::WeightInfo::force_burn(T::MaxInstanceMetadata::get()))]
738		#[transactional]
739		pub fn force_burn(
740			origin: OriginFor<T>,
741			instance: InstanceId,
742		) -> DispatchResultWithPostInfo {
743			let owner = ensure_signed(origin)?;
744			let metadata_count = Self::do_force_burn(&owner, instance)?;
745			Ok(Some(T::WeightInfo::force_burn(metadata_count)).into())
746		}
747
748		/// Delete an unused item definition owned by the signer.
749		///
750		/// Every live instance must be burned and every item metadata entry removed first.
751		/// Deleted item indices are never reused.
752		#[pallet::call_index(11)]
753		#[pallet::weight(T::WeightInfo::delete_item())]
754		#[transactional]
755		pub fn delete_item(
756			origin: OriginFor<T>,
757			collection: CollectionId,
758			item: ItemIndex,
759		) -> DispatchResult {
760			let owner = ensure_signed(origin)?;
761			Self::do_delete_item(&owner, collection, item)
762		}
763
764		/// Delete an empty collection owned by the signer.
765		///
766		/// Every item definition and collection metadata entry must be removed first. Deleted
767		/// collection identifiers are never reused.
768		#[pallet::call_index(12)]
769		#[pallet::weight(T::WeightInfo::delete_collection())]
770		#[transactional]
771		pub fn delete_collection(origin: OriginFor<T>, collection: CollectionId) -> DispatchResult {
772			let owner = ensure_signed(origin)?;
773			Self::do_delete_collection(&owner, collection)
774		}
775
776		/// Force-transfer one live instance as its collection owner.
777		///
778		/// The collection layer intentionally applies no holder-level ACL. When a runtime exposes
779		/// this call to its contract environment, a contract-owned collection can enforce its own
780		/// consent and game rules before calling it. The move increments the instance state nonce,
781		/// invalidating prior holder authorizations.
782		#[pallet::call_index(13)]
783		#[pallet::weight(T::WeightInfo::force_transfer())]
784		#[transactional]
785		pub fn force_transfer(
786			origin: OriginFor<T>,
787			instance: InstanceId,
788			to: T::AccountId,
789		) -> DispatchResult {
790			let owner = ensure_signed(origin)?;
791			Self::do_force_transfer(&owner, instance, to)
792		}
793	}
794
795	impl<T: Config> Pallet<T> {
796		fn is_nft_origin(origin: &OriginFor<T>) -> bool {
797			let converted: Result<Origin<T>, OriginFor<T>> = origin.clone().into();
798			matches!(converted, Ok(Origin::Nft { .. }))
799		}
800
801		fn increase_owner_deposit(
802			info: CollectionInfoOf<T>,
803			amount: BalanceOf<T>,
804		) -> Result<CollectionInfoOf<T>, DispatchError> {
805			let total = info.owner_deposit.checked_add(&amount).ok_or(ArithmeticError::Overflow)?;
806			Self::update_owner_deposit(info, total)
807		}
808
809		fn decrease_owner_deposit(
810			info: CollectionInfoOf<T>,
811			amount: BalanceOf<T>,
812		) -> Result<CollectionInfoOf<T>, DispatchError> {
813			let total =
814				info.owner_deposit.checked_sub(&amount).ok_or(ArithmeticError::Underflow)?;
815			Self::update_owner_deposit(info, total)
816		}
817
818		fn replace_owner_deposit(
819			info: CollectionInfoOf<T>,
820			old: BalanceOf<T>,
821			new: BalanceOf<T>,
822		) -> Result<CollectionInfoOf<T>, DispatchError> {
823			let total_without_old =
824				info.owner_deposit.checked_sub(&old).ok_or(ArithmeticError::Underflow)?;
825			let total = total_without_old.checked_add(&new).ok_or(ArithmeticError::Overflow)?;
826			Self::update_owner_deposit(info, total)
827		}
828
829		fn update_owner_deposit(
830			info: CollectionInfoOf<T>,
831			owner_deposit: BalanceOf<T>,
832		) -> Result<CollectionInfoOf<T>, DispatchError> {
833			let CollectionInfo {
834				owner,
835				pending_owner,
836				next_item_index,
837				item_count,
838				metadata_count,
839				collection_deposit,
840				owner_deposit: _,
841				consideration,
842			} = info;
843			let consideration = consideration.update(&owner, owner_deposit)?;
844			Ok(CollectionInfo {
845				owner,
846				pending_owner,
847				next_item_index,
848				item_count,
849				metadata_count,
850				collection_deposit,
851				owner_deposit,
852				consideration,
853			})
854		}
855
856		fn change_collection_owner(
857			info: CollectionInfoOf<T>,
858			new_owner: T::AccountId,
859		) -> Result<CollectionInfoOf<T>, DispatchError> {
860			let CollectionInfo {
861				owner,
862				pending_owner: _,
863				next_item_index,
864				item_count,
865				metadata_count,
866				collection_deposit,
867				owner_deposit,
868				consideration: old_consideration,
869			} = info;
870			// Charge the successor before refunding the former owner. The enclosing dispatchable
871			// is transactional, so any failure leaves both tickets unchanged.
872			let consideration = T::Consideration::new(&new_owner, owner_deposit)?;
873			old_consideration.drop(&owner)?;
874			Ok(CollectionInfo {
875				owner: new_owner,
876				pending_owner: None,
877				next_item_index,
878				item_count,
879				metadata_count,
880				collection_deposit,
881				owner_deposit,
882				consideration,
883			})
884		}
885
886		/// Allocate a collection identifier and record its initial owner.
887		pub fn do_create_collection(owner: T::AccountId) -> Result<CollectionId, DispatchError> {
888			let collection = NextCollectionId::<T>::get();
889			let next_collection =
890				collection.checked_add(1).ok_or(Error::<T>::TooManyCollections)?;
891			let footprint = Footprint::from_mel::<CollectionInfoOf<T>>();
892			let collection_deposit = T::CollectionDeposit::convert(footprint);
893			let consideration = T::Consideration::new(&owner, collection_deposit)?;
894
895			NextCollectionId::<T>::put(next_collection);
896			Collections::<T>::insert(
897				collection,
898				CollectionInfo {
899					owner: owner.clone(),
900					pending_owner: None,
901					next_item_index: 0,
902					item_count: 0,
903					metadata_count: 0,
904					collection_deposit,
905					owner_deposit: collection_deposit,
906					consideration,
907				},
908			);
909			Self::deposit_event(Event::CollectionCreated { collection, owner });
910			Ok(collection)
911		}
912
913		/// Add an immutable item definition to an owned collection.
914		#[transactional]
915		pub fn do_define_item(
916			owner: T::AccountId,
917			collection: CollectionId,
918			metadata: Vec<(MetadataKeyOf<T>, MetadataValueOf<T>)>,
919		) -> Result<ItemIndex, DispatchError> {
920			let mut info =
921				Collections::<T>::get(collection).ok_or(Error::<T>::UnknownCollection)?;
922			ensure!(info.owner == owner, Error::<T>::NoPermission);
923
924			let item = info.next_item_index;
925			info.next_item_index = item.checked_add(1).ok_or(Error::<T>::TooManyItems)?;
926			info.item_count = info.item_count.checked_add(1).ok_or(Error::<T>::TooManyItems)?;
927			let footprint = Footprint::from_mel::<ItemDefinition<BalanceOf<T>>>();
928			let deposit = T::ItemDeposit::convert(footprint);
929			info = Self::increase_owner_deposit(info, deposit)?;
930			let definition =
931				ItemDefinition { supply: 0, live_supply: 0, metadata_count: 0, deposit };
932
933			Collections::<T>::insert(collection, info);
934			ItemDefs::<T>::insert(collection, item, definition);
935			Self::deposit_event(Event::ItemDefined { collection, item });
936			for (key, value) in metadata {
937				Self::do_set_item_metadata(&owner, collection, item, key, Some(value))?;
938			}
939			Ok(item)
940		}
941
942		/// Effective value for `key` on `(collection, item)`.
943		///
944		/// An item-level entry wins; otherwise the collection-level default is returned.
945		pub fn item_metadata_of(
946			collection: CollectionId,
947			item: ItemIndex,
948			key: &MetadataKeyOf<T>,
949		) -> Option<MetadataValueOf<T>> {
950			ItemMetadata::<T>::get((collection, item, key.clone()))
951				.map(|entry| entry.value)
952				.or_else(|| Self::collection_metadata_of(collection, key))
953		}
954
955		/// Effective value for `key` on a live minted instance.
956		///
957		/// An instance-level entry wins, followed by the item definition and then collection.
958		pub fn instance_metadata_of(
959			instance: InstanceId,
960			key: &MetadataKeyOf<T>,
961		) -> Option<MetadataValueOf<T>> {
962			let owner = Instances::<T>::get(instance)?;
963			let nft = NftsByOwner::<T>::get(owner)?;
964			if nft.instance != instance {
965				return None;
966			}
967			InstanceMetadata::<T>::get(instance, key)
968				.map(|entry| entry.value)
969				.or_else(|| Self::item_metadata_of(nft.collection, nft.item, key))
970		}
971
972		/// Only the collection-level value for `key`, without item-level resolution.
973		pub fn collection_metadata_of(
974			collection: CollectionId,
975			key: &MetadataKeyOf<T>,
976		) -> Option<MetadataValueOf<T>> {
977			CollectionMetadata::<T>::get(collection, key).map(|entry| entry.value)
978		}
979
980		fn metadata_footprint(key: &MetadataKeyOf<T>, value: &MetadataValueOf<T>) -> Footprint {
981			// Logical record footprint. Runtime pricing may include trie/hash overhead in its
982			// per-record base rather than coupling this pallet to a storage backend.
983			Footprint::from_parts(
984				1,
985				key.len()
986					.saturating_add(value.len())
987					.saturating_add(BalanceOf::<T>::max_encoded_len()),
988			)
989		}
990
991		fn do_set_collection_metadata(
992			owner: &T::AccountId,
993			collection: CollectionId,
994			key: MetadataKeyOf<T>,
995			value: Option<MetadataValueOf<T>>,
996		) -> DispatchResult {
997			let mut info =
998				Collections::<T>::get(collection).ok_or(Error::<T>::UnknownCollection)?;
999			ensure!(info.owner == *owner, Error::<T>::NoPermission);
1000
1001			match (CollectionMetadata::<T>::get(collection, &key), value) {
1002				(Some(entry), Some(value)) => {
1003					let deposit =
1004						T::MetadataDeposit::convert(Self::metadata_footprint(&key, &value));
1005					info = Self::replace_owner_deposit(info, entry.deposit, deposit)?;
1006					CollectionMetadata::<T>::insert(
1007						collection,
1008						&key,
1009						MetadataEntry { value, deposit },
1010					);
1011					Self::deposit_event(Event::CollectionMetadataSet { collection, key });
1012				},
1013				(None, Some(value)) => {
1014					info.metadata_count =
1015						info.metadata_count.checked_add(1).ok_or(ArithmeticError::Overflow)?;
1016					let deposit =
1017						T::MetadataDeposit::convert(Self::metadata_footprint(&key, &value));
1018					info = Self::increase_owner_deposit(info, deposit)?;
1019					CollectionMetadata::<T>::insert(
1020						collection,
1021						&key,
1022						MetadataEntry { value, deposit },
1023					);
1024					Self::deposit_event(Event::CollectionMetadataSet { collection, key });
1025				},
1026				(Some(entry), None) => {
1027					info.metadata_count =
1028						info.metadata_count.checked_sub(1).ok_or(ArithmeticError::Underflow)?;
1029					info = Self::decrease_owner_deposit(info, entry.deposit)?;
1030					CollectionMetadata::<T>::remove(collection, &key);
1031					Self::deposit_event(Event::CollectionMetadataRemoved { collection, key });
1032				},
1033				(None, None) => {},
1034			}
1035			Collections::<T>::insert(collection, info);
1036			Ok(())
1037		}
1038
1039		fn do_set_item_metadata(
1040			owner: &T::AccountId,
1041			collection: CollectionId,
1042			item: ItemIndex,
1043			key: MetadataKeyOf<T>,
1044			value: Option<MetadataValueOf<T>>,
1045		) -> DispatchResult {
1046			let mut info =
1047				Collections::<T>::get(collection).ok_or(Error::<T>::UnknownCollection)?;
1048			ensure!(info.owner == *owner, Error::<T>::NoPermission);
1049			let mut definition =
1050				ItemDefs::<T>::get(collection, item).ok_or(Error::<T>::UnknownItem)?;
1051
1052			match (ItemMetadata::<T>::get((collection, item, key.clone())), value) {
1053				(Some(entry), Some(value)) => {
1054					let deposit =
1055						T::MetadataDeposit::convert(Self::metadata_footprint(&key, &value));
1056					info = Self::replace_owner_deposit(info, entry.deposit, deposit)?;
1057					ItemMetadata::<T>::insert(
1058						(collection, item, key.clone()),
1059						MetadataEntry { value, deposit },
1060					);
1061					Self::deposit_event(Event::ItemMetadataSet { collection, item, key });
1062				},
1063				(None, Some(value)) => {
1064					definition.metadata_count = definition
1065						.metadata_count
1066						.checked_add(1)
1067						.ok_or(ArithmeticError::Overflow)?;
1068					let deposit =
1069						T::MetadataDeposit::convert(Self::metadata_footprint(&key, &value));
1070					info = Self::increase_owner_deposit(info, deposit)?;
1071					ItemMetadata::<T>::insert(
1072						(collection, item, key.clone()),
1073						MetadataEntry { value, deposit },
1074					);
1075					ItemDefs::<T>::insert(collection, item, definition);
1076					Self::deposit_event(Event::ItemMetadataSet { collection, item, key });
1077				},
1078				(Some(entry), None) => {
1079					definition.metadata_count = definition
1080						.metadata_count
1081						.checked_sub(1)
1082						.ok_or(ArithmeticError::Underflow)?;
1083					info = Self::decrease_owner_deposit(info, entry.deposit)?;
1084					ItemMetadata::<T>::remove((collection, item, key.clone()));
1085					ItemDefs::<T>::insert(collection, item, definition);
1086					Self::deposit_event(Event::ItemMetadataRemoved { collection, item, key });
1087				},
1088				(None, None) => {},
1089			}
1090			Collections::<T>::insert(collection, info);
1091			Ok(())
1092		}
1093
1094		fn do_set_instance_metadata(
1095			owner: &T::AccountId,
1096			instance: InstanceId,
1097			key: MetadataKeyOf<T>,
1098			value: Option<MetadataValueOf<T>>,
1099			with_deposit: bool,
1100		) -> DispatchResult {
1101			let purse = Instances::<T>::get(instance).ok_or(Error::<T>::UnknownInstance)?;
1102			let nft = NftsByOwner::<T>::get(purse).ok_or(Error::<T>::UnknownInstance)?;
1103			ensure!(nft.instance == instance, Error::<T>::UnknownInstance);
1104
1105			let mut info =
1106				Collections::<T>::get(nft.collection).ok_or(Error::<T>::UnknownCollection)?;
1107			ensure!(info.owner == *owner, Error::<T>::NoPermission);
1108
1109			match (InstanceMetadata::<T>::get(instance, &key), value) {
1110				(Some(entry), Some(value)) => {
1111					let deposit = if with_deposit {
1112						T::MetadataDeposit::convert(Self::metadata_footprint(&key, &value))
1113					} else {
1114						Zero::zero()
1115					};
1116					info = Self::replace_owner_deposit(info, entry.deposit, deposit)?;
1117					InstanceMetadata::<T>::insert(instance, &key, MetadataEntry { value, deposit });
1118					Self::deposit_event(Event::InstanceMetadataSet { instance, key });
1119				},
1120				(None, Some(value)) => {
1121					let count = InstanceMetadataCount::<T>::get(instance);
1122					ensure!(
1123						count < T::MaxInstanceMetadata::get(),
1124						Error::<T>::TooManyInstanceMetadata
1125					);
1126					let next_count = count.checked_add(1).ok_or(ArithmeticError::Overflow)?;
1127					let deposit = if with_deposit {
1128						T::MetadataDeposit::convert(Self::metadata_footprint(&key, &value))
1129					} else {
1130						Zero::zero()
1131					};
1132					info = Self::increase_owner_deposit(info, deposit)?;
1133					InstanceMetadata::<T>::insert(instance, &key, MetadataEntry { value, deposit });
1134					InstanceMetadataCount::<T>::insert(instance, next_count);
1135					Self::deposit_event(Event::InstanceMetadataSet { instance, key });
1136				},
1137				(Some(entry), None) => {
1138					let count = InstanceMetadataCount::<T>::get(instance);
1139					let next_count = count.checked_sub(1).ok_or(ArithmeticError::Underflow)?;
1140					info = Self::decrease_owner_deposit(info, entry.deposit)?;
1141					InstanceMetadata::<T>::remove(instance, &key);
1142					InstanceMetadataCount::<T>::insert(instance, next_count);
1143					Self::deposit_event(Event::InstanceMetadataRemoved { instance, key });
1144				},
1145				(None, None) => {},
1146			}
1147			Collections::<T>::insert(nft.collection, info);
1148			Ok(())
1149		}
1150
1151		fn do_burn(nft: Nft) -> Result<u32, DispatchError> {
1152			let instance = nft.instance;
1153			let expected_metadata_count = InstanceMetadataCount::<T>::take(instance);
1154			let mut actual_metadata_count = 0u32;
1155			let mut deposit = InstanceDeposits::<T>::take(instance).unwrap_or_else(Zero::zero);
1156			for (_, entry) in InstanceMetadata::<T>::drain_prefix(instance) {
1157				actual_metadata_count = actual_metadata_count.saturating_add(1);
1158				deposit = deposit.checked_add(&entry.deposit).ok_or(ArithmeticError::Overflow)?;
1159			}
1160			debug_assert_eq!(actual_metadata_count, expected_metadata_count);
1161
1162			ItemDefs::<T>::try_mutate(nft.collection, nft.item, |maybe_definition| {
1163				let definition = maybe_definition.as_mut().ok_or(Error::<T>::UnknownItem)?;
1164				definition.live_supply =
1165					definition.live_supply.checked_sub(1).ok_or(ArithmeticError::Underflow)?;
1166				Ok::<_, DispatchError>(())
1167			})?;
1168			let info =
1169				Collections::<T>::get(nft.collection).ok_or(Error::<T>::UnknownCollection)?;
1170			let info = Self::decrease_owner_deposit(info, deposit)?;
1171			Collections::<T>::insert(nft.collection, info);
1172			Instances::<T>::remove(instance);
1173			Self::deposit_event(Event::Burned { instance });
1174			Ok(actual_metadata_count)
1175		}
1176
1177		fn do_force_burn(owner: &T::AccountId, instance: InstanceId) -> Result<u32, DispatchError> {
1178			let purse = Instances::<T>::get(instance).ok_or(Error::<T>::UnknownInstance)?;
1179			let nft = NftsByOwner::<T>::get(&purse).ok_or(Error::<T>::UnknownInstance)?;
1180			ensure!(nft.instance == instance, Error::<T>::UnknownInstance);
1181			let info =
1182				Collections::<T>::get(nft.collection).ok_or(Error::<T>::UnknownCollection)?;
1183			ensure!(info.owner == *owner, Error::<T>::NoPermission);
1184
1185			NftsByOwner::<T>::remove(&purse);
1186			Locked::<T>::remove(&purse);
1187			Self::do_burn(nft)
1188		}
1189
1190		fn do_force_transfer(
1191			owner: &T::AccountId,
1192			instance: InstanceId,
1193			to: T::AccountId,
1194		) -> DispatchResult {
1195			let from = Instances::<T>::get(instance).ok_or(Error::<T>::UnknownInstance)?;
1196			let nft = NftsByOwner::<T>::get(&from).ok_or(Error::<T>::UnknownInstance)?;
1197			ensure!(nft.instance == instance, Error::<T>::UnknownInstance);
1198			let info =
1199				Collections::<T>::get(nft.collection).ok_or(Error::<T>::UnknownCollection)?;
1200			ensure!(info.owner == *owner, Error::<T>::NoPermission);
1201			ensure!(to != from, Error::<T>::SelfTransfer);
1202			ensure!(!NftsByOwner::<T>::contains_key(&to), Error::<T>::AddressOccupied);
1203
1204			let state_nonce =
1205				nft.state_nonce.checked_add(1).ok_or(Error::<T>::StateNonceOverflow)?;
1206			let nft = Nft { last_moved: T::UnixTime::now().as_secs(), state_nonce, ..nft };
1207			NftsByOwner::<T>::remove(&from);
1208			Locked::<T>::remove(&from);
1209			NftsByOwner::<T>::insert(&to, nft);
1210			Instances::<T>::insert(instance, &to);
1211			Self::deposit_event(Event::ForceTransferred { instance, from, to });
1212			Ok(())
1213		}
1214
1215		fn do_delete_item(
1216			owner: &T::AccountId,
1217			collection: CollectionId,
1218			item: ItemIndex,
1219		) -> DispatchResult {
1220			let mut info =
1221				Collections::<T>::get(collection).ok_or(Error::<T>::UnknownCollection)?;
1222			ensure!(info.owner == *owner, Error::<T>::NoPermission);
1223			let definition = ItemDefs::<T>::get(collection, item).ok_or(Error::<T>::UnknownItem)?;
1224			ensure!(definition.live_supply == 0, Error::<T>::ItemInUse);
1225			ensure!(definition.metadata_count == 0, Error::<T>::ItemMetadataNotEmpty);
1226			ensure!(
1227				ItemMetadata::<T>::iter_prefix((collection, item)).next().is_none(),
1228				Error::<T>::DeletionInvariant
1229			);
1230
1231			info.item_count = info.item_count.checked_sub(1).ok_or(ArithmeticError::Underflow)?;
1232			info = Self::decrease_owner_deposit(info, definition.deposit)?;
1233			Collections::<T>::insert(collection, info);
1234			ItemDefs::<T>::remove(collection, item);
1235			Self::deposit_event(Event::ItemDeleted { collection, item });
1236			Ok(())
1237		}
1238
1239		fn do_delete_collection(owner: &T::AccountId, collection: CollectionId) -> DispatchResult {
1240			let info = Collections::<T>::get(collection).ok_or(Error::<T>::UnknownCollection)?;
1241			ensure!(info.owner == *owner, Error::<T>::NoPermission);
1242			ensure!(info.item_count == 0, Error::<T>::CollectionItemsNotEmpty);
1243			ensure!(info.metadata_count == 0, Error::<T>::CollectionMetadataNotEmpty);
1244			ensure!(
1245				ItemDefs::<T>::iter_prefix(collection).next().is_none(),
1246				Error::<T>::DeletionInvariant
1247			);
1248			ensure!(
1249				CollectionMetadata::<T>::iter_prefix(collection).next().is_none(),
1250				Error::<T>::DeletionInvariant
1251			);
1252			ensure!(info.owner_deposit == info.collection_deposit, Error::<T>::DeletionInvariant);
1253
1254			let CollectionInfo { owner, consideration, .. } = info;
1255			consideration.drop(&owner)?;
1256			Collections::<T>::remove(collection);
1257			Self::deposit_event(Event::CollectionDeleted { collection });
1258			Ok(())
1259		}
1260
1261		/// Mint an instance after enforcing collection ownership and the one-NFT-per-key rule.
1262		pub fn do_mint(
1263			owner: T::AccountId,
1264			collection: CollectionId,
1265			item: ItemIndex,
1266			to: T::AccountId,
1267			metadata: Vec<(MetadataKeyOf<T>, MetadataValueOf<T>)>,
1268		) -> Result<InstanceId, DispatchError> {
1269			let info = Collections::<T>::get(collection).ok_or(Error::<T>::UnknownCollection)?;
1270			ensure!(info.owner == owner, Error::<T>::NoPermission);
1271			Self::do_mint_inner(collection, item, to, metadata, true)
1272		}
1273
1274		#[transactional]
1275		fn do_mint_inner(
1276			collection: CollectionId,
1277			item: ItemIndex,
1278			to: T::AccountId,
1279			metadata: Vec<(MetadataKeyOf<T>, MetadataValueOf<T>)>,
1280			with_deposit: bool,
1281		) -> Result<InstanceId, DispatchError> {
1282			ensure!(
1283				metadata.len() <= T::MaxInstanceMetadata::get() as usize,
1284				Error::<T>::TooManyInstanceMetadata
1285			);
1286			let mut info =
1287				Collections::<T>::get(collection).ok_or(Error::<T>::UnknownCollection)?;
1288			let mut definition =
1289				ItemDefs::<T>::get(collection, item).ok_or(Error::<T>::UnknownItem)?;
1290			ensure!(!NftsByOwner::<T>::contains_key(&to), Error::<T>::AddressOccupied);
1291
1292			let instance = NextInstanceId::<T>::get();
1293			let next_instance = instance.checked_add(1).ok_or(Error::<T>::TooManyInstances)?;
1294			let next_supply = definition.supply.checked_add(1).ok_or(Error::<T>::SupplyOverflow)?;
1295			let next_live_supply =
1296				definition.live_supply.checked_add(1).ok_or(Error::<T>::SupplyOverflow)?;
1297			let now = T::UnixTime::now().as_secs();
1298			let nft =
1299				Nft { instance, collection, item, minted_at: now, last_moved: now, state_nonce: 0 };
1300			let instance_deposit = if with_deposit {
1301				// Four storage entries back one instance: `NftsByOwner`, the `Instances`
1302				// reverse index, `InstanceDeposits`, and `InstanceMetadataCount`. This measures
1303				// their logical encoded payload; the runtime's per-record base can price trie
1304				// and hash-prefix overhead.
1305				let record_size = nft
1306					.encoded_size()
1307					.saturating_add(to.encoded_size().saturating_mul(2))
1308					.saturating_add(instance.encoded_size().saturating_mul(3))
1309					.saturating_add(BalanceOf::<T>::max_encoded_len())
1310					.saturating_add(u32::max_encoded_len());
1311				let deposit = T::InstanceDeposit::convert(Footprint::from_parts(4, record_size));
1312				info = Self::increase_owner_deposit(info, deposit)?;
1313				Some(deposit)
1314			} else {
1315				None
1316			};
1317
1318			definition.supply = next_supply;
1319			definition.live_supply = next_live_supply;
1320			NextInstanceId::<T>::put(next_instance);
1321			let collection_owner = info.owner.clone();
1322			Collections::<T>::insert(collection, info);
1323			ItemDefs::<T>::insert(collection, item, definition);
1324			NftsByOwner::<T>::insert(&to, nft);
1325			Instances::<T>::insert(instance, &to);
1326			InstanceMetadataCount::<T>::insert(instance, 0);
1327			if let Some(deposit) = instance_deposit {
1328				InstanceDeposits::<T>::insert(instance, deposit);
1329			}
1330			for (key, value) in metadata {
1331				Self::do_set_instance_metadata(
1332					&collection_owner,
1333					instance,
1334					key,
1335					Some(value),
1336					with_deposit,
1337				)?;
1338			}
1339			Self::deposit_event(Event::Minted { instance, collection, item, owner: to });
1340			Ok(instance)
1341		}
1342	}
1343
1344	impl<T: Config> crate::MintWithoutDeposit<T::AccountId> for Pallet<T> {
1345		type MetadataKey = MetadataKeyOf<T>;
1346		type MetadataValue = MetadataValueOf<T>;
1347
1348		fn mint_without_deposit(
1349			collection: CollectionId,
1350			item: ItemIndex,
1351			to: T::AccountId,
1352			metadata: Vec<(Self::MetadataKey, Self::MetadataValue)>,
1353		) -> Result<InstanceId, DispatchError> {
1354			ensure!(Collections::<T>::contains_key(collection), Error::<T>::UnknownCollection);
1355			Self::do_mint_inner(collection, item, to, metadata, false)
1356		}
1357	}
1358
1359	#[cfg(any(test, feature = "try-runtime"))]
1360	impl<T: Config> Pallet<T> {
1361		/// Check allocation counters, catalogue references, ownership indexes, deposits, locks,
1362		/// and metadata references.
1363		pub(crate) fn do_try_state() -> Result<(), TryRuntimeError> {
1364			let next_collection = NextCollectionId::<T>::get();
1365			let next_instance = NextInstanceId::<T>::get();
1366			let mut expected_collection_deposits = BTreeMap::<CollectionId, BalanceOf<T>>::new();
1367
1368			for (collection, info) in Collections::<T>::iter() {
1369				if collection >= next_collection {
1370					return Err(TryRuntimeError::Other(
1371						"collection identifier is not below NextCollectionId",
1372					));
1373				}
1374				expected_collection_deposits.insert(collection, info.collection_deposit);
1375			}
1376
1377			let mut actual_item_counts = BTreeMap::<CollectionId, u32>::new();
1378			for (collection, item, definition) in ItemDefs::<T>::iter() {
1379				let info = Collections::<T>::get(collection)
1380					.ok_or(TryRuntimeError::Other("ItemDefs entry has no matching collection"))?;
1381				if item >= info.next_item_index {
1382					return Err(TryRuntimeError::Other(
1383						"item index is not below the collection's next item index",
1384					));
1385				}
1386				if definition.live_supply > definition.supply {
1387					return Err(TryRuntimeError::Other(
1388						"item live supply exceeds its minted supply",
1389					));
1390				}
1391				let item_count = actual_item_counts.entry(collection).or_default();
1392				*item_count = item_count
1393					.checked_add(1)
1394					.ok_or(TryRuntimeError::Other("collection item count overflowed"))?;
1395				let expected = expected_collection_deposits
1396					.get_mut(&collection)
1397					.ok_or(TryRuntimeError::Other("ItemDefs entry has no deposit aggregate"))?;
1398				*expected = expected
1399					.checked_add(&definition.deposit)
1400					.ok_or(TryRuntimeError::Other("collection deposit aggregate overflowed"))?;
1401			}
1402			for (collection, info) in Collections::<T>::iter() {
1403				let actual = actual_item_counts.get(&collection).copied().unwrap_or_default();
1404				if info.item_count != actual {
1405					return Err(TryRuntimeError::Other(
1406						"collection item count does not match stored definitions",
1407					));
1408				}
1409			}
1410
1411			let mut live_by_item = BTreeMap::<(CollectionId, ItemIndex), u64>::new();
1412			for (owner, nft) in NftsByOwner::<T>::iter() {
1413				if nft.instance >= next_instance {
1414					return Err(TryRuntimeError::Other("NFT instance is not below NextInstanceId"));
1415				}
1416				if Instances::<T>::get(nft.instance) != Some(owner) {
1417					return Err(TryRuntimeError::Other(
1418						"NftsByOwner entry has no matching Instances entry",
1419					));
1420				}
1421				let definition = ItemDefs::<T>::get(nft.collection, nft.item)
1422					.ok_or(TryRuntimeError::Other("NFT has no matching item definition"))?;
1423				let live = live_by_item.entry((nft.collection, nft.item)).or_default();
1424				*live += 1;
1425				if *live > u64::from(definition.live_supply) {
1426					return Err(TryRuntimeError::Other(
1427						"item live supply is below its stored instance count",
1428					));
1429				}
1430			}
1431			for (collection, item, definition) in ItemDefs::<T>::iter() {
1432				let actual = live_by_item.get(&(collection, item)).copied().unwrap_or_default();
1433				if u64::from(definition.live_supply) != actual {
1434					return Err(TryRuntimeError::Other(
1435						"item live supply does not match stored instances",
1436					));
1437				}
1438			}
1439
1440			for (instance, owner) in Instances::<T>::iter() {
1441				if instance >= next_instance {
1442					return Err(TryRuntimeError::Other(
1443						"Instances identifier is not below NextInstanceId",
1444					));
1445				}
1446				if !InstanceMetadataCount::<T>::contains_key(instance) {
1447					return Err(TryRuntimeError::Other(
1448						"live instance has no metadata count entry",
1449					));
1450				}
1451				if !matches!(
1452					NftsByOwner::<T>::get(&owner),
1453					Some(nft) if nft.instance == instance
1454				) {
1455					return Err(TryRuntimeError::Other(
1456						"Instances entry has no matching NftsByOwner entry",
1457					));
1458				}
1459			}
1460
1461			for (instance, deposit) in InstanceDeposits::<T>::iter() {
1462				if instance >= next_instance {
1463					return Err(TryRuntimeError::Other(
1464						"InstanceDeposits identifier is not below NextInstanceId",
1465					));
1466				}
1467				if !Instances::<T>::contains_key(instance) {
1468					return Err(TryRuntimeError::Other(
1469						"InstanceDeposits entry has no matching instance",
1470					));
1471				}
1472				let owner = Instances::<T>::get(instance).ok_or(TryRuntimeError::Other(
1473					"InstanceDeposits entry has no matching owner",
1474				))?;
1475				let nft = NftsByOwner::<T>::get(owner)
1476					.ok_or(TryRuntimeError::Other("InstanceDeposits entry has no matching NFT"))?;
1477				let expected = expected_collection_deposits.get_mut(&nft.collection).ok_or(
1478					TryRuntimeError::Other(
1479						"InstanceDeposits entry has no collection deposit aggregate",
1480					),
1481				)?;
1482				*expected = expected
1483					.checked_add(&deposit)
1484					.ok_or(TryRuntimeError::Other("collection deposit aggregate overflowed"))?;
1485			}
1486
1487			let mut actual_instance_metadata_counts = BTreeMap::<InstanceId, u32>::new();
1488			for (instance, _, entry) in InstanceMetadata::<T>::iter() {
1489				if instance >= next_instance {
1490					return Err(TryRuntimeError::Other(
1491						"InstanceMetadata identifier is not below NextInstanceId",
1492					));
1493				}
1494				let owner = Instances::<T>::get(instance).ok_or(TryRuntimeError::Other(
1495					"InstanceMetadata entry has no matching instance",
1496				))?;
1497				let nft = NftsByOwner::<T>::get(owner)
1498					.ok_or(TryRuntimeError::Other("InstanceMetadata entry has no matching NFT"))?;
1499				if nft.instance != instance {
1500					return Err(TryRuntimeError::Other(
1501						"InstanceMetadata entry resolves to a different NFT",
1502					));
1503				}
1504				let count = actual_instance_metadata_counts.entry(instance).or_default();
1505				*count = count
1506					.checked_add(1)
1507					.ok_or(TryRuntimeError::Other("instance metadata count overflowed"))?;
1508				if *count > T::MaxInstanceMetadata::get() {
1509					return Err(TryRuntimeError::Other(
1510						"instance metadata count exceeds configured maximum",
1511					));
1512				}
1513				let expected = expected_collection_deposits.get_mut(&nft.collection).ok_or(
1514					TryRuntimeError::Other("InstanceMetadata entry has no deposit aggregate"),
1515				)?;
1516				*expected = expected
1517					.checked_add(&entry.deposit)
1518					.ok_or(TryRuntimeError::Other("collection deposit aggregate overflowed"))?;
1519			}
1520
1521			for (instance, declared_count) in InstanceMetadataCount::<T>::iter() {
1522				if !Instances::<T>::contains_key(instance) {
1523					return Err(TryRuntimeError::Other(
1524						"InstanceMetadataCount entry has no matching instance",
1525					));
1526				}
1527				let actual_count =
1528					actual_instance_metadata_counts.get(&instance).copied().unwrap_or_default();
1529				if declared_count != actual_count {
1530					return Err(TryRuntimeError::Other(
1531						"instance metadata count does not match stored entries",
1532					));
1533				}
1534				if declared_count > T::MaxInstanceMetadata::get() {
1535					return Err(TryRuntimeError::Other(
1536						"instance metadata count exceeds configured maximum",
1537					));
1538				}
1539			}
1540
1541			for (owner, lock) in Locked::<T>::iter() {
1542				if !NftsByOwner::<T>::contains_key(owner) {
1543					return Err(TryRuntimeError::Other("Locked entry has no matching NFT"));
1544				}
1545				if lock.retries == 0 {
1546					return Err(TryRuntimeError::Other("Locked retry count must begin at one"));
1547				}
1548			}
1549
1550			let mut actual_collection_metadata_counts = BTreeMap::<CollectionId, u32>::new();
1551			for (collection, _, entry) in CollectionMetadata::<T>::iter() {
1552				if !Collections::<T>::contains_key(collection) {
1553					return Err(TryRuntimeError::Other(
1554						"CollectionMetadata entry has no matching collection",
1555					));
1556				}
1557				let count = actual_collection_metadata_counts.entry(collection).or_default();
1558				*count = count
1559					.checked_add(1)
1560					.ok_or(TryRuntimeError::Other("collection metadata count overflowed"))?;
1561				let expected = expected_collection_deposits.get_mut(&collection).ok_or(
1562					TryRuntimeError::Other("CollectionMetadata entry has no deposit aggregate"),
1563				)?;
1564				*expected = expected
1565					.checked_add(&entry.deposit)
1566					.ok_or(TryRuntimeError::Other("collection deposit aggregate overflowed"))?;
1567			}
1568			for (collection, info) in Collections::<T>::iter() {
1569				let actual =
1570					actual_collection_metadata_counts.get(&collection).copied().unwrap_or_default();
1571				if info.metadata_count != actual {
1572					return Err(TryRuntimeError::Other(
1573						"collection metadata count does not match stored entries",
1574					));
1575				}
1576			}
1577
1578			let mut actual_item_metadata_counts = BTreeMap::<(CollectionId, ItemIndex), u32>::new();
1579			for ((collection, item, _), entry) in ItemMetadata::<T>::iter() {
1580				if !Collections::<T>::contains_key(collection) {
1581					return Err(TryRuntimeError::Other(
1582						"ItemMetadata entry has no matching collection",
1583					));
1584				}
1585				if !ItemDefs::<T>::contains_key(collection, item) {
1586					return Err(TryRuntimeError::Other(
1587						"ItemMetadata entry has no matching item definition",
1588					));
1589				}
1590				let count = actual_item_metadata_counts.entry((collection, item)).or_default();
1591				*count = count
1592					.checked_add(1)
1593					.ok_or(TryRuntimeError::Other("item metadata count overflowed"))?;
1594				let expected = expected_collection_deposits
1595					.get_mut(&collection)
1596					.ok_or(TryRuntimeError::Other("ItemMetadata entry has no deposit aggregate"))?;
1597				*expected = expected
1598					.checked_add(&entry.deposit)
1599					.ok_or(TryRuntimeError::Other("collection deposit aggregate overflowed"))?;
1600			}
1601			for (collection, item, definition) in ItemDefs::<T>::iter() {
1602				let actual = actual_item_metadata_counts
1603					.get(&(collection, item))
1604					.copied()
1605					.unwrap_or_default();
1606				if definition.metadata_count != actual {
1607					return Err(TryRuntimeError::Other(
1608						"item metadata count does not match stored entries",
1609					));
1610				}
1611			}
1612
1613			for (collection, info) in Collections::<T>::iter() {
1614				let expected = expected_collection_deposits
1615					.get(&collection)
1616					.ok_or(TryRuntimeError::Other("collection has no deposit aggregate"))?;
1617				if info.owner_deposit != *expected {
1618					return Err(TryRuntimeError::Other(
1619						"collection owner deposit does not match its stored components",
1620					));
1621				}
1622			}
1623
1624			Ok(())
1625		}
1626	}
1627}