difftreelog
Refactor fungible logic, add positive tests for sponsorship confirmation
in: master
4 files changed
pallets/nft/src/lib.rsdiffbeforeafterboth152152153#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]153#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]154#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]154#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]155pub struct FungibleItemType<AccountId> {155pub struct FungibleItemType {156 pub owner: AccountId,157 pub value: u128,156 pub value: u128,158}157}159158165 pub variable_data: Vec<u8>,164 pub variable_data: Vec<u8>,166}165}167166168#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]169#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]170pub struct ApprovePermissions<AccountId> {171 pub approved: AccountId,172 pub amount: u128,173}174175// #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]167// #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]176// #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]168// #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]177// pub struct VestingItem<AccountId, Moment> {169// pub struct VestingItem<AccountId, Moment> {183// pub vesting_date: Moment,175// pub vesting_date: Moment,184// }176// }185177186#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]187#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]188pub struct BasketItem<AccountId, BlockNumber> {189 pub address: AccountId,190 pub start_block: BlockNumber,191}192193#[derive(Encode, Decode, Debug, Clone, PartialEq)]178#[derive(Encode, Decode, Debug, Clone, PartialEq)]194#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]179#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]195pub struct CollectionLimits {180pub struct CollectionLimits {260#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]245#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]261#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]246#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]262pub struct CreateFungibleData {247pub struct CreateFungibleData {248 pub value: u128,263}249}264250265#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]251#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]414 /// Balance owner per collection map400 /// Balance owner per collection map415 pub Balance get(fn balance_count): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => u128;401 pub Balance get(fn balance_count): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => u128;416402417 /// second parameter: item id + owner account id403 /// second parameter: item id + owner account id + spender account id418 pub ApprovedList get(fn approved): double_map hasher(identity) CollectionId, hasher(twox_64_concat) (TokenId, T::AccountId) => Vec<ApprovePermissions<T::AccountId>>;404 pub Allowances get(fn approved): double_map hasher(identity) CollectionId, hasher(twox_64_concat) (TokenId, T::AccountId, T::AccountId) => u128;419405420 /// Item collections406 /// Item collections421 pub NftItemList get(fn nft_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => NftItemType<T::AccountId>;407 pub NftItemList get(fn nft_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => NftItemType<T::AccountId>;422 pub FungibleItemList get(fn fungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => FungibleItemType<T::AccountId>;408 pub FungibleItemList get(fn fungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => FungibleItemType;423 pub ReFungibleItemList get(fn refungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => ReFungibleItemType<T::AccountId>;409 pub ReFungibleItemList get(fn refungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => ReFungibleItemType<T::AccountId>;424410425 /// Index list411 /// Index list426 pub AddressTokens get(fn address_tokens): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => Vec<TokenId>;412 pub AddressTokens get(fn address_tokens): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => Vec<TokenId>;427413428 /// Tokens transfer baskets414 /// Tokens transfer baskets429 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => T::BlockNumber;415 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => T::BlockNumber;430 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => Vec<BasketItem<T::AccountId, T::BlockNumber>>;416 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => T::BlockNumber;431 pub ReFungibleTransferBasket get(fn refungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => T::BlockNumber;417 pub ReFungibleTransferBasket get(fn refungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => T::BlockNumber;432418433 // Contract Sponsorship and Ownership419 // Contract Sponsorship and Ownership447 <Module<T>>::init_nft_token(*_c, _i);433 <Module<T>>::init_nft_token(*_c, _i);448 }434 }449435450 for (_num, _c, _i) in &config.fungible_item_id {436 for (collection_id, account_id, fungible_item) in &config.fungible_item_id {451 <Module<T>>::init_fungible_token(*_c, _i);437 <Module<T>>::init_fungible_token(*collection_id, account_id, fungible_item);452 }438 }453439454 for (_num, _c, _i) in &config.refungible_item_id {440 for (_num, _c, _i) in &config.refungible_item_id {619 Self::check_owner_permissions(collection_id, sender)?;605 Self::check_owner_permissions(collection_id, sender)?;620606621 <AddressTokens<T>>::remove_prefix(collection_id);607 <AddressTokens<T>>::remove_prefix(collection_id);622 <ApprovedList<T>>::remove_prefix(collection_id);608 <Allowances<T>>::remove_prefix(collection_id);623 <Balance<T>>::remove_prefix(collection_id);609 <Balance<T>>::remove_prefix(collection_id);624 <ItemListIndex>::remove(collection_id);610 <ItemListIndex>::remove(collection_id);625 <AdminList<T>>::remove(collection_id);611 <AdminList<T>>::remove(collection_id);1015 /// 1001 /// 1016 /// * item_id: ID of NFT to burn.1002 /// * item_id: ID of NFT to burn.1017 #[weight = T::WeightInfo::burn_item()]1003 #[weight = T::WeightInfo::burn_item()]1018 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId) -> DispatchResult {1004 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResult {101910051020 let sender = ensure_signed(origin)?;1006 let sender = ensure_signed(origin)?;1021 Self::collection_exists(collection_id)?;1007 Self::collection_exists(collection_id)?;1033 match target_collection.mode1019 match target_collection.mode1034 {1020 {1035 CollectionMode::NFT => Self::burn_nft_item(collection_id, item_id)?,1021 CollectionMode::NFT => Self::burn_nft_item(collection_id, item_id)?,1036 CollectionMode::Fungible(_) => Self::burn_fungible_item(collection_id, item_id)?,1022 CollectionMode::Fungible(_) => Self::burn_fungible_item(&sender, collection_id, value)?,1037 CollectionMode::ReFungible(_) => Self::burn_refungible_item(collection_id, item_id, sender.clone())?,1023 CollectionMode::ReFungible(_) => Self::burn_refungible_item(collection_id, item_id, sender.clone())?,1038 _ => ()1024 _ => ()1039 };1025 };1089 match target_collection.mode1075 match target_collection.mode1090 {1076 {1091 CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, sender.clone(), recipient)?,1077 CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, sender.clone(), recipient)?,1092 CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, item_id, value, sender.clone(), recipient)?,1078 CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, value, &sender, &recipient)?,1093 CollectionMode::ReFungible(_) => Self::transfer_refungible(collection_id, item_id, value, sender.clone(), recipient)?,1079 CollectionMode::ReFungible(_) => Self::transfer_refungible(collection_id, item_id, value, sender.clone(), recipient)?,1094 _ => ()1080 _ => ()1095 };1081 };1113 /// 1099 /// 1114 /// * item_id: ID of the item.1100 /// * item_id: ID of the item.1115 #[weight = T::WeightInfo::approve()]1101 #[weight = T::WeightInfo::approve()]1116 pub fn approve(origin, approved: T::AccountId, collection_id: CollectionId, item_id: TokenId) -> DispatchResult {1102 pub fn approve(origin, spender: T::AccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResult {111711031118 let sender = ensure_signed(origin)?;1104 let sender = ensure_signed(origin)?;11191105112511111126 if target_collection.access == AccessMode::WhiteList {1112 if target_collection.access == AccessMode::WhiteList {1127 Self::check_white_list(collection_id, &sender)?;1113 Self::check_white_list(collection_id, &sender)?;1128 Self::check_white_list(collection_id, &approved)?;1114 Self::check_white_list(collection_id, &spender)?;1129 }1115 }113011161131 // amount param stub1117 let allowance_exists = <Allowances<T>>::contains_key(collection_id, (item_id, &sender, &spender));1132 let amount = 100000000;11331134 let list_exists = <ApprovedList<T>>::contains_key(collection_id, (item_id, sender.clone()));1135 if list_exists {1118 let mut allowance: u128 = amount;11361137 let mut list = <ApprovedList<T>>::get(collection_id, (item_id, sender.clone()));1138 let item_contains = list.iter().any(|i| i.approved == approved);11391140 if !item_contains {1119 if allowance_exists {1141 list.push(ApprovePermissions { approved: approved.clone(), amount: amount });1120 allowance += <Allowances<T>>::get(collection_id, (item_id, &sender, &spender));1142 <ApprovedList<T>>::insert(collection_id, (item_id, sender.clone()), list);1143 }1144 } else {11451146 let mut list = Vec::new();1147 list.push(ApprovePermissions { approved: approved.clone(), amount: amount });1148 <ApprovedList<T>>::insert(collection_id, (item_id, sender.clone()), list);1149 }1121 }1122 <Allowances<T>>::insert(collection_id, (item_id, sender.clone(), spender.clone()), allowance);115011231151 Ok(())1124 Ok(())1152 }1125 }1176 let sender = ensure_signed(origin)?;1149 let sender = ensure_signed(origin)?;1177 let mut appoved_transfer = false;1150 let mut appoved_transfer = false;117811511179 // Check approve1152 // Check approval1180 if <ApprovedList<T>>::contains_key(collection_id, (item_id, from.clone())) {1153 let mut approval: u128 = 0;1154 if <Allowances<T>>::contains_key(collection_id, (item_id, &from, &recipient)) {1181 let list_itm = <ApprovedList<T>>::get(collection_id, (item_id, from.clone()));1155 approval = <Allowances<T>>::get(collection_id, (item_id, &from, &recipient));1182 let opt_item = list_itm.iter().find(|i| i.approved == sender.clone());1183 if opt_item.is_some()1156 ensure!(approval >= value, Error::<T>::TokenValueNotEnough);1184 {1185 appoved_transfer = true;1186 ensure!(opt_item.unwrap().amount >= value, Error::<T>::TokenValueNotEnough);1187 }1157 appoved_transfer = true;1188 }1158 }118911591190 let target_collection = <Collection<T>>::get(collection_id);1160 let target_collection = <Collection<T>>::get(collection_id);119411641195 // Transfer permissions check 1165 // Transfer permissions check 1196 ensure!(appoved_transfer || Self::is_owner_or_admin_permissions(collection_id, sender.clone()),1166 ensure!(appoved_transfer || Self::is_owner_or_admin_permissions(collection_id, sender.clone()),1197 Error::<T>::NoPermission);1167 Error::<T>::NoPermission);119811681199 if target_collection.access == AccessMode::WhiteList {1169 if target_collection.access == AccessMode::WhiteList {1200 Self::check_white_list(collection_id, &sender)?;1170 Self::check_white_list(collection_id, &sender)?;1201 Self::check_white_list(collection_id, &recipient)?;1171 Self::check_white_list(collection_id, &recipient)?;1202 }1172 }120311731204 // remove approve1174 // Reduce approval by transferred amount or remove if remaining approval drops to 01205 let approve_list: Vec<ApprovePermissions<T::AccountId>> = <ApprovedList<T>>::get(collection_id, (item_id, from.clone()))1175 if approval - value > 0 {1176 <Allowances<T>>::insert(collection_id, (item_id, &from, &recipient), approval - value);1206 .into_iter().filter(|i| i.approved != sender.clone()).collect();1177 }1178 else {1207 <ApprovedList<T>>::insert(collection_id, (item_id, from.clone()), approve_list);1179 <Allowances<T>>::remove(collection_id, (item_id, &from, &recipient));1180 }1208118112091210 match target_collection.mode1182 match target_collection.mode1211 {1183 {1212 CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, from, recipient)?,1184 CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, from, recipient)?,1213 CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, item_id, value, from.clone(), recipient)?,1185 CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, value, &from, &recipient)?,1214 CollectionMode::ReFungible(_) => Self::transfer_refungible(collection_id, item_id, value, from.clone(), recipient)?,1186 CollectionMode::ReFungible(_) => Self::transfer_refungible(collection_id, item_id, value, from.clone(), recipient)?,1215 _ => ()1187 _ => ()1216 };1188 };157415461575 Self::add_nft_item(collection_id, item)?;1547 Self::add_nft_item(collection_id, item)?;1576 },1548 },1577 CreateItemData::Fungible(_) => {1549 CreateItemData::Fungible(data) => {1578 let item = FungibleItemType {1550 Self::add_fungible_item(collection_id, &owner, data.value)?;1579 owner,1580 value: (10 as u128).pow(collection.decimal_points as u32)1581 };15821583 Self::add_fungible_item(collection_id, item)?;1584 },1551 },1585 CreateItemData::ReFungible(data) => {1552 CreateItemData::ReFungible(data) => {1586 let mut owner_list = Vec::new();1553 let mut owner_list = Vec::new();1603 Ok(())1570 Ok(())1604 }1571 }160515721606 fn add_fungible_item(collection_id: CollectionId, item: FungibleItemType<T::AccountId>) -> DispatchResult {1573 fn add_fungible_item(collection_id: CollectionId, owner: &T::AccountId, value: u128) -> DispatchResult {1607 let current_index = <ItemListIndex>::get(collection_id)1608 .checked_add(1)1609 .ok_or(Error::<T>::NumOverflow)?;1610 let itemcopy = item.clone();1611 let owner = item.owner.clone();161215741613 Self::add_token_index(collection_id, current_index, owner.clone())?;1575 // Does new owner already have an account?1576 let mut balance: u128 = 0;1577 if <FungibleItemList<T>>::contains_key(collection_id, owner) {1578 balance = <FungibleItemList<T>>::get(collection_id, owner).value;1579 } 161415801615 <ItemListIndex>::insert(collection_id, current_index);1581 // Mint 1582 let item = FungibleItemType {1583 value: balance + value1584 };1616 <FungibleItemList<T>>::insert(collection_id, current_index, itemcopy);1585 <FungibleItemList<T>>::insert(collection_id, (*owner).clone(), item);161715861618 // Add current block1619 let v: Vec<BasketItem<T::AccountId, T::BlockNumber>> = Vec::new();1620 <FungibleTransferBasket<T>>::insert(collection_id, current_index, v);1621 1622 // Update balance1587 // Update balance1623 let new_balance = <Balance<T>>::get(collection_id, owner.clone())1588 let new_balance = <Balance<T>>::get(collection_id, owner)1624 .checked_add(item.value)1589 .checked_add(value)1625 .ok_or(Error::<T>::NumOverflow)?;1590 .ok_or(Error::<T>::NumOverflow)?;1626 <Balance<T>>::insert(collection_id, owner.clone(), new_balance);1591 <Balance<T>>::insert(collection_id, (*owner).clone(), new_balance);162715921628 Ok(())1593 Ok(())1629 }1594 }1642 <ItemListIndex>::insert(collection_id, current_index);1607 <ItemListIndex>::insert(collection_id, current_index);1643 <ReFungibleItemList<T>>::insert(collection_id, current_index, itemcopy);1608 <ReFungibleItemList<T>>::insert(collection_id, current_index, itemcopy);164416091645 // Add current block1646 let block_number: T::BlockNumber = 0.into();1647 <ReFungibleTransferBasket<T>>::insert(collection_id, current_index, block_number);16481649 // Update balance1610 // Update balance1650 let new_balance = <Balance<T>>::get(collection_id, owner.clone())1611 let new_balance = <Balance<T>>::get(collection_id, owner.clone())1651 .checked_add(value)1612 .checked_add(value)1666 <ItemListIndex>::insert(collection_id, current_index);1627 <ItemListIndex>::insert(collection_id, current_index);1667 <NftItemList<T>>::insert(collection_id, current_index, item);1628 <NftItemList<T>>::insert(collection_id, current_index, item);166816291669 // Add current block1670 let block_number: T::BlockNumber = 0.into();1671 <NftTransferBasket<T>>::insert(collection_id, current_index, block_number);16721673 // Update balance1630 // Update balance1674 let new_balance = <Balance<T>>::get(collection_id, item_owner.clone())1631 let new_balance = <Balance<T>>::get(collection_id, item_owner.clone())1675 .checked_add(1)1632 .checked_add(1)1697 .unwrap();1654 .unwrap();1698 Self::remove_token_index(collection_id, item_id, owner.clone())?;1655 Self::remove_token_index(collection_id, item_id, owner.clone())?;169916561700 // remove approve list1701 <ApprovedList<T>>::remove(collection_id, (item_id, owner.clone()));17021703 // update balance1657 // update balance1704 let new_balance = <Balance<T>>::get(collection_id, item.owner.clone())1658 let new_balance = <Balance<T>>::get(collection_id, item.owner.clone())1705 .checked_sub(item.fraction)1659 .checked_sub(item.fraction)1719 let item = <NftItemList<T>>::get(collection_id, item_id);1673 let item = <NftItemList<T>>::get(collection_id, item_id);1720 Self::remove_token_index(collection_id, item_id, item.owner.clone())?;1674 Self::remove_token_index(collection_id, item_id, item.owner.clone())?;172116751722 // remove approve list1723 <ApprovedList<T>>::remove(collection_id, (item_id, item.owner.clone()));17241725 // update balance1676 // update balance1726 let new_balance = <Balance<T>>::get(collection_id, item.owner.clone())1677 let new_balance = <Balance<T>>::get(collection_id, item.owner.clone())1727 .checked_sub(1)1678 .checked_sub(1)1732 Ok(())1683 Ok(())1733 }1684 }173416851735 fn burn_fungible_item(collection_id: CollectionId, item_id: TokenId) -> DispatchResult {1686 fn burn_fungible_item(owner: &T::AccountId, collection_id: CollectionId, value: u128) -> DispatchResult {1736 ensure!(1687 ensure!(1737 <FungibleItemList<T>>::contains_key(collection_id, item_id),1688 <FungibleItemList<T>>::contains_key(collection_id, owner),1738 Error::<T>::TokenNotFound1689 Error::<T>::TokenNotFound1739 );1690 );1740 let item = <FungibleItemList<T>>::get(collection_id, item_id);1691 let mut balance = <FungibleItemList<T>>::get(collection_id, owner);1741 Self::remove_token_index(collection_id, item_id, item.owner.clone())?;1692 ensure!(balance.value >= value, Error::<T>::TokenValueNotEnough);174216931743 // remove approve list1744 <ApprovedList<T>>::remove(collection_id, (item_id, item.owner.clone()));17451746 // update balance1694 // update balance1747 let new_balance = <Balance<T>>::get(collection_id, item.owner.clone())1695 let new_balance = <Balance<T>>::get(collection_id, owner)1748 .checked_sub(item.value)1696 .checked_sub(value)1749 .ok_or(Error::<T>::NumOverflow)?;1697 .ok_or(Error::<T>::NumOverflow)?;1750 <Balance<T>>::insert(collection_id, item.owner.clone(), new_balance);1698 <Balance<T>>::insert(collection_id, (*owner).clone(), new_balance);175116991752 <FungibleItemList<T>>::remove(collection_id, item_id);1700 if balance.value - value > 0 {1701 balance.value -= value;1702 <FungibleItemList<T>>::insert(collection_id, (*owner).clone(), balance);1703 }1704 else {1705 <FungibleItemList<T>>::remove(collection_id, owner);1706 }175317071754 Ok(())1708 Ok(())1755 }1709 }1810 <NftItemList<T>>::get(collection_id, item_id).owner == subject1764 <NftItemList<T>>::get(collection_id, item_id).owner == subject1811 }1765 }1812 CollectionMode::Fungible(_) => {1766 CollectionMode::Fungible(_) => {1813 <FungibleItemList<T>>::get(collection_id, item_id).owner == subject1767 <FungibleItemList<T>>::contains_key(collection_id, &subject)1814 }1768 }1815 CollectionMode::ReFungible(_) => {1769 CollectionMode::ReFungible(_) => {1816 <ReFungibleItemList<T>>::get(collection_id, item_id)1770 <ReFungibleItemList<T>>::get(collection_id, item_id)183317871834 fn transfer_fungible(1788 fn transfer_fungible(1835 collection_id: CollectionId,1789 collection_id: CollectionId,1836 item_id: TokenId,1837 value: u128,1790 value: u128,1838 owner: T::AccountId,1791 owner: &T::AccountId,1839 new_owner: T::AccountId,1792 recipient: &T::AccountId,1840 ) -> DispatchResult {1793 ) -> DispatchResult {1841 ensure!(1794 ensure!(1842 <FungibleItemList<T>>::contains_key(collection_id, item_id),1795 <FungibleItemList<T>>::contains_key(collection_id, owner),1843 Error::<T>::TokenNotFound1796 Error::<T>::TokenNotFound1844 );1797 );184517981846 let full_item = <FungibleItemList<T>>::get(collection_id, item_id);1799 let mut balance = <FungibleItemList<T>>::get(collection_id, owner);1847 let amount = full_item.value;1800 ensure!(balance.value >= value, Error::<T>::TokenValueTooLow);184818011849 ensure!(amount >= value, Error::<T>::TokenValueTooLow);1802 // Send balance to recipient (updates balanceOf of recipient)1803 Self::add_fungible_item(collection_id, recipient, value)?;185018041851 // update balance1805 // update balanceOf of sender1852 let balance_old_owner = <Balance<T>>::get(collection_id, owner.clone())1806 <Balance<T>>::insert(collection_id, (*owner).clone(), balance.value - value);1853 .checked_sub(value)1854 .ok_or(Error::<T>::NumOverflow)?;1855 <Balance<T>>::insert(collection_id, owner.clone(), balance_old_owner);185618071857 let mut new_owner_account_id = 0;1808 // Reduce or remove sender1858 let new_owner_items = <AddressTokens<T>>::get(collection_id, new_owner.clone());1809 if balance.value == value {1859 if new_owner_items.len() > 0 {1860 new_owner_account_id = new_owner_items[0];1810 <FungibleItemList<T>>::remove(collection_id, owner);1861 }1811 }18621812 else {1863 // transfer1813 balance.value -= value;1864 if amount == value && new_owner_account_id == 0 {1814 <FungibleItemList<T>>::insert(collection_id, (*owner).clone(), balance);1865 // change owner1866 // new owner do not have account1867 let mut new_full_item = full_item.clone();1868 new_full_item.owner = new_owner.clone();1869 <FungibleItemList<T>>::insert(collection_id, item_id, new_full_item);18701871 // update balance1872 let balancenew_owner = <Balance<T>>::get(collection_id, new_owner.clone())1873 .checked_add(value)1874 .ok_or(Error::<T>::NumOverflow)?;1875 <Balance<T>>::insert(collection_id, new_owner.clone(), balancenew_owner);18761877 // update index collection1878 Self::move_token_index(collection_id, item_id, owner.clone(), new_owner.clone())?;1879 } else {1880 let mut new_full_item = full_item.clone();1881 new_full_item.value -= value;18821883 // separate amount1884 if new_owner_account_id > 0 {1885 // new owner has account1886 let mut item = <FungibleItemList<T>>::get(collection_id, new_owner_account_id);1887 item.value += value;18881889 // update balance1890 let balancenew_owner = <Balance<T>>::get(collection_id, new_owner.clone())1891 .checked_add(value)1892 .ok_or(Error::<T>::NumOverflow)?;1893 <Balance<T>>::insert(collection_id, new_owner.clone(), balancenew_owner);18941895 <FungibleItemList<T>>::insert(collection_id, new_owner_account_id, item);1896 } else {1897 // new owner do not have account1898 let item = FungibleItemType {1899 owner: new_owner.clone(),1900 value1901 };19021903 Self::add_fungible_item(collection_id, item)?;1904 }19051906 if amount == value {1907 Self::remove_token_index(collection_id, item_id, full_item.owner.clone())?;19081909 // remove approve list1910 <ApprovedList<T>>::remove(collection_id, (item_id, full_item.owner.clone()));1911 <FungibleItemList<T>>::remove(collection_id, item_id);1912 }19131914 <FungibleItemList<T>>::insert(collection_id, item_id, new_full_item);1915 }1815 }191618161917 Ok(())1817 Ok(())2039 // update index collection1939 // update index collection2040 Self::move_token_index(collection_id, item_id, old_owner.clone(), new_owner.clone())?;1940 Self::move_token_index(collection_id, item_id, old_owner.clone(), new_owner.clone())?;204119412042 // reset approved list2043 <ApprovedList<T>>::remove(collection_id, (item_id, old_owner));2044 Ok(())1942 Ok(())2045 }1943 }2046 1944 2052 match mode {1950 match mode {2053 CollectionMode::NFT => ensure!(<NftItemList<T>>::contains_key(collection_id, item_id), Error::<T>::TokenNotFound),1951 CollectionMode::NFT => ensure!(<NftItemList<T>>::contains_key(collection_id, item_id), Error::<T>::TokenNotFound),2054 CollectionMode::ReFungible(_) => ensure!(<ReFungibleItemList<T>>::contains_key(collection_id, item_id), Error::<T>::TokenNotFound),1952 CollectionMode::ReFungible(_) => ensure!(<ReFungibleItemList<T>>::contains_key(collection_id, item_id), Error::<T>::TokenNotFound),2055 CollectionMode::Fungible(_) => ensure!(<FungibleItemList<T>>::contains_key(collection_id, item_id), Error::<T>::TokenNotFound),2056 _ => ()1953 _ => ()2057 };1954 };2058 1955 2131 <Balance<T>>::insert(collection_id, item_owner.clone(), new_balance);2028 <Balance<T>>::insert(collection_id, item_owner.clone(), new_balance);2132 }2029 }213320302134 fn init_fungible_token(collection_id: CollectionId, item: &FungibleItemType<T::AccountId>) {2031 fn init_fungible_token(collection_id: CollectionId, owner: &T::AccountId, item: &FungibleItemType) {2135 let current_index = <ItemListIndex>::get(collection_id)2032 let current_index = <ItemListIndex>::get(collection_id)2136 .checked_add(1)2033 .checked_add(1)2137 .unwrap();2034 .unwrap();2138 let owner = item.owner.clone();213920352140 Self::add_token_index(collection_id, current_index, owner.clone()).unwrap();2036 Self::add_token_index(collection_id, current_index, (*owner).clone()).unwrap();214120372142 <ItemListIndex>::insert(collection_id, current_index);2038 <ItemListIndex>::insert(collection_id, current_index);214320392144 // Update balance2040 // Update balance2145 let new_balance = <Balance<T>>::get(collection_id, owner.clone())2041 let new_balance = <Balance<T>>::get(collection_id, owner)2146 .checked_add(item.value)2042 .checked_add(item.value)2147 .unwrap();2043 .unwrap();2148 <Balance<T>>::insert(collection_id, owner.clone(), new_balance);2044 <Balance<T>>::insert(collection_id, (*owner).clone(), new_balance);2149 }2045 }215020462151 fn init_refungible_token(collection_id: CollectionId, item: &ReFungibleItemType<T::AccountId>) {2047 fn init_refungible_token(collection_id: CollectionId, item: &ReFungibleItemType<T::AccountId>) {2343 T::AccountId::default()2239 T::AccountId::default()2344 }2240 }2345 }2241 }2346 Some(Call::transfer(new_owner, collection_id, item_id, _value)) => {2242 Some(Call::transfer(_new_owner, collection_id, item_id, _value)) => {2347 2243 2348 let mut sponsor_transfer = false;2244 let mut sponsor_transfer = false;2349 if <Collection<T>>::get(collection_id).sponsor_confirmed {2245 if <Collection<T>>::get(collection_id).sponsor_confirmed {2352 let collection_mode = <Collection<T>>::get(collection_id).mode;2248 let collection_mode = <Collection<T>>::get(collection_id).mode;2353 2249 2354 // sponsor timeout2250 // sponsor timeout2251 let block_number = <system::Module<T>>::block_number() as T::BlockNumber;2355 sponsor_transfer = match collection_mode {2252 sponsor_transfer = match collection_mode {2356 CollectionMode::NFT => {2253 CollectionMode::NFT => {2357 2254 2362 ChainLimit::get().nft_sponsor_transfer_timeout2259 ChainLimit::get().nft_sponsor_transfer_timeout2363 };2260 };2364 2261 2365 let basket = <NftTransferBasket<T>>::get(collection_id, item_id);2262 let mut sponsored = true;2263 if <NftTransferBasket<T>>::contains_key(collection_id, item_id) {2366 let block_number = <system::Module<T>>::block_number() as T::BlockNumber;2264 let last_tx_block = <NftTransferBasket<T>>::get(collection_id, item_id);2367 let limit_time = basket + limit.into();2265 let limit_time = last_tx_block + limit.into();2368 if block_number >= limit_time {2266 if block_number <= limit_time {2267 sponsored = false;2268 }2269 }2270 if sponsored {2369 <NftTransferBasket<T>>::insert(collection_id, item_id, block_number);2271 <NftTransferBasket<T>>::insert(collection_id, item_id, block_number);2370 true2371 }2272 }2372 else {22732373 false2374 }2274 sponsored2375 }2275 }2376 CollectionMode::Fungible(_) => {2276 CollectionMode::Fungible(_) => {2377 2277 2382 ChainLimit::get().fungible_sponsor_transfer_timeout2282 ChainLimit::get().fungible_sponsor_transfer_timeout2383 };2283 };2384 2284 2385 let mut basket = <FungibleTransferBasket<T>>::get(collection_id, item_id);2386 let block_number = <system::Module<T>>::block_number() as T::BlockNumber;2285 let block_number = <system::Module<T>>::block_number() as T::BlockNumber;2387 if basket.iter().any(|i| i.address == new_owner.clone())2286 let mut sponsored = true;2388 {2287 if <FungibleTransferBasket<T>>::contains_key(collection_id, who) {2389 let item = basket.iter_mut().find(|i| i.address == new_owner.clone()).unwrap().clone();2288 let last_tx_block = <FungibleTransferBasket<T>>::get(collection_id, who);2390 let limit_time = item.start_block + limit.into();2289 let limit_time = last_tx_block + limit.into();2391 if block_number >= limit_time {2290 if block_number <= limit_time {2392 basket.retain(|x| x.address == item.address);2291 sponsored = false;2393 basket.push(BasketItem { start_block: block_number, address: new_owner.clone() });2394 <FungibleTransferBasket<T>>::insert(collection_id, item_id, basket);2395 true2396 }2292 }2397 else {2398 false2399 }2400 }2293 }2401 else {2294 if sponsored {2402 basket.push(BasketItem { start_block: block_number, address: new_owner.clone()});2295 <FungibleTransferBasket<T>>::insert(collection_id, who, block_number);2403 true2404 }2296 }22972298 sponsored2405 }2299 }2406 CollectionMode::ReFungible(_) => {2300 CollectionMode::ReFungible(_) => {2407 2301 2412 ChainLimit::get().refungible_sponsor_transfer_timeout2306 ChainLimit::get().refungible_sponsor_transfer_timeout2413 };2307 };2414 2308 2415 let basket = <ReFungibleTransferBasket<T>>::get(collection_id, item_id);2309 let mut sponsored = true;2310 if <ReFungibleTransferBasket<T>>::contains_key(collection_id, item_id) {2416 let block_number = <system::Module<T>>::block_number() as T::BlockNumber;2311 let last_tx_block = <ReFungibleTransferBasket<T>>::get(collection_id, item_id);2417 let limit_time = basket + limit.into();2312 let limit_time = last_tx_block + limit.into();2418 if block_number >= limit_time {2313 if block_number <= limit_time {2314 sponsored = false;2315 }2316 }2317 if sponsored {2419 <ReFungibleTransferBasket<T>>::insert(collection_id, item_id, block_number);2318 <ReFungibleTransferBasket<T>>::insert(collection_id, item_id, block_number);2420 true2421 } else {2422 false2423 }2319 }23202321 sponsored2424 }2322 }2425 _ => {2323 _ => {2426 false2324 falseruntime_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.tsdiffbeforeafterboth--- a/tests/src/util/helpers.ts
+++ b/tests/src/util/helpers.ts
@@ -13,6 +13,8 @@
import { strToUTF16, utf16ToStr, hexToStr } from '../util/util';
import { IKeyringPair } from "@polkadot/types/types";
import { BigNumber } from 'bignumber.js';
+import { Struct, Enum } from '@polkadot/types/codec';
+import { u128 } from '@polkadot/types/primitive';
chai.use(chaiAsPromised);
const expect = chai.expect;
@@ -258,25 +260,73 @@
});
}
+export interface CreateFungibleData extends Struct {
+ readonly value: u128;
+};
+
+export interface CreateReFungibleData extends Struct {};
+export interface CreateNftData extends Struct {};
+
+export interface CreateItemData extends Enum {
+ NFT: CreateNftData,
+ Fungible: CreateFungibleData,
+ ReFungible: CreateReFungibleData
+};
+
export async function createItemExpectSuccess(collectionId: number, createMode: string, owner: string = '', senderSeed: string = '//Alice') {
let newItemId: number = 0;
await usingApi(async (api) => {
const AItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString());
+ const Aitem: any = (await api.query.nft.fungibleItemList(collectionId, owner)).toJSON();
+ const AItemBalance = new BigNumber(Aitem.Value);
const sender = privateKey(senderSeed);
if (owner === '') owner = sender.address;
- const tx = api.tx.nft.createItem(collectionId, owner, createMode);
+
+ let tx;
+ if (createMode == 'Fungible') {
+ let createData = {fungible: {value: 10}};
+ tx = api.tx.nft.createItem(collectionId, owner, createData);
+ }
+ else {
+ tx = api.tx.nft.createItem(collectionId, owner, createMode);
+ }
const events = await submitTransactionAsync(sender, tx);
const result = getCreateItemResult(events);
-
+
const BItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString());
+ const Bitem: any = (await api.query.nft.fungibleItemList(collectionId, owner)).toJSON();
+ const BItemBalance = new BigNumber(Bitem.Value);
// What to expect
expect(result.success).to.be.true;
- expect(BItemCount).to.be.equal(AItemCount+1);
+ if (createMode == 'Fungible') {
+ expect(BItemBalance.minus(AItemBalance).toNumber()).to.be.equal(10);
+ }
+ else {
+ expect(BItemCount).to.be.equal(AItemCount+1);
+ }
expect(collectionId).to.be.equal(result.collectionId);
expect(BItemCount).to.be.equal(result.itemId);
newItemId = result.itemId;
});
return newItemId;
}
+
+export async function enableWhiteListExpectSuccess(collectionId: number, senderSeed: string = '//Alice') {
+ await usingApi(async (api) => {
+
+ // Run the transaction
+ const sender = privateKey(senderSeed);
+ const tx = api.tx.nft.setPublicAccessMode(collectionId, 'WhiteList');
+ const events = await submitTransactionAsync(sender, tx);
+ const result = getGenericResult(events);
+
+ // Get the collection
+ const collection: any = (await api.query.nft.collection(collectionId)).toJSON();
+
+ // What to expect
+ expect(result.success).to.be.true;
+ expect(collection.Access).to.be.equal('WhiteList');
+ });
+}