//! # Unit Auction Pallet
//! <!-- Original author of paragraph: @faiz
//!
//! ## Overview
//!
//! Pallet that allows a user to create nft collections and items inside a collection, users can transfer , auction their items.
//!
//! ### Goals
//!
//! The pallet is designed to make the following possible:
//!
//! * create collection.
//! * create item.
//! * Bid on an item.
//! * accept bid
//! * transfer item.
//!
//! ## Interface
//!
//! ### Permissionless Functions
//!
//! - `create_nft_collection`: create a collection.
//! - `create_nft_item` : create an item inside a collection.
//! - `make_nft_bid` : to bid on an item inside a collection.
//! - `accept_nft_bid` : accept a bid.
//! - `transfer` : transfer item.
//!
//!//! Please refer to the [`Call`] enum and its associated variants for documentation on each
//! function.
// Ensure we're `no_std` when compiling for Wasm.
#![cfg_attr(not(feature = "std"), no_std)]
// Re-export pallet items so that they can be accessed from the crate namespace.
pub use pallet::*;
mod types;
pub use types::*;
use frame_support::traits::{
Currency,
ReservableCurrency,
fungibles::{ self, Inspect, Mutate },
tokens::Preservation::Expendable,
};
use traits::profile::ProfileInspect;
use traits::{ subaccounts::{SubAccounts, AccountOrigin}, asset::{AssetInterface, TokenType} };
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
// Definition of the pallet logic, to be aggregated at runtime definition through
// `construct_runtime`.
#[frame_support::pallet]
pub mod pallet {
use frame_support::PalletId;
use frame_support::sp_runtime::traits::AccountIdConversion;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use frame_support::sp_runtime::traits::StaticLookup;
use frame_support::StorageHasher;
// Import various types used to declare pallet in scope
use super::*;
use frame_support::dispatch::Vec;
use frame_system::RawOrigin;
pub type Decimals = u8;
// Balance type alias.
pub type BalanceOf<T> =
<<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
/// Asset id type alias.
pub type AssetIdOf<T> =
<<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::AssetId;
/// A type alias for the balance type from this pallet's point of view.
pub type DepositBalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
/// The type used to identify a unique item within a collection.
pub type InctanceId<T> = <T as pallet_uniques::Config>::ItemId;
/// Identifier for the collection of item.
pub type ClassId<T> = <T as pallet_uniques::Config>::CollectionId;
/// The module configuration trait.
#[pallet::config]
pub trait Config: frame_system::Config + pallet_uniques::Config {
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// The currency mechanism, used for paying for reserves.
type Currency: ReservableCurrency<Self::AccountId>;
/// The pallet's id
#[pallet::constant]
type AuctionPalletId: Get<PalletId>;
#[pallet::constant]
type StringLimit: Get<u32>;
/// Type to access the profile pallet
type Profile: ProfileInspect<
Self::AccountId,
<Self as pallet::Config>::StringLimit
>;
/// Type to access the sub account pallet
type SubAccounts: SubAccounts<Self::AccountId, AccountOrigin>;
/// Type to access the Assets Pallet.
type Fungibles: fungibles::Inspect<Self::AccountId> + //, AssetId = u32> // Hash this
fungibles::Mutate<Self::AccountId> +
fungibles::metadata::Inspect<Self::AccountId> +
fungibles::Create<Self::AccountId> +
fungibles::roles::Inspect<Self::AccountId> +
AssetInterface<AssetIdOf<Self>, BalanceOf<Self>, Self::AccountId, Decimals, TokenType>;
}
// Simple declaration of the `Pallet` type. It is placeholder we use to implement traits and
// method.
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::storage]
#[pallet::getter(fn collection_id)]
/// current collection Id
pub type CollectionId<T> = StorageValue<_, u32, ValueQuery>; // TODO: make this number a parameter
#[pallet::storage]
#[pallet::getter(fn item_id)]
/// current Item Id
pub type ItemId<T: Config> = StorageMap<
_,
Blake2_128Concat,
<T as pallet_uniques::Config>::CollectionId,
u32,
ValueQuery
>;
#[pallet::storage]
#[pallet::getter(fn asset_collections)]
/// all collections for a specific asset
pub type AssetCollections<T: Config> = StorageMap<
_,
Blake2_128Concat,
AssetIdOf<T>,
Vec<<T as pallet_uniques::Config>::CollectionId>,
ValueQuery
>;
#[pallet::storage]
#[pallet::getter(fn collection_items)]
/// all collection items
pub type CollectionItems<T: Config> = StorageMap<
_,
Blake2_128Concat,
<T as pallet_uniques::Config>::CollectionId,
Vec<<T as pallet_uniques::Config>::ItemId>,
ValueQuery
>;
#[pallet::storage]
#[pallet::getter(fn collection_item_bid)]
/// last Bid for an Item inside a collection
pub type ItmeBid<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
<T as pallet_uniques::Config>::CollectionId,
Blake2_128Concat,
<T as pallet_uniques::Config>::ItemId,
CollectionItemBid<T::AccountId, BalanceOf<T>, AssetIdOf<T>>,
OptionQuery
>;
#[pallet::storage]
#[pallet::getter(fn collection_item_sell_bid)]
/// sell bid put on item of a collection by the owner
pub type ItmeSellBid<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
<T as pallet_uniques::Config>::CollectionId,
Blake2_128Concat,
<T as pallet_uniques::Config>::ItemId,
CollectionItemBid<T::AccountId, BalanceOf<T>, AssetIdOf<T>>,
OptionQuery
>;
#[pallet::storage]
#[pallet::getter(fn collection_item_highest_buy_bid)]
/// highest Item sell bid
pub type ItemHighestBuyBid<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
<T as pallet_uniques::Config>::CollectionId,
Blake2_128Concat,
<T as pallet_uniques::Config>::ItemId,
CollectionItemBid<T::AccountId, BalanceOf<T>, AssetIdOf<T>>,
OptionQuery
>;
#[pallet::storage]
#[pallet::getter(fn collection_item_bids)]
/// all item bids
pub type ItmeBids<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
<T as pallet_uniques::Config>::CollectionId,
Blake2_128Concat,
<T as pallet_uniques::Config>::ItemId,
Vec<CollectionItemBid<T::AccountId, BalanceOf<T>, AssetIdOf<T>>>,
ValueQuery
>;
#[pallet::storage]
#[pallet::getter(fn collection_transfers)]
/// collection transfers
pub type CollectionTransfers<T: Config> = StorageValue<
_,
Vec<
CollectionItemTransfers<
<T as pallet_uniques::Config>::ItemId,
BalanceOf<T>,
T::AccountId,
<T as pallet_uniques::Config>::CollectionId
>
>,
ValueQuery
>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// A `collection` was created.
CollectionCreated {
collection_id: <T as pallet_uniques::Config>::CollectionId,
},
/// A `item` was created.
CreatedNftItem {
collection_id: <T as pallet_uniques::Config>::CollectionId,
item_id: <T as pallet_uniques::Config>::ItemId,
},
/// A `Bid` is inserted.
BidInserted {
collection_id: <T as pallet_uniques::Config>::CollectionId,
item_id: <T as pallet_uniques::Config>::ItemId,
bid: BalanceOf<T>,
},
/// A `Bid` is Accepted.
BidAccepted {
collection_id: <T as pallet_uniques::Config>::CollectionId,
item_id: <T as pallet_uniques::Config>::ItemId,
bid: BalanceOf<T>,
},
/// 'Item' Transfered
ItemTransfered {
collection_id: <T as pallet_uniques::Config>::CollectionId,
item_id: <T as pallet_uniques::Config>::ItemId,
},
}
#[pallet::error]
pub enum Error<T> {
/// Bid Amount Should be greater than the last bid.
LowBidAmount,
/// Invalid owner for Nft Item.
InvalidOwnerOfItem,
/// Requested bid not Found.
BidNotFound,
/// Owner of auction cannot place bid.
OwnerCannotPlaceBid,
/// Asset is not allowed for bidding on this auction.
AssetBidNotAllowed,
/// Asset not Found.
UnknownAsset,
/// Error Occured wile transferring nft item.
UniquesTransferFailed,
/// Not Enough Assets in Account.
NotEnoughAssets,
/// Overflow Occured performing calculations
Overflow,
/// Profile Not Found
ProfileNotFound,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Issue a new collection of non-fungible items from a public origin.
///
/// This new collection has no items initially and its owner is the origin.
///
/// Origin must be Signed.
///
///
/// Parameters:
/// - `max_supply`: The max supply of items for that collections.
/// - `data`: The data that will be set as metadata for that collection
/// - `asset`: The asset Id for which the collection is created
///
/// Emits `CollectionCreated` event when successful.
#[pallet::call_index(0)]
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
pub fn create_nft_collection(
origin: OriginFor<T>,
max_supply: u32,
data: BoundedVec<u8, <T as pallet_uniques::Config>::StringLimit>,
asset: AssetIdOf<T>
) -> DispatchResult {
let mut _who = ensure_signed(origin.clone())?;
// Mutate the origin to transfer funds from the main account
_who = <T as pallet::Config>::SubAccounts::get_main_account(_who)?;
let current_collection_id = CollectionId::<T>::get();
// creating nft collection
pallet_uniques::Pallet::<T>::create(
RawOrigin::Signed(_who.clone()).into(),
current_collection_id.into(),
T::Lookup::unlookup(_who.clone())
)?;
// setting max supply
pallet_uniques::Pallet::<T>::set_collection_max_supply(
RawOrigin::Signed(_who.clone()).into(),
current_collection_id.into(),
max_supply
)?;
// setting metadata
pallet_uniques::Pallet::<T>::set_collection_metadata(
RawOrigin::Signed(_who.clone()).into(),
current_collection_id.into(),
data,
false
)?;
// add to all collections for a asset
let mut all_collections = AssetCollections::<T>::get(&asset);
all_collections.push(current_collection_id.into());
AssetCollections::<T>::insert(asset, all_collections);
// updating the id
let new_id = current_collection_id.checked_add(1).ok_or(Error::<T>::Overflow)?;
// updating the storage
CollectionId::<T>::put(new_id);
Self::deposit_event(Event::CollectionCreated {
collection_id: current_collection_id.into(),
});
Ok(())
}
/// Creates a new non-fungible item for that collection.
///
///
/// Origin must be Signed.
///
///
/// Parameters:
/// - `class_id`: The class Id for which the item is created
/// - `data`: The data that will be set as metadata for that collection
///
/// Emits `CreatedNftItem` event when successful.
#[pallet::call_index(1)]
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
pub fn create_nft_item(
origin: OriginFor<T>,
class_id: <T as pallet_uniques::Config>::CollectionId,
data: BoundedVec<u8, <T as pallet_uniques::Config>::StringLimit>
) -> DispatchResult {
let mut _who = ensure_signed(origin.clone())?;
// Mutate the origin to transfer funds from the main account
_who = <T as pallet::Config>::SubAccounts::get_main_account(_who)?;
let current_item_id = ItemId::<T>::get(class_id.clone());
// creating nft collection
pallet_uniques::Pallet::<T>::mint(
RawOrigin::Signed(_who.clone()).into(),
class_id.clone(),
current_item_id.into(),
T::Lookup::unlookup(_who.clone())
)?;
// setting metadata
pallet_uniques::Pallet::<T>::set_metadata(
RawOrigin::Signed(_who.clone()).into(),
class_id.clone(),
current_item_id.into(),
data,
false
)?;
// getting the current id
let new_id = current_item_id.checked_add(1).ok_or(Error::<T>::Overflow)?;
// updating the storage
ItemId::<T>::insert(class_id, new_id);
Self::deposit_event(Event::CreatedNftItem {
collection_id: class_id,
item_id: current_item_id.into(),
});
Ok(())
}
/// make bid for a non-fungible Item.
///
///
/// Origin must be Signed.
///
///
/// Parameters:
/// - `class_id`: The class Id for which the user will make bid
/// - `item_id`: The Item Id for which the user will make bid
/// - `bid`: The bid amount
/// - `asset`: The asset id for that collection
/// - `bid_type`: The bid type for that Bid ( Buy / Sell)
///
/// Emits `BidInserted` event when successful.
#[pallet::call_index(2)]
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
pub fn make_nft_bid(
origin: OriginFor<T>,
class_id: <T as pallet_uniques::Config>::CollectionId,
item_id: <T as pallet_uniques::Config>::ItemId,
bid: BalanceOf<T>,
asset: AssetIdOf<T>,
bid_type: BidType
) -> DispatchResult {
let mut _who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
_who = <T as pallet::Config>::SubAccounts::get_main_account(_who)?;
// getting current user balance
let balance = T::Fungibles::balance(asset.clone(), &_who.clone());
// balance should be greater than bid amount
ensure!(balance > bid, Error::<T>::NotEnoughAssets);
// getting if the last bid exists
let last_bid = ItmeBid::<T>::get(class_id.clone(), item_id);
if bid_type == BidType::Sell {
// ensuring the owner is the bidder of item
let _asset_details = Self::get_asset_storage_value_from_uniques(
class_id.clone(),
item_id
).ok_or(Error::<T>::UnknownAsset)?;
ensure!(_asset_details.owner.clone() == _who, Error::<T>::InvalidOwnerOfItem);
}
if bid_type == BidType::Buy {
// ensuring the owner is not the bidder of item
let _asset_details = Self::get_asset_storage_value_from_uniques(
class_id.clone(),
item_id
).ok_or(Error::<T>::UnknownAsset)?;
ensure!(_asset_details.owner.clone() != _who, Error::<T>::OwnerCannotPlaceBid);
}
// if last bid exists then
if last_bid.is_some() {
// check new bid is greater then the old
let last_bid = last_bid.ok_or(Error::<T>::BidNotFound)?;
ensure!(last_bid.bid < bid, Error::<T>::LowBidAmount);
}
// getting all collection for an asset
let all_collections = AssetCollections::<T>::get(&asset);
// checking this collection exists in those collections
let collection_id = all_collections.into_iter().find(|x| x == &class_id);
ensure!(collection_id.is_some(), Error::<T>::AssetBidNotAllowed);
let collection_bid = CollectionItemBid {
owner: _who,
bid,
asset,
bid_type: bid_type.clone(),
};
// if bid is buy check if it is highest
if bid_type == BidType::Buy {
// getting the highest bid
let highest_bid = ItemHighestBuyBid::<T>::get(class_id, item_id);
// if the highest bid exists
if highest_bid.is_some() {
// getting the highest bid
let highest_bid = highest_bid.ok_or(Error::<T>::BidNotFound)?;
// checking if new bid is greater then the highest bid
if collection_bid.bid > highest_bid.bid {
// updating the highest bid
ItemHighestBuyBid::<T>::insert(class_id, item_id, collection_bid.clone());
// locking assets
Self::lock_assets(
collection_bid.clone().owner,
collection_bid.clone().asset,
collection_bid.clone().bid
)?;
// unlocking assets of previous bidder
Self::unlock_assets(
highest_bid.clone().owner,
highest_bid.clone().asset,
highest_bid.clone().bid
)?;
}
} else {
// updating the highest bid
ItemHighestBuyBid::<T>::insert(class_id, item_id, collection_bid.clone());
// locking assets
Self::lock_assets(
collection_bid.clone().owner,
collection_bid.clone().asset,
collection_bid.clone().bid
)?;
}
}
// inserting the bid
ItmeBid::<T>::insert(class_id, item_id, collection_bid.clone());
// store all bids
if bid_type != BidType::Sell {
let mut all_bids = ItmeBids::<T>::get(class_id, item_id);
all_bids.push(collection_bid);
// updating the storage
ItmeBids::<T>::insert(class_id, item_id, all_bids);
} else {
// inserting the bid
ItmeSellBid::<T>::insert(class_id, item_id, collection_bid.clone());
}
Self::deposit_event(Event::BidInserted {
collection_id: class_id,
item_id: item_id,
bid: bid,
});
Ok(())
}
/// accept bid for a non-fungible Item.
///
///
/// Origin must be Signed and the owner of the item and auction.
///
///
/// Parameters:
/// - `class_id`: The class Id of the auction Item
/// - `item_id`: The Item Id of the auction Item
///
/// Emits `BidAccepted` event when successful.
#[pallet::call_index(3)]
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
pub fn accept_nft_bid(
origin: OriginFor<T>,
class_id: <T as pallet_uniques::Config>::CollectionId,
item_id: <T as pallet_uniques::Config>::ItemId
) -> DispatchResult {
let mut _who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
_who = <T as pallet::Config>::SubAccounts::get_main_account(_who)?;
// ensuring the owner of instance from uniques pallet
let _asset_details = Self::get_asset_storage_value_from_uniques(
class_id.clone(),
item_id
).ok_or(Error::<T>::UnknownAsset)?;
ensure!(_asset_details.owner.clone() == _who, Error::<T>::InvalidOwnerOfItem);
// getting the highest bid if exists
let last_bid = ItemHighestBuyBid::<T>::get(class_id.clone(), item_id);
// checking if it exists
ensure!(last_bid.is_some(), Error::<T>::BidNotFound);
// getting the highest bid
let bid_item = last_bid.ok_or(Error::<T>::BidNotFound)?;
// bid should be Buy
ensure!(bid_item.bid_type == BidType::Buy, Error::<T>::BidNotFound);
// unlocking assets of previous bidder
Self::unlock_assets(
bid_item.owner.clone(),
bid_item.asset.clone(),
bid_item.bid.clone()
)?;
// transfering the amount to the owner
let transfer = T::Fungibles::transfer(
bid_item.asset,
&bid_item.owner.clone(),
&_who.clone(),
bid_item.bid,
Expendable
);
// transfer should be successfull
ensure!(transfer.is_ok(), Error::<T>::NotEnoughAssets);
// transferring item to bidder
pallet_uniques::Pallet::<T>::transfer(
RawOrigin::Signed(_who.clone()).into(),
class_id,
item_id,
T::Lookup::unlookup(bid_item.owner.clone())
)?;
let transfer = CollectionItemTransfers {
from: _who.clone(),
to: bid_item.owner.clone(),
item_id,
collection_id: class_id,
bid_amount: bid_item.bid,
};
let mut all_transfers = CollectionTransfers::<T>::get();
all_transfers.push(transfer);
// updating the transfer storage
CollectionTransfers::<T>::put(all_transfers);
Self::deposit_event(Event::BidAccepted {
collection_id: class_id,
item_id: item_id,
bid: bid_item.bid,
});
Ok(())
}
/// transfer a non-fungible Item.
///
///
/// Origin must be Signed and the owner of the item .
///
///
/// Parameters:
/// - `class_id`: The class Id of the auction Item
/// - `item_id`: The Item Id of the auction Item
///
/// Emits `ItemTransfered` event when successful.
#[pallet::call_index(4)]
#[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())]
pub fn transfer(
origin: OriginFor<T>,
to: T::AccountId,
class_id: <T as pallet_uniques::Config>::CollectionId,
item_id: <T as pallet_uniques::Config>::ItemId
) -> DispatchResult {
let mut _who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
_who = <T as pallet::Config>::SubAccounts::get_main_account(_who)?;
// ensuring the owner of instance from uniques pallet
let _asset_details = Self::get_asset_storage_value_from_uniques(
class_id.clone(),
item_id
).ok_or(Error::<T>::UnknownAsset)?;
ensure!(_asset_details.owner.clone() == _who, Error::<T>::InvalidOwnerOfItem);
// transferring item
pallet_uniques::Pallet::<T>::transfer(
RawOrigin::Signed(_who.clone()).into(),
class_id,
item_id,
T::Lookup::unlookup(to.clone())
)?;
let transfer = CollectionItemTransfers {
from: _who.clone(),
to: to.clone(),
item_id,
collection_id: class_id,
bid_amount: Self::u32_to_asset_balance(0),
};
let mut all_transfers = CollectionTransfers::<T>::get();
all_transfers.push(transfer);
// updating the transfer storage
CollectionTransfers::<T>::put(all_transfers);
Self::deposit_event(Event::ItemTransfered {
collection_id: class_id,
item_id: item_id,
});
Ok(())
}
}
// to fetch storage items from asset pallet
const PALLET_NAME: &'static str = "Uniques";
const STORAGE_ITEM: &'static str = "Asset";
impl<T: Config> Pallet<T> {
/// returns Item storage Item from Uniques Pallet
fn get_asset_storage_value_from_uniques(
key: <T as pallet_uniques::Config>::CollectionId,
key2: <T as pallet_uniques::Config>::ItemId
) -> Option<ItemDetails<T::AccountId, DepositBalanceOf<T>>> {
let pallet_hash = sp_io::hashing::twox_128(PALLET_NAME.as_bytes());
let storage_hash = sp_io::hashing::twox_128(STORAGE_ITEM.as_bytes());
let key_hashed = frame_support::Blake2_128Concat::hash(&key.encode());
let key2_hashed = frame_support::Blake2_128Concat::hash(&key2.encode());
let mut final_key = Vec::new();
final_key.extend_from_slice(&pallet_hash);
final_key.extend_from_slice(&storage_hash);
final_key.extend_from_slice(&key_hashed);
final_key.extend_from_slice(&key2_hashed);
frame_support::storage::unhashed::get::<ItemDetails<T::AccountId, DepositBalanceOf<T>>>(
&final_key
)
}
/// type convertion
pub fn u32_to_asset_balance(input: u32) -> BalanceOf<T> {
input.into()
}
/// returns account id for the pallet
pub fn account_id() -> T::AccountId {
T::AuctionPalletId::get().into_account_truncating()
}
/// locks assets of user
pub fn lock_assets(
account: T::AccountId,
asset_id: AssetIdOf<T>,
amount: BalanceOf<T>
) -> DispatchResult {
let transfer = T::Fungibles::transfer(
asset_id,
&account.clone(),
&Self::account_id(),
amount,
Expendable
);
ensure!(transfer.is_ok(), Error::<T>::NotEnoughAssets);
Ok(())
}
/// unlocks assets of user
pub fn unlock_assets(
account: T::AccountId,
asset_id: AssetIdOf<T>,
amount: BalanceOf<T>
) -> DispatchResult {
let transfer = T::Fungibles::transfer(
asset_id,
&Self::account_id(),
&account,
amount,
Expendable
);
ensure!(transfer.is_ok(), Error::<T>::NotEnoughAssets);
Ok(())
}
}
}