Skip to content

Commit

Permalink
Merge pull request #190 from rmrk-team/bug/147-decouple-equip-and-une…
Browse files Browse the repository at this point in the history
…quip

decouple equip and unequip
  • Loading branch information
ilionic authored Jul 28, 2022
2 parents 54ec2ee + 82d3f70 commit b56ef02
Show file tree
Hide file tree
Showing 5 changed files with 322 additions and 137 deletions.
6 changes: 6 additions & 0 deletions pallets/rmrk-core/src/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,12 @@ where
Ok(())
}

/// Helper function for checking if an NFT exists
pub fn nft_exists(item: (CollectionId, NftId)) -> bool {
let (item_collection_id, item_nft_id) = item;
Nfts::<T>::get(item_collection_id, item_nft_id).is_some()
}

// Check NFT is not equipped
pub fn check_is_not_equipped(nft: &InstanceInfoOf<T>) -> DispatchResult {
ensure!(!nft.equipped, Error::<T>::CannotSendEquippedItem);
Expand Down
259 changes: 159 additions & 100 deletions pallets/rmrk-equip/src/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,52 +32,53 @@ impl<T: Config> Pallet<T> {
})
}

pub fn iterate_part_types(base_id: BaseId) -> impl Iterator<Item=PartTypeOf<T>> {
/// Helper function for checking if an item is equipped
/// If the Equippings storage contains the Base/Slot for the Collection+NFT ID, the item is
/// already equipped
pub fn slot_is_equipped(item: (CollectionId, NftId), base_id: BaseId, slot_id: SlotId) -> bool {
let (equipper_collection_id, equipper_nft_id) = item;
Equippings::<T>::get(((equipper_collection_id, equipper_nft_id), base_id, slot_id))
.is_some()
}

pub fn iterate_part_types(base_id: BaseId) -> impl Iterator<Item = PartTypeOf<T>> {
Parts::<T>::iter_prefix_values(base_id)
}

pub fn iterate_theme_names(base_id: BaseId) -> impl Iterator<Item=StringLimitOf<T>> {
Themes::<T>::iter_key_prefix((base_id,))
.map(|(theme_name, ..)| theme_name)
pub fn iterate_theme_names(base_id: BaseId) -> impl Iterator<Item = StringLimitOf<T>> {
Themes::<T>::iter_key_prefix((base_id,)).map(|(theme_name, ..)| theme_name)
}

pub fn get_theme(
base_id: BaseId,
theme_name: StringLimitOf<T>,
filter_keys: Option<BTreeSet<StringLimitOf<T>>>
filter_keys: Option<BTreeSet<StringLimitOf<T>>>,
) -> Result<Option<BoundedThemeOf<T>>, Error<T>> {
let properties: BoundedThemePropertiesOf<T> = Self::query_theme_kv(base_id, &theme_name, filter_keys)?;
let properties: BoundedThemePropertiesOf<T> =
Self::query_theme_kv(base_id, &theme_name, filter_keys)?;

if properties.is_empty() {
Ok(None)
} else {
Ok(Some(BoundedThemeOf::<T> {
name: theme_name,
properties,
inherit: false,
}))
Ok(Some(BoundedThemeOf::<T> { name: theme_name, properties, inherit: false }))
}
}

fn query_theme_kv(
base_id: BaseId,
theme_name: &StringLimitOf<T>,
filter_keys: Option<BTreeSet<StringLimitOf<T>>>
filter_keys: Option<BTreeSet<StringLimitOf<T>>>,
) -> Result<BoundedThemePropertiesOf<T>, Error<T>> {
BoundedVec::try_from(
Themes::<T>::iter_prefix((base_id, theme_name.clone()))
.filter(|(key, _)| match &filter_keys {
Some (filter_keys) => filter_keys.contains(key),
None => true
Some(filter_keys) => filter_keys.contains(key),
None => true,
})
.map(|(key, value)| {
ThemeProperty {
key,
value,
}
})
.collect::<Vec<_>>()
).or(Err(Error::<T>::TooManyProperties))
.map(|(key, value)| ThemeProperty { key, value })
.collect::<Vec<_>>(),
)
.or(Err(Error::<T>::TooManyProperties))
}
}

