#![cfg_attr(not(feature = "std"), no_std)]
mod types;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
#[cfg(test)]
pub mod mock;
#[cfg(test)]
mod tests;
pub mod weights;
use frame_system::Config as SystemConfig;
pub use pallet::*;
pub use scale_info::Type;
pub use types::*;
pub use weights::WeightInfo;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use core::fmt::Display;
use frame_support::{
dispatch::DispatchResult,
ensure,
pallet_prelude::*,
sp_runtime::traits::{AccountIdConversion, StaticLookup},
traits::{
fungible::{
hold::Mutate as HoldMutateFungible, Inspect as InspectFungible,
Mutate as MutateFungible,
},
fungibles::{
metadata::{MetadataDeposit, Mutate as MutateMetadata},
Create, Destroy, Inspect, Mutate,
},
tokens::{
nonfungibles_v2::{Inspect as NonFungiblesInspect, Transfer},
AssetId, Balance as AssetBalance,
Fortitude::Polite,
Precision::{BestEffort, Exact},
Preservation::{Expendable, Preserve},
},
},
BoundedVec, PalletId,
};
use frame_system::pallet_prelude::*;
use scale_info::prelude::{format, string::String};
use sp_runtime::traits::{One, Zero};
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type Currency: InspectFungible<Self::AccountId>
+ MutateFungible<Self::AccountId>
+ HoldMutateFungible<Self::AccountId, Reason = Self::RuntimeHoldReason>;
type RuntimeHoldReason: From<HoldReason>;
#[pallet::constant]
type Deposit: Get<DepositOf<Self>>;
type NftCollectionId: Member + Parameter + MaxEncodedLen + Copy + Display;
type NftId: Member + Parameter + MaxEncodedLen + Copy + Display;
type AssetBalance: AssetBalance;
type AssetId: AssetId;
type Assets: Inspect<Self::AccountId, AssetId = Self::AssetId, Balance = Self::AssetBalance>
+ Create<Self::AccountId>
+ Destroy<Self::AccountId>
+ Mutate<Self::AccountId>
+ MutateMetadata<Self::AccountId>
+ MetadataDeposit<DepositOf<Self>>;
type Nfts: NonFungiblesInspect<
Self::AccountId,
ItemId = Self::NftId,
CollectionId = Self::NftCollectionId,
> + Transfer<Self::AccountId>;
#[pallet::constant]
type PalletId: Get<PalletId>;
#[pallet::constant]
type NewAssetSymbol: Get<BoundedVec<u8, Self::StringLimit>>;
#[pallet::constant]
type NewAssetName: Get<BoundedVec<u8, Self::StringLimit>>;
#[pallet::constant]
type StringLimit: Get<u32>;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper: BenchmarkHelper<Self::AssetId, Self::NftCollectionId, Self::NftId>;
type WeightInfo: WeightInfo;
}
#[pallet::storage]
#[pallet::getter(fn nft_to_asset)]
pub type NftToAsset<T: Config> = StorageMap<
_,
Blake2_128Concat,
(T::NftCollectionId, T::NftId),
Details<AssetIdOf<T>, AssetBalanceOf<T>, DepositOf<T>, T::AccountId>,
OptionQuery,
>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
NftFractionalized {
nft_collection: T::NftCollectionId,
nft: T::NftId,
fractions: AssetBalanceOf<T>,
asset: AssetIdOf<T>,
beneficiary: T::AccountId,
},
NftUnified {
nft_collection: T::NftCollectionId,
nft: T::NftId,
asset: AssetIdOf<T>,
beneficiary: T::AccountId,
},
}
#[pallet::error]
pub enum Error<T> {
IncorrectAssetId,
NoPermission,
NftNotFound,
NftNotFractionalized,
}
#[pallet::composite_enum]
pub enum HoldReason {
#[codec(index = 0)]
Fractionalized,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::fractionalize())]
pub fn fractionalize(
origin: OriginFor<T>,
nft_collection_id: T::NftCollectionId,
nft_id: T::NftId,
asset_id: AssetIdOf<T>,
beneficiary: AccountIdLookupOf<T>,
fractions: AssetBalanceOf<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let beneficiary = T::Lookup::lookup(beneficiary)?;
let nft_owner =
T::Nfts::owner(&nft_collection_id, &nft_id).ok_or(Error::<T>::NftNotFound)?;
ensure!(nft_owner == who, Error::<T>::NoPermission);
let pallet_account = Self::get_pallet_account();
let deposit = T::Deposit::get();
T::Currency::hold(&HoldReason::Fractionalized.into(), &nft_owner, deposit)?;
Self::do_lock_nft(nft_collection_id, nft_id)?;
Self::do_create_asset(asset_id.clone(), pallet_account.clone())?;
Self::do_mint_asset(asset_id.clone(), &beneficiary, fractions)?;
Self::do_set_metadata(
asset_id.clone(),
&who,
&pallet_account,
&nft_collection_id,
&nft_id,
)?;
NftToAsset::<T>::insert(
(nft_collection_id, nft_id),
Details { asset: asset_id.clone(), fractions, asset_creator: nft_owner, deposit },
);
Self::deposit_event(Event::NftFractionalized {
nft_collection: nft_collection_id,
nft: nft_id,
fractions,
asset: asset_id,
beneficiary,
});
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::unify())]
pub fn unify(
origin: OriginFor<T>,
nft_collection_id: T::NftCollectionId,
nft_id: T::NftId,
asset_id: AssetIdOf<T>,
beneficiary: AccountIdLookupOf<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let beneficiary = T::Lookup::lookup(beneficiary)?;
NftToAsset::<T>::try_mutate_exists((nft_collection_id, nft_id), |maybe_details| {
let details = maybe_details.take().ok_or(Error::<T>::NftNotFractionalized)?;
ensure!(details.asset == asset_id, Error::<T>::IncorrectAssetId);
let deposit = details.deposit;
let asset_creator = details.asset_creator;
Self::do_burn_asset(asset_id.clone(), &who, details.fractions)?;
Self::do_unlock_nft(nft_collection_id, nft_id, &beneficiary)?;
T::Currency::release(
&HoldReason::Fractionalized.into(),
&asset_creator,
deposit,
BestEffort,
)?;
Self::deposit_event(Event::NftUnified {
nft_collection: nft_collection_id,
nft: nft_id,
asset: asset_id,
beneficiary,
});
Ok(())
})
}
}
impl<T: Config> Pallet<T> {
fn get_pallet_account() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}
fn do_lock_nft(nft_collection_id: T::NftCollectionId, nft_id: T::NftId) -> DispatchResult {
T::Nfts::disable_transfer(&nft_collection_id, &nft_id)
}
fn do_unlock_nft(
nft_collection_id: T::NftCollectionId,
nft_id: T::NftId,
account: &T::AccountId,
) -> DispatchResult {
T::Nfts::enable_transfer(&nft_collection_id, &nft_id)?;
T::Nfts::transfer(&nft_collection_id, &nft_id, account)
}
fn do_create_asset(asset_id: AssetIdOf<T>, admin: T::AccountId) -> DispatchResult {
T::Assets::create(asset_id, admin, false, One::one())
}
fn do_mint_asset(
asset_id: AssetIdOf<T>,
beneficiary: &T::AccountId,
amount: AssetBalanceOf<T>,
) -> DispatchResult {
T::Assets::mint_into(asset_id, beneficiary, amount)?;
Ok(())
}
fn do_burn_asset(
asset_id: AssetIdOf<T>,
account: &T::AccountId,
amount: AssetBalanceOf<T>,
) -> DispatchResult {
T::Assets::burn_from(asset_id.clone(), account, amount, Expendable, Exact, Polite)?;
T::Assets::start_destroy(asset_id, None)
}
fn do_set_metadata(
asset_id: AssetIdOf<T>,
depositor: &T::AccountId,
pallet_account: &T::AccountId,
nft_collection_id: &T::NftCollectionId,
nft_id: &T::NftId,
) -> DispatchResult {
let name = format!(
"{} {nft_collection_id}-{nft_id}",
String::from_utf8_lossy(&T::NewAssetName::get())
);
let symbol: &[u8] = &T::NewAssetSymbol::get();
let existential_deposit = T::Currency::minimum_balance();
let pallet_account_balance = T::Currency::balance(&pallet_account);
if pallet_account_balance < existential_deposit {
T::Currency::transfer(&depositor, &pallet_account, existential_deposit, Preserve)?;
}
let metadata_deposit = T::Assets::calc_metadata_deposit(name.as_bytes(), symbol);
if !metadata_deposit.is_zero() {
T::Currency::transfer(&depositor, &pallet_account, metadata_deposit, Preserve)?;
}
T::Assets::set(asset_id, &pallet_account, name.into(), symbol.into(), 0)
}
}
}