difftreelog
Refactor fungible logic, add positive tests for sponsorship confirmation
in: master
4 files changed
pallets/nft/src/lib.rsdiffbeforeafterboth--- a/pallets/nft/src/lib.rs
+++ b/pallets/nft/src/lib.rs
@@ -152,8 +152,7 @@
#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
-pub struct FungibleItemType<AccountId> {
- pub owner: AccountId,
+pub struct FungibleItemType {
pub value: u128,
}
@@ -165,13 +164,6 @@
pub variable_data: Vec<u8>,
}
-#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]
-#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
-pub struct ApprovePermissions<AccountId> {
- pub approved: AccountId,
- pub amount: u128,
-}
-
// #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]
// #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
// pub struct VestingItem<AccountId, Moment> {
@@ -182,13 +174,6 @@
// pub amount: u64,
// pub vesting_date: Moment,
// }
-
-#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]
-#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
-pub struct BasketItem<AccountId, BlockNumber> {
- pub address: AccountId,
- pub start_block: BlockNumber,
-}
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
@@ -260,6 +245,7 @@
#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct CreateFungibleData {
+ pub value: u128,
}
#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]
@@ -414,12 +400,12 @@
/// Balance owner per collection map
pub Balance get(fn balance_count): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => u128;
- /// second parameter: item id + owner account id
- pub ApprovedList get(fn approved): double_map hasher(identity) CollectionId, hasher(twox_64_concat) (TokenId, T::AccountId) => Vec<ApprovePermissions<T::AccountId>>;
+ /// second parameter: item id + owner account id + spender account id
+ pub Allowances get(fn approved): double_map hasher(identity) CollectionId, hasher(twox_64_concat) (TokenId, T::AccountId, T::AccountId) => u128;
/// Item collections
pub NftItemList get(fn nft_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => NftItemType<T::AccountId>;
- pub FungibleItemList get(fn fungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => FungibleItemType<T::AccountId>;
+ pub FungibleItemList get(fn fungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => FungibleItemType;
pub ReFungibleItemList get(fn refungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => ReFungibleItemType<T::AccountId>;
/// Index list
@@ -427,7 +413,7 @@
/// Tokens transfer baskets
pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => T::BlockNumber;
- pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => Vec<BasketItem<T::AccountId, T::BlockNumber>>;
+ pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => T::BlockNumber;
pub ReFungibleTransferBasket get(fn refungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => T::BlockNumber;
// Contract Sponsorship and Ownership
@@ -447,8 +433,8 @@
<Module<T>>::init_nft_token(*_c, _i);
}
- for (_num, _c, _i) in &config.fungible_item_id {
- <Module<T>>::init_fungible_token(*_c, _i);
+ for (collection_id, account_id, fungible_item) in &config.fungible_item_id {
+ <Module<T>>::init_fungible_token(*collection_id, account_id, fungible_item);
}
for (_num, _c, _i) in &config.refungible_item_id {
@@ -619,7 +605,7 @@
Self::check_owner_permissions(collection_id, sender)?;
<AddressTokens<T>>::remove_prefix(collection_id);
- <ApprovedList<T>>::remove_prefix(collection_id);
+ <Allowances<T>>::remove_prefix(collection_id);
<Balance<T>>::remove_prefix(collection_id);
<ItemListIndex>::remove(collection_id);
<AdminList<T>>::remove(collection_id);
@@ -1015,7 +1001,7 @@
///
/// * item_id: ID of NFT to burn.
#[weight = T::WeightInfo::burn_item()]
- pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId) -> DispatchResult {
+ pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResult {
let sender = ensure_signed(origin)?;
Self::collection_exists(collection_id)?;
@@ -1033,7 +1019,7 @@
match target_collection.mode
{
CollectionMode::NFT => Self::burn_nft_item(collection_id, item_id)?,
- CollectionMode::Fungible(_) => Self::burn_fungible_item(collection_id, item_id)?,
+ CollectionMode::Fungible(_) => Self::burn_fungible_item(&sender, collection_id, value)?,
CollectionMode::ReFungible(_) => Self::burn_refungible_item(collection_id, item_id, sender.clone())?,
_ => ()
};
@@ -1089,7 +1075,7 @@
match target_collection.mode
{
CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, sender.clone(), recipient)?,
- CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, item_id, value, sender.clone(), recipient)?,
+ CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, value, &sender, &recipient)?,
CollectionMode::ReFungible(_) => Self::transfer_refungible(collection_id, item_id, value, sender.clone(), recipient)?,
_ => ()
};
@@ -1113,7 +1099,7 @@
///
/// * item_id: ID of the item.
#[weight = T::WeightInfo::approve()]
- pub fn approve(origin, approved: T::AccountId, collection_id: CollectionId, item_id: TokenId) -> DispatchResult {
+ pub fn approve(origin, spender: T::AccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResult {
let sender = ensure_signed(origin)?;
@@ -1125,28 +1111,15 @@
if target_collection.access == AccessMode::WhiteList {
Self::check_white_list(collection_id, &sender)?;
- Self::check_white_list(collection_id, &approved)?;
+ Self::check_white_list(collection_id, &spender)?;
}
- // amount param stub
- let amount = 100000000;
-
- let list_exists = <ApprovedList<T>>::contains_key(collection_id, (item_id, sender.clone()));
- if list_exists {
-
- let mut list = <ApprovedList<T>>::get(collection_id, (item_id, sender.clone()));
- let item_contains = list.iter().any(|i| i.approved == approved);
-
- if !item_contains {
- list.push(ApprovePermissions { approved: approved.clone(), amount: amount });
- <ApprovedList<T>>::insert(collection_id, (item_id, sender.clone()), list);
- }
- } else {
-
- let mut list = Vec::new();
- list.push(ApprovePermissions { approved: approved.clone(), amount: amount });
- <ApprovedList<T>>::insert(collection_id, (item_id, sender.clone()), list);
+ let allowance_exists = <Allowances<T>>::contains_key(collection_id, (item_id, &sender, &spender));
+ let mut allowance: u128 = amount;
+ if allowance_exists {
+ allowance += <Allowances<T>>::get(collection_id, (item_id, &sender, &spender));
}
+ <Allowances<T>>::insert(collection_id, (item_id, sender.clone(), spender.clone()), allowance);
Ok(())
}
@@ -1176,15 +1149,12 @@
let sender = ensure_signed(origin)?;
let mut appoved_transfer = false;
- // Check approve
- if <ApprovedList<T>>::contains_key(collection_id, (item_id, from.clone())) {
- let list_itm = <ApprovedList<T>>::get(collection_id, (item_id, from.clone()));
- let opt_item = list_itm.iter().find(|i| i.approved == sender.clone());
- if opt_item.is_some()
- {
- appoved_transfer = true;
- ensure!(opt_item.unwrap().amount >= value, Error::<T>::TokenValueNotEnough);
- }
+ // Check approval
+ let mut approval: u128 = 0;
+ if <Allowances<T>>::contains_key(collection_id, (item_id, &from, &recipient)) {
+ approval = <Allowances<T>>::get(collection_id, (item_id, &from, &recipient));
+ ensure!(approval >= value, Error::<T>::TokenValueNotEnough);
+ appoved_transfer = true;
}
let target_collection = <Collection<T>>::get(collection_id);
@@ -1194,23 +1164,25 @@
// Transfer permissions check
ensure!(appoved_transfer || Self::is_owner_or_admin_permissions(collection_id, sender.clone()),
- Error::<T>::NoPermission);
+ Error::<T>::NoPermission);
if target_collection.access == AccessMode::WhiteList {
Self::check_white_list(collection_id, &sender)?;
Self::check_white_list(collection_id, &recipient)?;
}
- // remove approve
- let approve_list: Vec<ApprovePermissions<T::AccountId>> = <ApprovedList<T>>::get(collection_id, (item_id, from.clone()))
- .into_iter().filter(|i| i.approved != sender.clone()).collect();
- <ApprovedList<T>>::insert(collection_id, (item_id, from.clone()), approve_list);
-
+ // Reduce approval by transferred amount or remove if remaining approval drops to 0
+ if approval - value > 0 {
+ <Allowances<T>>::insert(collection_id, (item_id, &from, &recipient), approval - value);
+ }
+ else {
+ <Allowances<T>>::remove(collection_id, (item_id, &from, &recipient));
+ }
match target_collection.mode
{
CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, from, recipient)?,
- CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, item_id, value, from.clone(), recipient)?,
+ CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, value, &from, &recipient)?,
CollectionMode::ReFungible(_) => Self::transfer_refungible(collection_id, item_id, value, from.clone(), recipient)?,
_ => ()
};
@@ -1574,13 +1546,8 @@
Self::add_nft_item(collection_id, item)?;
},
- CreateItemData::Fungible(_) => {
- let item = FungibleItemType {
- owner,
- value: (10 as u128).pow(collection.decimal_points as u32)
- };
-
- Self::add_fungible_item(collection_id, item)?;
+ CreateItemData::Fungible(data) => {
+ Self::add_fungible_item(collection_id, &owner, data.value)?;
},
CreateItemData::ReFungible(data) => {
let mut owner_list = Vec::new();
@@ -1603,27 +1570,25 @@
Ok(())
}
- fn add_fungible_item(collection_id: CollectionId, item: FungibleItemType<T::AccountId>) -> DispatchResult {
- let current_index = <ItemListIndex>::get(collection_id)
- .checked_add(1)
- .ok_or(Error::<T>::NumOverflow)?;
- let itemcopy = item.clone();
- let owner = item.owner.clone();
+ fn add_fungible_item(collection_id: CollectionId, owner: &T::AccountId, value: u128) -> DispatchResult {
- Self::add_token_index(collection_id, current_index, owner.clone())?;
+ // Does new owner already have an account?
+ let mut balance: u128 = 0;
+ if <FungibleItemList<T>>::contains_key(collection_id, owner) {
+ balance = <FungibleItemList<T>>::get(collection_id, owner).value;
+ }
- <ItemListIndex>::insert(collection_id, current_index);
- <FungibleItemList<T>>::insert(collection_id, current_index, itemcopy);
+ // Mint
+ let item = FungibleItemType {
+ value: balance + value
+ };
+ <FungibleItemList<T>>::insert(collection_id, (*owner).clone(), item);
- // Add current block
- let v: Vec<BasketItem<T::AccountId, T::BlockNumber>> = Vec::new();
- <FungibleTransferBasket<T>>::insert(collection_id, current_index, v);
-
// Update balance
- let new_balance = <Balance<T>>::get(collection_id, owner.clone())
- .checked_add(item.value)
+ let new_balance = <Balance<T>>::get(collection_id, owner)
+ .checked_add(value)
.ok_or(Error::<T>::NumOverflow)?;
- <Balance<T>>::insert(collection_id, owner.clone(), new_balance);
+ <Balance<T>>::insert(collection_id, (*owner).clone(), new_balance);
Ok(())
}
@@ -1642,10 +1607,6 @@
<ItemListIndex>::insert(collection_id, current_index);
<ReFungibleItemList<T>>::insert(collection_id, current_index, itemcopy);
- // Add current block
- let block_number: T::BlockNumber = 0.into();
- <ReFungibleTransferBasket<T>>::insert(collection_id, current_index, block_number);
-
// Update balance
let new_balance = <Balance<T>>::get(collection_id, owner.clone())
.checked_add(value)
@@ -1665,10 +1626,6 @@
<ItemListIndex>::insert(collection_id, current_index);
<NftItemList<T>>::insert(collection_id, current_index, item);
-
- // Add current block
- let block_number: T::BlockNumber = 0.into();
- <NftTransferBasket<T>>::insert(collection_id, current_index, block_number);
// Update balance
let new_balance = <Balance<T>>::get(collection_id, item_owner.clone())
@@ -1697,9 +1654,6 @@
.unwrap();
Self::remove_token_index(collection_id, item_id, owner.clone())?;
- // remove approve list
- <ApprovedList<T>>::remove(collection_id, (item_id, owner.clone()));
-
// update balance
let new_balance = <Balance<T>>::get(collection_id, item.owner.clone())
.checked_sub(item.fraction)
@@ -1718,9 +1672,6 @@
);
let item = <NftItemList<T>>::get(collection_id, item_id);
Self::remove_token_index(collection_id, item_id, item.owner.clone())?;
-
- // remove approve list
- <ApprovedList<T>>::remove(collection_id, (item_id, item.owner.clone()));
// update balance
let new_balance = <Balance<T>>::get(collection_id, item.owner.clone())
@@ -1732,24 +1683,27 @@
Ok(())
}
- fn burn_fungible_item(collection_id: CollectionId, item_id: TokenId) -> DispatchResult {
+ fn burn_fungible_item(owner: &T::AccountId, collection_id: CollectionId, value: u128) -> DispatchResult {
ensure!(
- <FungibleItemList<T>>::contains_key(collection_id, item_id),
+ <FungibleItemList<T>>::contains_key(collection_id, owner),
Error::<T>::TokenNotFound
);
- let item = <FungibleItemList<T>>::get(collection_id, item_id);
- Self::remove_token_index(collection_id, item_id, item.owner.clone())?;
-
- // remove approve list
- <ApprovedList<T>>::remove(collection_id, (item_id, item.owner.clone()));
+ let mut balance = <FungibleItemList<T>>::get(collection_id, owner);
+ ensure!(balance.value >= value, Error::<T>::TokenValueNotEnough);
// update balance
- let new_balance = <Balance<T>>::get(collection_id, item.owner.clone())
- .checked_sub(item.value)
+ let new_balance = <Balance<T>>::get(collection_id, owner)
+ .checked_sub(value)
.ok_or(Error::<T>::NumOverflow)?;
- <Balance<T>>::insert(collection_id, item.owner.clone(), new_balance);
+ <Balance<T>>::insert(collection_id, (*owner).clone(), new_balance);
- <FungibleItemList<T>>::remove(collection_id, item_id);
+ if balance.value - value > 0 {
+ balance.value -= value;
+ <FungibleItemList<T>>::insert(collection_id, (*owner).clone(), balance);
+ }
+ else {
+ <FungibleItemList<T>>::remove(collection_id, owner);
+ }
Ok(())
}
@@ -1810,7 +1764,7 @@
<NftItemList<T>>::get(collection_id, item_id).owner == subject
}
CollectionMode::Fungible(_) => {
- <FungibleItemList<T>>::get(collection_id, item_id).owner == subject
+ <FungibleItemList<T>>::contains_key(collection_id, &subject)
}
CollectionMode::ReFungible(_) => {
<ReFungibleItemList<T>>::get(collection_id, item_id)
@@ -1833,85 +1787,31 @@
fn transfer_fungible(
collection_id: CollectionId,
- item_id: TokenId,
value: u128,
- owner: T::AccountId,
- new_owner: T::AccountId,
+ owner: &T::AccountId,
+ recipient: &T::AccountId,
) -> DispatchResult {
ensure!(
- <FungibleItemList<T>>::contains_key(collection_id, item_id),
+ <FungibleItemList<T>>::contains_key(collection_id, owner),
Error::<T>::TokenNotFound
);
- let full_item = <FungibleItemList<T>>::get(collection_id, item_id);
- let amount = full_item.value;
+ let mut balance = <FungibleItemList<T>>::get(collection_id, owner);
+ ensure!(balance.value >= value, Error::<T>::TokenValueTooLow);
- ensure!(amount >= value, Error::<T>::TokenValueTooLow);
+ // Send balance to recipient (updates balanceOf of recipient)
+ Self::add_fungible_item(collection_id, recipient, value)?;
- // update balance
- let balance_old_owner = <Balance<T>>::get(collection_id, owner.clone())
- .checked_sub(value)
- .ok_or(Error::<T>::NumOverflow)?;
- <Balance<T>>::insert(collection_id, owner.clone(), balance_old_owner);
+ // update balanceOf of sender
+ <Balance<T>>::insert(collection_id, (*owner).clone(), balance.value - value);
- let mut new_owner_account_id = 0;
- let new_owner_items = <AddressTokens<T>>::get(collection_id, new_owner.clone());
- if new_owner_items.len() > 0 {
- new_owner_account_id = new_owner_items[0];
+ // Reduce or remove sender
+ if balance.value == value {
+ <FungibleItemList<T>>::remove(collection_id, owner);
}
-
- // transfer
- if amount == value && new_owner_account_id == 0 {
- // change owner
- // new owner do not have account
- let mut new_full_item = full_item.clone();
- new_full_item.owner = new_owner.clone();
- <FungibleItemList<T>>::insert(collection_id, item_id, new_full_item);
-
- // update balance
- let balancenew_owner = <Balance<T>>::get(collection_id, new_owner.clone())
- .checked_add(value)
- .ok_or(Error::<T>::NumOverflow)?;
- <Balance<T>>::insert(collection_id, new_owner.clone(), balancenew_owner);
-
- // update index collection
- Self::move_token_index(collection_id, item_id, owner.clone(), new_owner.clone())?;
- } else {
- let mut new_full_item = full_item.clone();
- new_full_item.value -= value;
-
- // separate amount
- if new_owner_account_id > 0 {
- // new owner has account
- let mut item = <FungibleItemList<T>>::get(collection_id, new_owner_account_id);
- item.value += value;
-
- // update balance
- let balancenew_owner = <Balance<T>>::get(collection_id, new_owner.clone())
- .checked_add(value)
- .ok_or(Error::<T>::NumOverflow)?;
- <Balance<T>>::insert(collection_id, new_owner.clone(), balancenew_owner);
-
- <FungibleItemList<T>>::insert(collection_id, new_owner_account_id, item);
- } else {
- // new owner do not have account
- let item = FungibleItemType {
- owner: new_owner.clone(),
- value
- };
-
- Self::add_fungible_item(collection_id, item)?;
- }
-
- if amount == value {
- Self::remove_token_index(collection_id, item_id, full_item.owner.clone())?;
-
- // remove approve list
- <ApprovedList<T>>::remove(collection_id, (item_id, full_item.owner.clone()));
- <FungibleItemList<T>>::remove(collection_id, item_id);
- }
-
- <FungibleItemList<T>>::insert(collection_id, item_id, new_full_item);
+ else {
+ balance.value -= value;
+ <FungibleItemList<T>>::insert(collection_id, (*owner).clone(), balance);
}
Ok(())
@@ -2039,8 +1939,6 @@
// update index collection
Self::move_token_index(collection_id, item_id, old_owner.clone(), new_owner.clone())?;
- // reset approved list
- <ApprovedList<T>>::remove(collection_id, (item_id, old_owner));
Ok(())
}
@@ -2052,7 +1950,6 @@
match mode {
CollectionMode::NFT => ensure!(<NftItemList<T>>::contains_key(collection_id, item_id), Error::<T>::TokenNotFound),
CollectionMode::ReFungible(_) => ensure!(<ReFungibleItemList<T>>::contains_key(collection_id, item_id), Error::<T>::TokenNotFound),
- CollectionMode::Fungible(_) => ensure!(<FungibleItemList<T>>::contains_key(collection_id, item_id), Error::<T>::TokenNotFound),
_ => ()
};
@@ -2131,21 +2028,20 @@
<Balance<T>>::insert(collection_id, item_owner.clone(), new_balance);
}
- fn init_fungible_token(collection_id: CollectionId, item: &FungibleItemType<T::AccountId>) {
+ fn init_fungible_token(collection_id: CollectionId, owner: &T::AccountId, item: &FungibleItemType) {
let current_index = <ItemListIndex>::get(collection_id)
.checked_add(1)
.unwrap();
- let owner = item.owner.clone();
- Self::add_token_index(collection_id, current_index, owner.clone()).unwrap();
+ Self::add_token_index(collection_id, current_index, (*owner).clone()).unwrap();
<ItemListIndex>::insert(collection_id, current_index);
// Update balance
- let new_balance = <Balance<T>>::get(collection_id, owner.clone())
+ let new_balance = <Balance<T>>::get(collection_id, owner)
.checked_add(item.value)
.unwrap();
- <Balance<T>>::insert(collection_id, owner.clone(), new_balance);
+ <Balance<T>>::insert(collection_id, (*owner).clone(), new_balance);
}
fn init_refungible_token(collection_id: CollectionId, item: &ReFungibleItemType<T::AccountId>) {
@@ -2343,7 +2239,7 @@
T::AccountId::default()
}
}
- Some(Call::transfer(new_owner, collection_id, item_id, _value)) => {
+ Some(Call::transfer(_new_owner, collection_id, item_id, _value)) => {
let mut sponsor_transfer = false;
if <Collection<T>>::get(collection_id).sponsor_confirmed {
@@ -2352,6 +2248,7 @@
let collection_mode = <Collection<T>>::get(collection_id).mode;
// sponsor timeout
+ let block_number = <system::Module<T>>::block_number() as T::BlockNumber;
sponsor_transfer = match collection_mode {
CollectionMode::NFT => {
@@ -2362,16 +2259,19 @@
ChainLimit::get().nft_sponsor_transfer_timeout
};
- let basket = <NftTransferBasket<T>>::get(collection_id, item_id);
- let block_number = <system::Module<T>>::block_number() as T::BlockNumber;
- let limit_time = basket + limit.into();
- if block_number >= limit_time {
- <NftTransferBasket<T>>::insert(collection_id, item_id, block_number);
- true
+ let mut sponsored = true;
+ if <NftTransferBasket<T>>::contains_key(collection_id, item_id) {
+ let last_tx_block = <NftTransferBasket<T>>::get(collection_id, item_id);
+ let limit_time = last_tx_block + limit.into();
+ if block_number <= limit_time {
+ sponsored = false;
+ }
}
- else {
- false
+ if sponsored {
+ <NftTransferBasket<T>>::insert(collection_id, item_id, block_number);
}
+
+ sponsored
}
CollectionMode::Fungible(_) => {
@@ -2382,26 +2282,20 @@
ChainLimit::get().fungible_sponsor_transfer_timeout
};
- let mut basket = <FungibleTransferBasket<T>>::get(collection_id, item_id);
let block_number = <system::Module<T>>::block_number() as T::BlockNumber;
- if basket.iter().any(|i| i.address == new_owner.clone())
- {
- let item = basket.iter_mut().find(|i| i.address == new_owner.clone()).unwrap().clone();
- let limit_time = item.start_block + limit.into();
- if block_number >= limit_time {
- basket.retain(|x| x.address == item.address);
- basket.push(BasketItem { start_block: block_number, address: new_owner.clone() });
- <FungibleTransferBasket<T>>::insert(collection_id, item_id, basket);
- true
- }
- else {
- false
+ let mut sponsored = true;
+ if <FungibleTransferBasket<T>>::contains_key(collection_id, who) {
+ let last_tx_block = <FungibleTransferBasket<T>>::get(collection_id, who);
+ let limit_time = last_tx_block + limit.into();
+ if block_number <= limit_time {
+ sponsored = false;
}
}
- else {
- basket.push(BasketItem { start_block: block_number, address: new_owner.clone()});
- true
+ if sponsored {
+ <FungibleTransferBasket<T>>::insert(collection_id, who, block_number);
}
+
+ sponsored
}
CollectionMode::ReFungible(_) => {
@@ -2412,15 +2306,19 @@
ChainLimit::get().refungible_sponsor_transfer_timeout
};
- let basket = <ReFungibleTransferBasket<T>>::get(collection_id, item_id);
- let block_number = <system::Module<T>>::block_number() as T::BlockNumber;
- let limit_time = basket + limit.into();
- if block_number >= limit_time {
+ let mut sponsored = true;
+ if <ReFungibleTransferBasket<T>>::contains_key(collection_id, item_id) {
+ let last_tx_block = <ReFungibleTransferBasket<T>>::get(collection_id, item_id);
+ let limit_time = last_tx_block + limit.into();
+ if block_number <= limit_time {
+ sponsored = false;
+ }
+ }
+ if sponsored {
<ReFungibleTransferBasket<T>>::insert(collection_id, item_id, block_number);
- true
- } else {
- false
}
+
+ sponsored
}
_ => {
false
runtime_types.jsondiffbeforeafterboth--- a/runtime_types.json
+++ b/runtime_types.json
@@ -42,7 +42,6 @@
"Fraction": "u128"
},
"FungibleItemType": {
- "Owner": "AccountId",
"Value": "u128"
},
"NftItemType": {
@@ -72,10 +71,6 @@
"VariableOnChainSchema": "Vec<u8>",
"ConstOnChainSchema": "Vec<u8>"
},
- "ApprovePermissions": {
- "Approved": "AccountId",
- "Amount": "u128"
- },
"RawData": "Vec<u8>",
"Address": "AccountId",
"LookupSource": "AccountId",
@@ -84,7 +79,9 @@
"const_data": "Vec<u8>",
"variable_data": "Vec<u8>"
},
- "CreateFungibleData": {},
+ "CreateFungibleData": {
+ "value": "u128"
+ },
"CreateReFungibleData": {
"const_data": "Vec<u8>",
"variable_data": "Vec<u8>"
@@ -104,10 +101,6 @@
},
"CollectionId": "u32",
"TokenId": "u32",
- "BasketItem": {
- "Address": "AccountId",
- "start_block": "BlockNumber"
- },
"ChainLimits": {
"collection_numbers_limit": "u32",
"account_token_ownership_limit": "u32",
tests/src/confirmSponsorship.test.tsdiffbeforeafterboth--- a/tests/src/confirmSponsorship.test.ts
+++ b/tests/src/confirmSponsorship.test.ts
@@ -16,6 +16,7 @@
createItemExpectSuccess,
findUnusedAddress,
getGenericResult,
+ enableWhiteListExpectSuccess,
} from "./util/helpers";
import { Keyring } from "@polkadot/api";
import { IKeyringPair } from "@polkadot/types/types";
@@ -72,7 +73,7 @@
// Mint token for unused address
const itemId = await createItemExpectSuccess(collectionId, 'NFT', zeroBalance.address, '//Alice');
- // Transfer this token from unused address to Alice
+ // Transfer this tokens from unused address to Alice
const zeroToAlice = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 0);
const events = await submitTransactionAsync(zeroBalance, zeroToAlice);
const result = getGenericResult(events);
@@ -86,30 +87,198 @@
});
it('Fungible: Transfer fees are paid by the sponsor after confirmation', async () => {
- expect(false).to.be.true;
+ const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'Fungible');
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ await usingApi(async (api) => {
+ const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ // Find unused address
+ const zeroBalance = await findUnusedAddress(api);
+
+ // Mint token for unused address
+ const itemId = await createItemExpectSuccess(collectionId, 'Fungible', zeroBalance.address, '//Alice');
+
+ // Transfer this tokens from unused address to Alice
+ const zeroToAlice = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 1);
+ const events1 = await submitTransactionAsync(zeroBalance, zeroToAlice);
+ const result1 = getGenericResult(events1);
+
+ const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ expect(result1.success).to.be.true;
+ expect(BsponsorBalance.lt(AsponsorBalance)).to.be.true;
+ });
});
it('ReFungible: Transfer fees are paid by the sponsor after confirmation', async () => {
- expect(false).to.be.true;
+ const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'ReFungible');
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ await usingApi(async (api) => {
+ const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ // Find unused address
+ const zeroBalance = await findUnusedAddress(api);
+
+ // Mint token for unused address
+ const itemId = await createItemExpectSuccess(collectionId, 'ReFungible', zeroBalance.address, '//Alice');
+
+ // Transfer this tokens from unused address to Alice
+ const zeroToAlice = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 1);
+ const events1 = await submitTransactionAsync(zeroBalance, zeroToAlice);
+ const result1 = getGenericResult(events1);
+
+ const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ expect(result1.success).to.be.true;
+ expect(BsponsorBalance.lt(AsponsorBalance)).to.be.true;
+ });
});
- it.skip('CreateItem fees are paid by the sponsor after confirmation', async () => {
- // const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
- // await setCollectionSponsorExpectSuccess(collectionId, bob.address);
- // await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
- expect(false).to.be.true;
+ it.only('CreateItem fees are paid by the sponsor after confirmation', async () => {
+ const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ // Enable collection white list
+ await enableWhiteListExpectSuccess(collectionId);
+
+ // Enable public minting
+
+ // Create Item
+
+
+
});
it('NFT: Sponsoring is rate limited', async () => {
- expect(false).to.be.true;
+ const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ await usingApi(async (api) => {
+ // Find unused address
+ const zeroBalance = await findUnusedAddress(api);
+
+ // Mint token for alice
+ const itemId = await createItemExpectSuccess(collectionId, 'NFT', alice.address, '//Alice');
+
+ // Transfer this token from Alice to unused address and back
+ // Alice to Zero gets sponsored
+ const aliceToZero = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 0);
+ const events1 = await submitTransactionAsync(alice, aliceToZero);
+ const result1 = getGenericResult(events1);
+
+ // Second transfer should fail
+ const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+ const zeroToAlice = api.tx.nft.transfer(alice.address, collectionId, itemId, 0);
+ const badTransaction = async function () {
+ console.log = function () {};
+ console.error = function () {};
+ await submitTransactionAsync(zeroBalance, zeroToAlice);
+ delete console.log;
+ delete console.error;
+ };
+ await expect(badTransaction()).to.be.rejectedWith("Inability to pay some fees");
+ const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ // Try again after Zero gets some balance - now it should succeed
+ const balancetx = api.tx.balances.transfer(zeroBalance.address, 1e15);
+ await submitTransactionAsync(alice, balancetx);
+ const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice);
+ const result2 = getGenericResult(events2);
+
+ expect(result1.success).to.be.true;
+ expect(result2.success).to.be.true;
+ expect(BsponsorBalance.isEqualTo(AsponsorBalance)).to.be.true;
+ });
});
it('Fungible: Sponsoring is rate limited', async () => {
- expect(false).to.be.true;
+ const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'Fungible');
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ await usingApi(async (api) => {
+ // Find unused address
+ const zeroBalance = await findUnusedAddress(api);
+
+ // Mint token for unused address
+ const itemId = await createItemExpectSuccess(collectionId, 'Fungible', zeroBalance.address, '//Alice');
+
+ // Transfer this tokens in parts from unused address to Alice
+ const zeroToAlice = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 1);
+ const events1 = await submitTransactionAsync(zeroBalance, zeroToAlice);
+ const result1 = getGenericResult(events1);
+
+ const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ const badTransaction = async function () {
+ console.log = function () {};
+ console.error = function () {};
+ await submitTransactionAsync(zeroBalance, zeroToAlice);
+ delete console.log;
+ delete console.error;
+ };
+
+ const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ // Try again after Zero gets some balance - now it should succeed
+ const balancetx = api.tx.balances.transfer(zeroBalance.address, 1e15);
+ await submitTransactionAsync(alice, balancetx);
+ const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice);
+ const result2 = getGenericResult(events2);
+
+ expect(result1.success).to.be.true;
+ expect(result2.success).to.be.true;
+ expect(BsponsorBalance.isEqualTo(AsponsorBalance)).to.be.true;
+ });
});
it('ReFungible: Sponsoring is rate limited', async () => {
- expect(false).to.be.true;
+ const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'ReFungible');
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ await usingApi(async (api) => {
+ // Find unused address
+ const zeroBalance = await findUnusedAddress(api);
+
+ // Mint token for alice
+ const itemId = await createItemExpectSuccess(collectionId, 'ReFungible', alice.address, '//Alice');
+
+ // Transfer this token from Alice to unused address and back
+ // Alice to Zero gets sponsored
+ const aliceToZero = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 1);
+ const events1 = await submitTransactionAsync(alice, aliceToZero);
+ const result1 = getGenericResult(events1);
+
+ // Second transfer should fail
+ const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+ const zeroToAlice = api.tx.nft.transfer(alice.address, collectionId, itemId, 1);
+ const badTransaction = async function () {
+ console.log = function () {};
+ console.error = function () {};
+ await submitTransactionAsync(zeroBalance, zeroToAlice);
+ delete console.log;
+ delete console.error;
+ };
+ await expect(badTransaction()).to.be.rejectedWith("Inability to pay some fees");
+ const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ // Try again after Zero gets some balance - now it should succeed
+ const balancetx = api.tx.balances.transfer(zeroBalance.address, 1e15);
+ await submitTransactionAsync(alice, balancetx);
+ const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice);
+ const result2 = getGenericResult(events2);
+
+ expect(result1.success).to.be.true;
+ expect(result2.success).to.be.true;
+ expect(BsponsorBalance.isEqualTo(AsponsorBalance)).to.be.true;
+ });
});
});
tests/src/util/helpers.tsdiffbeforeafterboth1//2// This file is subject to the terms and conditions defined in3// file 'LICENSE', which is part of this source code package.4//56import chai from 'chai';7import chaiAsPromised from 'chai-as-promised';8import type { AccountId, EventRecord } from '@polkadot/types/interfaces';9import { ApiPromise, Keyring } from "@polkadot/api";10import { default as usingApi, submitTransactionAsync } from "../substrate/substrate-api";11import privateKey from '../substrate/privateKey';12import { alicesPublicKey, nullPublicKey } from "../accounts";13import { strToUTF16, utf16ToStr, hexToStr } from '../util/util';14import { IKeyringPair } from "@polkadot/types/types";15import { BigNumber } from 'bignumber.js';1617chai.use(chaiAsPromised);18const expect = chai.expect;1920type GenericResult = {21 success: boolean,22};2324type CreateCollectionResult = {25 success: boolean,26 collectionId: number27};2829type CreateItemResult = {30 success: boolean,31 collectionId: number,32 itemId: number33};3435export function getGenericResult(events: EventRecord[]): GenericResult {36 let result: GenericResult = {37 success: false38 }39 events.forEach(({ phase, event: { data, method, section } }) => {40 // console.log(` ${phase}: ${section}.${method}:: ${data}`);41 if (method == 'ExtrinsicSuccess') {42 result.success = true;43 }44 });45 return result;46}4748function getCreateCollectionResult(events: EventRecord[]): CreateCollectionResult {49 let success = false;50 let collectionId: number = 0;51 events.forEach(({ phase, event: { data, method, section } }) => {52 // console.log(` ${phase}: ${section}.${method}:: ${data}`);53 if (method == 'ExtrinsicSuccess') {54 success = true;55 } else if ((section == 'nft') && (method == 'Created')) {56 collectionId = parseInt(data[0].toString());57 }58 });59 let result: CreateCollectionResult = {60 success,61 collectionId62 }63 return result;64}6566function getCreateItemResult(events: EventRecord[]): CreateItemResult {67 let success = false;68 let collectionId: number = 0;69 let itemId: number = 0;70 events.forEach(({ phase, event: { data, method, section } }) => {71 // console.log(` ${phase}: ${section}.${method}:: ${data}`);72 if (method == 'ExtrinsicSuccess') {73 success = true;74 } else if ((section == 'nft') && (method == 'ItemCreated')) {75 collectionId = parseInt(data[0].toString());76 itemId = parseInt(data[1].toString());77 }78 });79 let result: CreateItemResult = {80 success,81 collectionId,82 itemId83 }84 return result;85}8687export async function createCollectionExpectSuccess(name: string, description: string, tokenPrefix: string, mode: string): Promise<number> {88 let collectionId: number = 0;89 await usingApi(async (api) => {90 // Get number of collections before the transaction91 const AcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString());9293 // Run the CreateCollection transaction94 const alicePrivateKey = privateKey('//Alice');95 const tx = api.tx.nft.createCollection(strToUTF16(name), strToUTF16(description), strToUTF16(tokenPrefix), mode);96 const events = await submitTransactionAsync(alicePrivateKey, tx);97 const result = getCreateCollectionResult(events);9899 // Get number of collections after the transaction100 const BcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString());101102 // Get the collection 103 const collection: any = (await api.query.nft.collection(result.collectionId)).toJSON();104105 // What to expect106 expect(result.success).to.be.true;107 expect(result.collectionId).to.be.equal(BcollectionCount);108 expect(collection).to.be.not.null;109 expect(BcollectionCount).to.be.equal(AcollectionCount+1, 'Error: NFT collection NOT created.');110 expect(collection.Owner).to.be.equal(alicesPublicKey);111 expect(utf16ToStr(collection.Name)).to.be.equal(name);112 expect(utf16ToStr(collection.Description)).to.be.equal(description);113 expect(hexToStr(collection.TokenPrefix)).to.be.equal(tokenPrefix);114115 collectionId = result.collectionId;116 });117118 return collectionId;119}120 121export async function createCollectionExpectFailure(name: string, description: string, tokenPrefix: string, mode: string) {122 await usingApi(async (api) => {123 // Get number of collections before the transaction124 const AcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString());125126 // Run the CreateCollection transaction127 const alicePrivateKey = privateKey('//Alice');128 const tx = api.tx.nft.createCollection(name, description, tokenPrefix, mode);129 const events = await submitTransactionAsync(alicePrivateKey, tx);130 const result = getCreateCollectionResult(events);131132 // Get number of collections after the transaction133 const BcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString());134135 // What to expect136 expect(result.success).to.be.false;137 expect(BcollectionCount).to.be.equal(AcollectionCount, 'Error: Collection with incorrect data created.');138 });139}140 141export async function findUnusedAddress(api: ApiPromise): Promise<IKeyringPair> {142 let bal = new BigNumber(0);143 let unused;144 do {145 const randomSeed = 'seed' + Math.floor(Math.random() * Math.floor(10000));146 const keyring = new Keyring({ type: 'sr25519' });147 unused = keyring.addFromUri(`//${randomSeed}`);148 bal = new BigNumber((await api.query.system.account(unused.address)).data.free.toString());149 } while (bal.toFixed() != '0');150 return unused; 151}152153function getDestroyResult(events: EventRecord[]): boolean {154 let success: boolean = false;155 events.forEach(({ phase, event: { data, method, section } }) => {156 // console.log(` ${phase}: ${section}.${method}:: ${data}`);157 if (method == 'ExtrinsicSuccess') {158 success = true;159 }160 });161 return success;162}163164export async function destroyCollectionExpectFailure(collectionId: number, senderSeed: string = '//Alice') {165 await usingApi(async (api) => {166 // Run the DestroyCollection transaction167 const alicePrivateKey = privateKey(senderSeed);168 const tx = api.tx.nft.destroyCollection(collectionId);169 const events = await submitTransactionAsync(alicePrivateKey, tx);170 const result = getDestroyResult(events);171172 // What to expect173 expect(result).to.be.false;174 });175}176177export async function destroyCollectionExpectSuccess(collectionId: number, senderSeed: string = '//Alice') {178 await usingApi(async (api) => {179 // Run the DestroyCollection transaction180 const alicePrivateKey = privateKey(senderSeed);181 const tx = api.tx.nft.destroyCollection(collectionId);182 const events = await submitTransactionAsync(alicePrivateKey, tx);183 const result = getDestroyResult(events);184185 // Get the collection 186 const collection: any = (await api.query.nft.collection(collectionId)).toJSON();187188 // What to expect189 expect(result).to.be.true;190 expect(collection).to.be.not.null;191 expect(collection.Owner).to.be.equal(nullPublicKey);192 });193}194195export async function setCollectionSponsorExpectSuccess(collectionId: number, sponsor: string) {196 await usingApi(async (api) => {197198 // Run the transaction199 const alicePrivateKey = privateKey('//Alice');200 const tx = api.tx.nft.setCollectionSponsor(collectionId, sponsor);201 const events = await submitTransactionAsync(alicePrivateKey, tx);202 const result = getGenericResult(events);203204 // Get the collection 205 const collection: any = (await api.query.nft.collection(collectionId)).toJSON();206207 // What to expect208 expect(result.success).to.be.true;209 expect(collection.Sponsor.toString()).to.be.equal(sponsor.toString());210 expect(collection.SponsorConfirmed).to.be.false;211 });212}213214export async function setCollectionSponsorExpectFailure(collectionId: number, sponsor: string, senderSeed: string = '//Alice') {215 await usingApi(async (api) => {216217 // Run the transaction218 const alicePrivateKey = privateKey(senderSeed);219 const tx = api.tx.nft.setCollectionSponsor(collectionId, sponsor);220 const events = await submitTransactionAsync(alicePrivateKey, tx);221 const result = getGenericResult(events);222223 // What to expect224 expect(result.success).to.be.false;225 });226}227228export async function confirmSponsorshipExpectSuccess(collectionId: number, senderSeed: string = '//Alice') {229 await usingApi(async (api) => {230231 // Run the transaction232 const sender = privateKey(senderSeed);233 const tx = api.tx.nft.confirmSponsorship(collectionId);234 const events = await submitTransactionAsync(sender, tx);235 const result = getGenericResult(events);236237 // Get the collection 238 const collection: any = (await api.query.nft.collection(collectionId)).toJSON();239240 // What to expect241 expect(result.success).to.be.true;242 expect(collection.Sponsor).to.be.equal(sender.address);243 expect(collection.SponsorConfirmed).to.be.true;244 });245}246247export async function confirmSponsorshipExpectFailure(collectionId: number, senderSeed: string = '//Alice') {248 await usingApi(async (api) => {249250 // Run the transaction251 const sender = privateKey(senderSeed);252 const tx = api.tx.nft.confirmSponsorship(collectionId);253 const events = await submitTransactionAsync(sender, tx);254 const result = getGenericResult(events);255256 // What to expect257 expect(result.success).to.be.false;258 });259}260261export async function createItemExpectSuccess(collectionId: number, createMode: string, owner: string = '', senderSeed: string = '//Alice') {262 let newItemId: number = 0;263 await usingApi(async (api) => {264 const AItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString());265266 const sender = privateKey(senderSeed);267 if (owner === '') owner = sender.address;268 const tx = api.tx.nft.createItem(collectionId, owner, createMode);269 const events = await submitTransactionAsync(sender, tx);270 const result = getCreateItemResult(events);271 272 const BItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString());273274 // What to expect275 expect(result.success).to.be.true;276 expect(BItemCount).to.be.equal(AItemCount+1);277 expect(collectionId).to.be.equal(result.collectionId);278 expect(BItemCount).to.be.equal(result.itemId);279 newItemId = result.itemId;280 });281 return newItemId;282}