Expand All @@ -95,7 +96,7 @@ impl<T: Config>
T::PartsLimit,
>,
BoundedVec<CollectionId, T::MaxCollectionsEquippablePerPart>,
BoundedVec<ThemeProperty<BoundedVec<u8, T::StringLimit>>, T::MaxPropertiesPerTheme>
BoundedVec<ThemeProperty<BoundedVec<u8, T::StringLimit>>, T::MaxPropertiesPerTheme>,
> for Pallet<T>
where
T: pallet_uniques::Config<CollectionId = CollectionId, ItemId = NftId>,
Expand All @@ -122,7 +123,7 @@ where
>,
) -> Result<BaseId, DispatchError> {
let base_id = Self::get_next_base_id()?;
for part in parts.clone() {
for part in parts {
match part.clone() {
PartType::SlotPart(p) => {
Parts::<T>::insert(base_id, p.id, part);
Expand Down Expand Up @@ -161,17 +162,14 @@ where

/// Implementation of the do_equip function for the Base trait
/// Called by the equip extrinsic to equip a child NFT's resource to a parent's slot, if all are
/// available. Also can be called to unequip, which can be successful if
/// - Item has beeen burned
/// - Item is equipped and extrinsic called by equipping item owner
/// - Item is equipped and extrinsic called by equipper NFT owner
/// available.
/// Equipping operations are maintained inside the Equippings storage.
/// Modeled after [equip interaction](https://github.com/rmrk-team/rmrk-spec/blob/master/standards/rmrk2.0.0/interactions/equip.md)
///
/// Parameters:
/// - issuer: The caller of the function, not necessarily anything else
/// - item: Child NFT being equipped (or unequipped)
/// - equipper: Parent NFT which will equip (or unequip) the item
/// - item: Child NFT being equipped
/// - equipper: Parent NFT which will equip the item
/// - base_id: ID of the base which the item and equipper must each have a resource referencing
/// - slot_id: ID of the slot which the item and equipper must each have a resource referencing
fn do_equip(
Expand All @@ -181,7 +179,7 @@ where
resource_id: ResourceId,
base_id: BaseId,
slot_id: SlotId,
) -> Result<(CollectionId, NftId, BaseId, SlotId, bool), DispatchError> {
) -> Result<(CollectionId, NftId, BaseId, SlotId), DispatchError> {
let item_collection_id = item.0;
let item_nft_id = item.1;
let equipper_collection_id = equipper.0;
Expand All @@ -197,85 +195,44 @@ where
pallet_uniques::Error::<T>::Locked
);

let item_is_equipped =
Equippings::<T>::get(((equipper_collection_id, equipper_nft_id), base_id, slot_id))
.is_some();
let item_exists =
pallet_rmrk_core::Pallet::<T>::nfts(item_collection_id, item_nft_id).is_some();

// If item doesn't exist, anyone can unequip it.
if !item_exists && item_is_equipped {
// Remove from Equippings nft/base/slot storage
Equippings::<T>::remove(((equipper_collection_id, equipper_nft_id), base_id, slot_id));

// Update item's equipped property
pallet_rmrk_core::Nfts::<T>::try_mutate_exists(
item_collection_id,
item_nft_id,
|nft| -> DispatchResult {
if let Some(nft) = nft {
nft.equipped = false;
}
Ok(())
},
)?;

// Return unequip event details
return Ok((item_collection_id, item_nft_id, base_id, slot_id, false))
}

let item_owner =
pallet_rmrk_core::Pallet::<T>::lookup_root_owner(item_collection_id, item_nft_id)?;
let equipper_owner = pallet_rmrk_core::Pallet::<T>::lookup_root_owner(
equipper_collection_id,
equipper_nft_id,
)?;

// If the item is equipped in this slot, and either the equipper or the item owner is the
// caller, it will be unequipped
if item_is_equipped && (item_owner.0 == issuer || equipper_owner.0 == issuer) {
// Remove from Equippings nft/base/slot storage
Equippings::<T>::remove(((equipper_collection_id, equipper_nft_id), base_id, slot_id));

// Update item's equipped property
pallet_rmrk_core::Nfts::<T>::try_mutate_exists(
item_collection_id,
item_nft_id,
|nft| -> DispatchResult {
if let Some(nft) = nft {
nft.equipped = false;
}
Ok(())
},
)?;

// Return unequip event details
return Ok((item_collection_id, item_nft_id, base_id, slot_id, false))
}
// If the item's equipped property is true, it is already equipped
ensure!(
!pallet_rmrk_core::Pallet::<T>::nfts(item_collection_id, item_nft_id)
.unwrap()
.equipped,
Error::<T>::ItemAlreadyEquipped
);

// Equipper NFT must exist
// If the Equippings storage contains the Base/Slot for the Collection+NFT ID, the item is
// already equipped
ensure!(
pallet_rmrk_core::Pallet::<T>::nfts(equipper_collection_id, equipper_nft_id).is_some(),
Error::<T>::EquipperDoesntExist
!Self::slot_is_equipped((equipper_collection_id, equipper_nft_id), base_id, slot_id),
Error::<T>::SlotAlreadyEquipped
);

// Item must exist
let item_exists =
pallet_rmrk_core::Pallet::<T>::nft_exists((item_collection_id, item_nft_id));
ensure!(item_exists, Error::<T>::ItemDoesntExist);

// Is the item equipped anywhere?
// Equipper must exist
ensure!(
!pallet_rmrk_core::Pallet::<T>::nfts(item_collection_id, item_nft_id)
.unwrap()
.equipped,
Error::<T>::AlreadyEquipped
pallet_rmrk_core::Pallet::<T>::nfts(equipper_collection_id, equipper_nft_id).is_some(),
Error::<T>::EquipperDoesntExist
);

// Caller must root-own equipper?
ensure!(equipper_owner.0 == issuer, Error::<T>::PermissionError);

// Caller must root-own item
let item_owner =
pallet_rmrk_core::Pallet::<T>::lookup_root_owner(item_collection_id, item_nft_id)?;
ensure!(item_owner.0 == issuer, Error::<T>::PermissionError);

// Caller must root-own equipper
let equipper_owner = pallet_rmrk_core::Pallet::<T>::lookup_root_owner(
equipper_collection_id,
equipper_nft_id,
)?;
ensure!(equipper_owner.0 == issuer, Error::<T>::PermissionError);

// Equipper must be direct parent of item
let equipper_owner = pallet_rmrk_core::Pallet::<T>::nfts(item_collection_id, item_nft_id)
.unwrap()
Expand Down Expand Up @@ -318,7 +275,7 @@ where
// Part must exist
ensure!(Self::parts(base_id, slot_id).is_some(), Error::<T>::PartDoesntExist);

// Returns Result
// Check the PartType stored for this Base + Slot
match Self::parts(base_id, slot_id).unwrap() {
PartType::FixedPart(_) => {
// Part must be SlotPart type
Expand Down Expand Up @@ -352,11 +309,110 @@ where
Ok(())
},
)?;
Ok((item_collection_id, item_nft_id, base_id, slot_id, true))
Ok((item_collection_id, item_nft_id, base_id, slot_id))
},
}
}

/// Implementation of the do_unequip function for the Base trait
/// Called by the equip extrinsic to unequip a child NFT's resource from a parent's slot, if it
/// is equipped. Unequip can be successful if
/// - Item has been burned
/// - Item is equipped and extrinsic called by equipping item owner
/// - Item is equipped and extrinsic called by equipper NFT owner
/// Equipping operations are maintained inside the Equippings storage.
/// Modeled after [equip interaction](https://github.com/rmrk-team/rmrk-spec/blob/master/standards/rmrk2.0.0/interactions/equip.md)
///
/// Parameters:
/// - issuer: The caller of the function, not necessarily anything else
/// - item: Child NFT being equipped (or unequipped)
/// - equipper: Parent NFT which will equip (or unequip) the item
/// - base_id: ID of the equipped item's base
/// - slot_id: ID of the equipped item's slot
fn do_unequip(
issuer: T::AccountId,
item: (CollectionId, NftId),
equipper: (CollectionId, NftId),
base_id: BaseId,
slot_id: SlotId,
) -> Result<(CollectionId, NftId, BaseId, SlotId), DispatchError> {
let item_collection_id = item.0;
let item_nft_id = item.1;
let equipper_collection_id = equipper.0;
let equipper_nft_id = equipper.1;
// Check item NFT lock status
ensure!(
!pallet_rmrk_core::Pallet::<T>::is_locked(item_collection_id, item_nft_id),
pallet_uniques::Error::<T>::Locked
);
// Check equipper NFT lock status
ensure!(
!pallet_rmrk_core::Pallet::<T>::is_locked(equipper_collection_id, equipper_nft_id),
pallet_uniques::Error::<T>::Locked
);

ensure!(
Self::slot_is_equipped((equipper_collection_id, equipper_nft_id), base_id, slot_id),
Error::<T>::SlotNotEquipped
);

// Check if the item already exists
let item_exists =
pallet_rmrk_core::Pallet::<T>::nft_exists((item_collection_id, item_nft_id));

// If item doesn't exist, anyone can unequip it. This can happen because burn_nft can
// happen in rmrk-core, which doesn't know about rmrk-equip.
if !item_exists {
// Remove from Equippings nft/base/slot storage
Equippings::<T>::remove(((equipper_collection_id, equipper_nft_id), base_id, slot_id));

// Update item's equipped property
pallet_rmrk_core::Nfts::<T>::try_mutate_exists(
item_collection_id,
item_nft_id,
|nft| -> DispatchResult {
if let Some(nft) = nft {
nft.equipped = false;
}
Ok(())
},
)?;

// Return unequip event details
return Ok((item_collection_id, item_nft_id, base_id, slot_id))
}

let item_owner =
pallet_rmrk_core::Pallet::<T>::lookup_root_owner(item_collection_id, item_nft_id)?;
let equipper_owner = pallet_rmrk_core::Pallet::<T>::lookup_root_owner(
equipper_collection_id,
equipper_nft_id,
)?;

let issuer_owns_either_equipper_or_item =
item_owner.0 == issuer || equipper_owner.0 == issuer;
ensure!(
issuer_owns_either_equipper_or_item,
Error::<T>::UnequipperMustOwnEitherItemOrEquipper
);

// Remove from Equippings nft/base/slot storage
Equippings::<T>::remove(((equipper_collection_id, equipper_nft_id), base_id, slot_id));

// Update item's equipped property
pallet_rmrk_core::Nfts::<T>::try_mutate_exists(
item_collection_id,
item_nft_id,
|nft| -> DispatchResult {
if let Some(nft) = nft {
nft.equipped = false;
}
Ok(())
},
)?;
Ok((item_collection_id, item_nft_id, base_id, slot_id))
}

/// Implementation of the equippable function for the Base trait
/// Called by the equippable extrinsic to update the array of Collections allowed
/// to be equipped to a Base's specified Slot Part.
Expand Down Expand Up @@ -416,7 +472,10 @@ where
fn add_theme(
issuer: T::AccountId,
base_id: BaseId,
theme: BoundedThemeOf<T>,
theme: Theme<
BoundedVec<u8, T::StringLimit>,
BoundedVec<ThemeProperty<BoundedVec<u8, T::StringLimit>>, T::MaxPropertiesPerTheme>,
>,
) -> Result<(), DispatchError> {
// Base must exist
ensure!(Bases::<T>::get(base_id).is_some(), Error::<T>::BaseDoesntExist);
Expand Down
Loading

0 comments on commit b56ef02

Please sign in to comment.