difftreelog
Merge pull request #50 from usetech-llc/feature/NFTPAR-250
in: master
NFTPAR-250 Tests for collection sponsoring
9 files changed
node/src/chain_spec.rsdiffbeforeafterboth--- a/node/src/chain_spec.rs
+++ b/node/src/chain_spec.rs
@@ -193,7 +193,7 @@
offchain_schema: vec![],
schema_version: SchemaVersion::default(),
sponsor: get_account_id_from_seed::<sr25519::Public>("Alice"),
- unconfirmed_sponsor: get_account_id_from_seed::<sr25519::Public>("Alice"),
+ sponsor_confirmed: true,
const_on_chain_schema: vec![],
variable_on_chain_schema: vec![],
limits: CollectionLimits::default()
pallets/nft/src/lib.rsdiffbeforeafterboth--- a/pallets/nft/src/lib.rs
+++ b/pallets/nft/src/lib.rs
@@ -136,7 +136,7 @@
pub offchain_schema: Vec<u8>,
pub schema_version: SchemaVersion,
pub sponsor: AccountId, // Who pays fees. If set to default address, the fees are applied to the transaction sender
- pub unconfirmed_sponsor: AccountId, // Sponsor address that has not yet confirmed sponsorship
+ pub sponsor_confirmed: bool, // False if sponsor address has not yet confirmed sponsorship. True otherwise.
pub limits: CollectionLimits, // Collection private restrictions
pub variable_on_chain_schema: Vec<u8>, //
pub const_on_chain_schema: Vec<u8>, //
@@ -145,7 +145,6 @@
#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct NftItemType<AccountId> {
- pub collection: CollectionId,
pub owner: AccountId,
pub const_data: Vec<u8>,
pub variable_data: Vec<u8>,
@@ -153,45 +152,28 @@
#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
-pub struct FungibleItemType<AccountId> {
- pub collection: CollectionId,
- pub owner: AccountId,
+pub struct FungibleItemType {
pub value: u128,
}
#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct ReFungibleItemType<AccountId> {
- pub collection: CollectionId,
pub owner: Vec<Ownership<AccountId>>,
pub const_data: Vec<u8>,
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> {
- pub sender: AccountId,
- pub recipient: AccountId,
- pub collection_id: CollectionId,
- pub item_id: TokenId,
- 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, Default, Debug, Clone, PartialEq)]
+// #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+// pub struct VestingItem<AccountId, Moment> {
+// pub sender: AccountId,
+// pub recipient: AccountId,
+// pub collection_id: CollectionId,
+// pub item_id: TokenId,
+// pub amount: u64,
+// pub vesting_date: Moment,
+// }
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
@@ -263,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)]
@@ -417,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
@@ -430,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
@@ -448,16 +431,16 @@
<Module<T>>::init_collection(_c);
}
- for (_num, _q, _i) in &config.nft_item_id {
- <Module<T>>::init_nft_token(_i);
+ for (_num, _c, _i) in &config.nft_item_id {
+ <Module<T>>::init_nft_token(*_c, _i);
}
- for (_num, _q, _i) in &config.fungible_item_id {
- <Module<T>>::init_fungible_token(_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, _q, _i) in &config.refungible_item_id {
- <Module<T>>::init_refungible_token(_i);
+ for (_num, _c, _i) in &config.refungible_item_id {
+ <Module<T>>::init_refungible_token(*_c, _i);
}
})
}
@@ -593,7 +576,7 @@
offchain_schema: Vec::new(),
schema_version: SchemaVersion::ImageURL,
sponsor: T::AccountId::default(),
- unconfirmed_sponsor: T::AccountId::default(),
+ sponsor_confirmed: false,
variable_on_chain_schema: Vec::new(),
const_on_chain_schema: Vec::new(),
limits: CollectionLimits::default(),
@@ -624,7 +607,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);
@@ -852,7 +835,8 @@
let mut target_collection = <Collection<T>>::get(collection_id);
ensure!(sender == target_collection.owner, Error::<T>::NoPermission);
- target_collection.unconfirmed_sponsor = new_sponsor;
+ target_collection.sponsor = new_sponsor;
+ target_collection.sponsor_confirmed = false;
<Collection<T>>::insert(collection_id, target_collection);
Ok(())
@@ -872,10 +856,9 @@
ensure!(<Collection<T>>::contains_key(collection_id), Error::<T>::CollectionNotFound);
let mut target_collection = <Collection<T>>::get(collection_id);
- ensure!(sender == target_collection.unconfirmed_sponsor, Error::<T>::ConfirmUnsetSponsorFail);
+ ensure!(sender == target_collection.sponsor, Error::<T>::ConfirmUnsetSponsorFail);
- target_collection.sponsor = target_collection.unconfirmed_sponsor;
- target_collection.unconfirmed_sponsor = T::AccountId::default();
+ target_collection.sponsor_confirmed = true;
<Collection<T>>::insert(collection_id, target_collection);
Ok(())
@@ -900,6 +883,7 @@
ensure!(sender == target_collection.owner, Error::<T>::NoPermission);
target_collection.sponsor = T::AccountId::default();
+ target_collection.sponsor_confirmed = false;
<Collection<T>>::insert(collection_id, target_collection);
Ok(())
@@ -1000,7 +984,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)?;
@@ -1018,7 +1002,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())?,
_ => ()
};
@@ -1074,7 +1058,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)?,
_ => ()
};
@@ -1098,7 +1082,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)?;
@@ -1110,28 +1094,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(())
}
@@ -1161,15 +1132,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);
@@ -1179,23 +1147,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)?,
_ => ()
};
@@ -1614,22 +1584,15 @@
{
CreateItemData::NFT(data) => {
let item = NftItemType {
- collection: collection_id,
owner,
const_data: data.const_data,
variable_data: data.variable_data
};
- Self::add_nft_item(item)?;
+ Self::add_nft_item(collection_id, item)?;
},
- CreateItemData::Fungible(_) => {
- let item = FungibleItemType {
- collection: collection_id,
- owner,
- value: (10 as u128).pow(collection.decimal_points as u32)
- };
-
- Self::add_fungible_item(item)?;
+ CreateItemData::Fungible(data) => {
+ Self::add_fungible_item(collection_id, &owner, data.value)?;
},
CreateItemData::ReFungible(data) => {
let mut owner_list = Vec::new();
@@ -1637,13 +1600,12 @@
owner_list.push(Ownership {owner: owner.clone(), fraction: value});
let item = ReFungibleItemType {
- collection: collection_id,
owner: owner_list,
const_data: data.const_data,
variable_data: data.variable_data
};
- Self::add_refungible_item(item)?;
+ Self::add_refungible_item(collection_id, item)?;
}
};
@@ -1653,33 +1615,31 @@
Ok(())
}
- fn add_fungible_item(item: FungibleItemType<T::AccountId>) -> DispatchResult {
- let current_index = <ItemListIndex>::get(item.collection)
- .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(item.collection, 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(item.collection, current_index);
- <FungibleItemList<T>>::insert(item.collection, 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(item.collection, current_index, v);
-
// Update balance
- let new_balance = <Balance<T>>::get(item.collection, 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(item.collection, owner.clone(), new_balance);
+ <Balance<T>>::insert(collection_id, (*owner).clone(), new_balance);
Ok(())
}
- fn add_refungible_item(item: ReFungibleItemType<T::AccountId>) -> DispatchResult {
- let current_index = <ItemListIndex>::get(item.collection)
+ fn add_refungible_item(collection_id: CollectionId, item: ReFungibleItemType<T::AccountId>) -> DispatchResult {
+ let current_index = <ItemListIndex>::get(collection_id)
.checked_add(1)
.ok_or(Error::<T>::NumOverflow)?;
let itemcopy = item.clone();
@@ -1687,40 +1647,31 @@
let value = item.owner.first().unwrap().fraction;
let owner = item.owner.first().unwrap().owner.clone();
- Self::add_token_index(item.collection, current_index, owner.clone())?;
+ Self::add_token_index(collection_id, current_index, owner.clone())?;
- <ItemListIndex>::insert(item.collection, current_index);
- <ReFungibleItemList<T>>::insert(item.collection, current_index, itemcopy);
-
- // Add current block
- let block_number: T::BlockNumber = 0.into();
- <ReFungibleTransferBasket<T>>::insert(item.collection, current_index, block_number);
+ <ItemListIndex>::insert(collection_id, current_index);
+ <ReFungibleItemList<T>>::insert(collection_id, current_index, itemcopy);
// Update balance
- let new_balance = <Balance<T>>::get(item.collection, owner.clone())
+ let new_balance = <Balance<T>>::get(collection_id, owner.clone())
.checked_add(value)
.ok_or(Error::<T>::NumOverflow)?;
- <Balance<T>>::insert(item.collection, owner.clone(), new_balance);
+ <Balance<T>>::insert(collection_id, owner.clone(), new_balance);
Ok(())
}
- fn add_nft_item(item: NftItemType<T::AccountId>) -> DispatchResult {
- let current_index = <ItemListIndex>::get(item.collection)
+ fn add_nft_item(collection_id: CollectionId, item: NftItemType<T::AccountId>) -> DispatchResult {
+ let current_index = <ItemListIndex>::get(collection_id)
.checked_add(1)
.ok_or(Error::<T>::NumOverflow)?;
let item_owner = item.owner.clone();
- let collection_id = item.collection.clone();
Self::add_token_index(collection_id, current_index, item.owner.clone())?;
<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())
.checked_add(1)
@@ -1747,9 +1698,6 @@
.next()
.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())
@@ -1770,9 +1718,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())
.checked_sub(1)
@@ -1783,24 +1728,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(())
}
@@ -1861,7 +1809,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)
@@ -1882,86 +1830,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 balance_new_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(), balance_new_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 balance_new_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(), balance_new_owner);
-
- <FungibleItemList<T>>::insert(collection_id, new_owner_account_id, item);
- } else {
- // new owner do not have account
- let item = FungibleItemType {
- collection: collection_id,
- owner: new_owner.clone(),
- value
- };
-
- Self::add_fungible_item(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(())
@@ -1996,10 +1889,10 @@
.ok_or(Error::<T>::NumOverflow)?;
<Balance<T>>::insert(collection_id, item.owner.clone(), balance_old_owner);
- let balance_new_owner = <Balance<T>>::get(collection_id, new_owner.clone())
+ 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(), balance_new_owner);
+ <Balance<T>>::insert(collection_id, new_owner.clone(), balancenew_owner);
let old_owner = item.owner.clone();
let new_owner_has_account = full_item.owner.iter().any(|i| i.owner == new_owner);
@@ -2076,10 +1969,10 @@
.ok_or(Error::<T>::NumOverflow)?;
<Balance<T>>::insert(collection_id, item.owner.clone(), balance_old_owner);
- let balance_new_owner = <Balance<T>>::get(collection_id, new_owner.clone())
+ let balancenew_owner = <Balance<T>>::get(collection_id, new_owner.clone())
.checked_add(1)
.ok_or(Error::<T>::NumOverflow)?;
- <Balance<T>>::insert(collection_id, new_owner.clone(), balance_new_owner);
+ <Balance<T>>::insert(collection_id, new_owner.clone(), balancenew_owner);
// change owner
let old_owner = item.owner.clone();
@@ -2089,8 +1982,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(())
}
@@ -2102,7 +1993,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),
_ => ()
};
@@ -2164,13 +2054,12 @@
CreatedCollectionCount::put(next_id);
}
- fn init_nft_token(item: &NftItemType<T::AccountId>) {
- let current_index = <ItemListIndex>::get(item.collection)
+ fn init_nft_token(collection_id: CollectionId, item: &NftItemType<T::AccountId>) {
+ let current_index = <ItemListIndex>::get(collection_id)
.checked_add(1)
.unwrap();
let item_owner = item.owner.clone();
- let collection_id = item.collection.clone();
Self::add_token_index(collection_id, current_index, item.owner.clone()).unwrap();
<ItemListIndex>::insert(collection_id, current_index);
@@ -2182,40 +2071,39 @@
<Balance<T>>::insert(collection_id, item_owner.clone(), new_balance);
}
- fn init_fungible_token(item: &FungibleItemType<T::AccountId>) {
- let current_index = <ItemListIndex>::get(item.collection)
+ 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(item.collection, current_index, owner.clone()).unwrap();
+ Self::add_token_index(collection_id, current_index, (*owner).clone()).unwrap();
- <ItemListIndex>::insert(item.collection, current_index);
+ <ItemListIndex>::insert(collection_id, current_index);
// Update balance
- let new_balance = <Balance<T>>::get(item.collection, owner.clone())
+ let new_balance = <Balance<T>>::get(collection_id, owner)
.checked_add(item.value)
.unwrap();
- <Balance<T>>::insert(item.collection, owner.clone(), new_balance);
+ <Balance<T>>::insert(collection_id, (*owner).clone(), new_balance);
}
- fn init_refungible_token(item: &ReFungibleItemType<T::AccountId>) {
- let current_index = <ItemListIndex>::get(item.collection)
+ fn init_refungible_token(collection_id: CollectionId, item: &ReFungibleItemType<T::AccountId>) {
+ let current_index = <ItemListIndex>::get(collection_id)
.checked_add(1)
.unwrap();
let value = item.owner.first().unwrap().fraction;
let owner = item.owner.first().unwrap().owner.clone();
- Self::add_token_index(item.collection, current_index, owner.clone()).unwrap();
+ Self::add_token_index(collection_id, current_index, owner.clone()).unwrap();
- <ItemListIndex>::insert(item.collection, current_index);
+ <ItemListIndex>::insert(collection_id, current_index);
// Update balance
- let new_balance = <Balance<T>>::get(item.collection, owner.clone())
+ let new_balance = <Balance<T>>::get(collection_id, owner.clone())
.checked_add(value)
.unwrap();
- <Balance<T>>::insert(item.collection, owner.clone(), new_balance);
+ <Balance<T>>::insert(collection_id, owner.clone(), new_balance);
}
fn add_token_index(collection_id: CollectionId, item_index: TokenId, owner: T::AccountId) -> DispatchResult {
@@ -2386,99 +2274,111 @@
// };
let fee = Self::traditional_fee(len, info, tip);
+ // Only mess with balances if fee is not zero.
+ if fee.is_zero() {
+ return Ok((fee, None));
+ }
+
// Determine who is paying transaction fee based on ecnomic model
// Parse call to extract collection ID and access collection sponsor
let mut sponsor: T::AccountId = match IsSubType::<Call<T>>::is_sub_type(call) {
Some(Call::create_item(collection_id, _owner, _properties)) => {
// check free create limit
- if <Collection<T>>::get(collection_id).limits.sponsored_data_size >= (_properties.len() as u32)
+ if (<Collection<T>>::get(collection_id).limits.sponsored_data_size >= (_properties.len() as u32)) &&
+ (<Collection<T>>::get(collection_id).sponsor_confirmed)
{
<Collection<T>>::get(collection_id).sponsor
} else {
T::AccountId::default()
}
}
- Some(Call::transfer(_new_owner, collection_id, _item_id, _value)) => {
+ Some(Call::transfer(_new_owner, collection_id, item_id, _value)) => {
- let _collection_limits = <Collection<T>>::get(collection_id).limits;
- let _collection_mode = <Collection<T>>::get(collection_id).mode;
+ let mut sponsor_transfer = false;
+ if <Collection<T>>::get(collection_id).sponsor_confirmed {
- // sponsor timeout
- let sponsor_transfer = match _collection_mode {
- CollectionMode::NFT => {
+ let collection_limits = <Collection<T>>::get(collection_id).limits;
+ 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 => {
+
+ // get correct limit
+ let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 {
+ collection_limits.sponsor_transfer_timeout
+ } else {
+ ChainLimit::get().nft_sponsor_transfer_timeout
+ };
+
+ 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;
+ }
+ }
+ if sponsored {
+ <NftTransferBasket<T>>::insert(collection_id, item_id, block_number);
+ }
- // get correct limit
- let limit: u32 = if _collection_limits.sponsor_transfer_timeout > 0 {
- _collection_limits.sponsor_transfer_timeout
- } else {
- 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
- }
- else {
- false
+ sponsored
}
- }
- CollectionMode::Fungible(_) => {
-
- // get correct limit
- let limit: u32 = if _collection_limits.sponsor_transfer_timeout > 0 {
- _collection_limits.sponsor_transfer_timeout
- } else {
- 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
+ CollectionMode::Fungible(_) => {
+
+ // get correct limit
+ let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 {
+ collection_limits.sponsor_transfer_timeout
+ } else {
+ ChainLimit::get().fungible_sponsor_transfer_timeout
+ };
+
+ let block_number = <system::Module<T>>::block_number() as T::BlockNumber;
+ 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 {
- false
+ if sponsored {
+ <FungibleTransferBasket<T>>::insert(collection_id, who, block_number);
}
+
+ sponsored
}
- else {
- basket.push(BasketItem { start_block: block_number, address: _new_owner.clone()});
- true
+ CollectionMode::ReFungible(_) => {
+
+ // get correct limit
+ let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 {
+ collection_limits.sponsor_transfer_timeout
+ } else {
+ ChainLimit::get().refungible_sponsor_transfer_timeout
+ };
+
+ 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);
+ }
+
+ sponsored
}
- }
- CollectionMode::ReFungible(_) => {
-
- // get correct limit
- let limit: u32 = if _collection_limits.sponsor_transfer_timeout > 0 {
- _collection_limits.sponsor_transfer_timeout
- } else {
- 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 {
- <ReFungibleTransferBasket<T>>::insert(collection_id, _item_id, block_number);
- true
- } else {
+ _ => {
false
- }
- }
- _ => {
- false
- },
- };
+ },
+ };
+ }
if !sponsor_transfer {
T::AccountId::default()
@@ -2555,11 +2455,6 @@
let mut who_pays_fee: T::AccountId = sponsor.clone();
if sponsor == T::AccountId::default() {
who_pays_fee = who.clone();
- }
-
- // Only mess with balances if fee is not zero.
- if fee.is_zero() {
- return Ok((fee, None));
}
match <T as transaction_payment::Trait>::Currency::withdraw(
runtime_types.jsondiffbeforeafterboth--- a/runtime_types.json
+++ b/runtime_types.json
@@ -42,18 +42,14 @@
"Fraction": "u128"
},
"FungibleItemType": {
- "Collection": "CollectionId",
- "Owner": "AccountId",
"Value": "u128"
},
"NftItemType": {
- "Collection": "CollectionId",
"Owner": "AccountId",
"ConstData": "Vec<u8>",
"VariableData": "Vec<u8>"
},
"ReFungibleItemType": {
- "Collection": "CollectionId",
"Owner": "Vec<Ownership<AccountId>>",
"ConstData": "Vec<u8>",
"VariableData": "Vec<u8>"
@@ -70,14 +66,10 @@
"OffchainSchema": "Vec<u8>",
"SchemaVersion": "SchemaVersion",
"Sponsor": "AccountId",
- "UnconfirmedSponsor": "AccountId",
+ "SponsorConfirmed": "bool",
"Limits": "CollectionLimits",
"VariableOnChainSchema": "Vec<u8>",
"ConstOnChainSchema": "Vec<u8>"
- },
- "ApprovePermissions": {
- "Approved": "AccountId",
- "Amount": "u128"
},
"RawData": "Vec<u8>",
"Address": "AccountId",
@@ -87,7 +79,9 @@
"const_data": "Vec<u8>",
"variable_data": "Vec<u8>"
},
- "CreateFungibleData": {},
+ "CreateFungibleData": {
+ "value": "u128"
+ },
"CreateReFungibleData": {
"const_data": "Vec<u8>",
"variable_data": "Vec<u8>"
@@ -107,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--- /dev/null
+++ b/tests/src/confirmSponsorship.test.ts
@@ -0,0 +1,350 @@
+//
+// This file is subject to the terms and conditions defined in
+// file 'LICENSE', which is part of this source code package.
+//
+
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import { default as usingApi, submitTransactionAsync } from "./substrate/substrate-api";
+import {
+ createCollectionExpectSuccess,
+ setCollectionSponsorExpectSuccess,
+ destroyCollectionExpectSuccess,
+ setCollectionSponsorExpectFailure,
+ confirmSponsorshipExpectSuccess,
+ confirmSponsorshipExpectFailure,
+ createItemExpectSuccess,
+ findUnusedAddress,
+ getGenericResult,
+ enableWhiteListExpectSuccess,
+ enablePublicMintingExpectSuccess,
+ addToWhiteListExpectSuccess,
+} from "./util/helpers";
+import { Keyring } from "@polkadot/api";
+import { IKeyringPair } from "@polkadot/types/types";
+import type { AccountId } from '@polkadot/types/interfaces';
+import { BigNumber } from 'bignumber.js';
+
+chai.use(chaiAsPromised);
+const expect = chai.expect;
+
+let alice: IKeyringPair;
+let bob: IKeyringPair;
+let charlie: IKeyringPair;
+
+describe.only('integration test: ext. confirmSponsorship():', () => {
+
+ before(async () => {
+ await usingApi(async (api) => {
+ const keyring = new Keyring({ type: 'sr25519' });
+ alice = keyring.addFromUri(`//Alice`);
+ bob = keyring.addFromUri(`//Bob`);
+ charlie = keyring.addFromUri(`//Charlie`);
+ });
+ });
+
+ it('Confirm collection sponsorship', async () => {
+ const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+ });
+ it('Add sponsor to a collection after the same sponsor was already added and confirmed', async () => {
+ const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ });
+ it('Add new sponsor to a collection after another sponsor was already added and confirmed', async () => {
+ const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+ await setCollectionSponsorExpectSuccess(collectionId, charlie.address);
+ });
+
+ it('NFT: Transfer 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');
+
+ 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(alice, collectionId, 'NFT', zeroBalance.address);
+
+ // 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);
+
+ const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ expect(result.success).to.be.true;
+ expect(BsponsorBalance.lt(AsponsorBalance)).to.be.true;
+ });
+
+ });
+
+ it('Fungible: Transfer fees are paid by the sponsor after confirmation', async () => {
+ 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(alice, collectionId, 'Fungible', zeroBalance.address);
+
+ // 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 () => {
+ 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(alice, collectionId, 'ReFungible', zeroBalance.address);
+
+ // 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('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(alice, collectionId);
+
+ // Enable public minting
+ await enablePublicMintingExpectSuccess(alice, collectionId);
+
+ // Create Item
+ 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);
+
+ // Add zeroBalance address to white list
+ await addToWhiteListExpectSuccess(alice, collectionId, zeroBalance.address);
+
+ // Mint token using unused address as signer
+ const tokenId = await createItemExpectSuccess(zeroBalance, collectionId, 'NFT', zeroBalance.address);
+
+ const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ expect(BsponsorBalance.lt(AsponsorBalance)).to.be.true;
+ });
+ });
+
+ it('NFT: Sponsoring is rate limited', async () => {
+ 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(alice, collectionId, 'NFT', alice.address);
+
+ // 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 () => {
+ 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(alice, collectionId, 'Fungible', zeroBalance.address);
+
+ // 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 () => {
+ 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(alice, collectionId, 'ReFungible', alice.address);
+
+ // 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;
+ });
+ });
+
+});
+
+describe.only('(!negative test!) integration test: ext. setCollectionSponsor():', () => {
+ before(async () => {
+ await usingApi(async (api) => {
+ const keyring = new Keyring({ type: 'sr25519' });
+ alice = keyring.addFromUri(`//Alice`);
+ bob = keyring.addFromUri(`//Bob`);
+ charlie = keyring.addFromUri(`//Charlie`);
+ });
+ });
+
+ it('(!negative test!) Confirm sponsorship for a collection that never existed', async () => {
+ // Find the collection that never existed
+ const collectionId = 0;
+ await usingApi(async (api) => {
+ const collectionId = parseInt((await api.query.nft.createdCollectionCount()).toString()) + 1;
+ });
+
+ await confirmSponsorshipExpectFailure(collectionId, '//Bob');
+ });
+
+ it('(!negative test!) Confirm sponsorship using a non-sponsor address', async () => {
+ const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+
+ await usingApi(async (api) => {
+ const transfer = api.tx.balances.transfer(charlie.address, 1e15);
+ await submitTransactionAsync(alice, transfer);
+ });
+
+ await confirmSponsorshipExpectFailure(collectionId, '//Charlie');
+ });
+
+ it('(!negative test!) Confirm sponsorship using owner address', async () => {
+ const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectFailure(collectionId, '//Alice');
+ });
+
+ it('(!negative test!) Confirm sponsorship without sponsor being set with setCollectionSponsor', async () => {
+ const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
+ await confirmSponsorshipExpectFailure(collectionId, '//Bob');
+ });
+
+ it('(!negative test!) Confirm sponsorship in a collection that was destroyed', async () => {
+ const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
+ await destroyCollectionExpectSuccess(collectionId);
+ await confirmSponsorshipExpectFailure(collectionId, '//Bob');
+ });
+});
tests/src/createItem.test.tsdiffbeforeafterboth--- a/tests/src/createItem.test.ts
+++ b/tests/src/createItem.test.ts
@@ -1,27 +1,34 @@
-import { assert } from 'chai';
-import { alicesPublicKey } from './accounts';
-import privateKey from './substrate/privateKey';
import { default as usingApi } from './substrate/substrate-api';
-import waitNewBlocks from './substrate/wait-new-blocks';
+import { Keyring } from "@polkadot/api";
+import { IKeyringPair } from "@polkadot/types/types";
import {
createCollectionExpectSuccess,
createItemExpectSuccess
} from './util/helpers';
+let alice: IKeyringPair;
+
describe('integration test: ext. createItem():', () => {
+ before(async () => {
+ await usingApi(async (api) => {
+ const keyring = new Keyring({ type: 'sr25519' });
+ alice = keyring.addFromUri(`//Alice`);
+ });
+ });
+
it('Create new item in NFT collection', async () => {
const createMode = 'NFT';
const newCollectionID = await createCollectionExpectSuccess('0', '0', '0', createMode);
- await createItemExpectSuccess(newCollectionID, createMode, '//Alice');
+ await createItemExpectSuccess(alice, newCollectionID, createMode);
});
it('Create new item in Fungible collection', async () => {
const createMode = 'Fungible';
const newCollectionID = await createCollectionExpectSuccess('0', '0', '0', createMode);
- await createItemExpectSuccess(newCollectionID, createMode, '//Alice');
+ await createItemExpectSuccess(alice, newCollectionID, createMode);
});
it('Create new item in ReFungible collection', async () => {
const createMode = 'ReFungible';
const newCollectionID = await createCollectionExpectSuccess('0', '0', '0', createMode);
- await createItemExpectSuccess(newCollectionID, createMode, '//Alice');
+ await createItemExpectSuccess(alice, newCollectionID, createMode);
});
});
tests/src/createMultipleItems.test.tsdiffbeforeafterboth--- a/tests/src/createMultipleItems.test.ts
+++ b/tests/src/createMultipleItems.test.ts
@@ -11,7 +11,7 @@
const idCollection = 12;
-describe('integration test: ext. createMultipleItems():', () => {
+describe.skip('integration test: ext. createMultipleItems():', () => {
it('Create two NFT tokens in active NFT collection', async () => {
await usingApi(async (api) => {
const AitemListIndex = await api.query.nft.itemListIndex(idCollection);
tests/src/destroyCollection.test.tsdiffbeforeafterboth--- a/tests/src/destroyCollection.test.ts
+++ b/tests/src/destroyCollection.test.ts
@@ -1,55 +1,12 @@
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { default as usingApi, submitTransactionAsync } from "./substrate/substrate-api";
-import { createCollectionExpectSuccess, createCollectionExpectFailure } from "./util/helpers";
+import { createCollectionExpectSuccess, createCollectionExpectFailure, destroyCollectionExpectSuccess, destroyCollectionExpectFailure } from "./util/helpers";
import type { AccountId, EventRecord } from '@polkadot/types/interfaces';
import privateKey from './substrate/privateKey';
-import { nullPublicKey } from './accounts';
chai.use(chaiAsPromised);
const expect = chai.expect;
-
-function getDestroyResult(events: EventRecord[]): boolean {
- let success: boolean = false;
- events.forEach(({ phase, event: { data, method, section } }) => {
- // console.log(` ${phase}: ${section}.${method}:: ${data}`);
- if (method == 'ExtrinsicSuccess') {
- success = true;
- }
- });
- return success;
-}
-
-async function destroyCollectionExpectSuccess(collectionId: number, senderSeed: string = '//Alice') {
- await usingApi(async (api) => {
- // Run the DestroyCollection transaction
- const alicePrivateKey = privateKey(senderSeed);
- const tx = api.tx.nft.destroyCollection(collectionId);
- const events = await submitTransactionAsync(alicePrivateKey, tx);
- const result = getDestroyResult(events);
-
- // Get the collection
- const collection: any = (await api.query.nft.collection(collectionId)).toJSON();
-
- // What to expect
- expect(result).to.be.true;
- expect(collection).to.be.not.null;
- expect(collection.Owner).to.be.equal(nullPublicKey);
- });
-}
-
-async function destroyCollectionExpectFailure(collectionId: number, senderSeed: string = '//Alice') {
- await usingApi(async (api) => {
- // Run the DestroyCollection transaction
- const alicePrivateKey = privateKey(senderSeed);
- const tx = api.tx.nft.destroyCollection(collectionId);
- const events = await submitTransactionAsync(alicePrivateKey, tx);
- const result = getDestroyResult(events);
-
- // What to expect
- expect(result).to.be.false;
- });
-}
describe('integration test: ext. destroyCollection():', () => {
it('NFT collection can be destroyed', async () => {
tests/src/setCollectionSponsor.test.tsdiffbeforeafterbothno changes
tests/src/util/helpers.tsdiffbeforeafterboth--- a/tests/src/util/helpers.ts
+++ b/tests/src/util/helpers.ts
@@ -9,10 +9,12 @@
import { ApiPromise, Keyring } from "@polkadot/api";
import { default as usingApi, submitTransactionAsync } from "../substrate/substrate-api";
import privateKey from '../substrate/privateKey';
-import { alicesPublicKey } from "../accounts";
+import { alicesPublicKey, nullPublicKey } from "../accounts";
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;
@@ -26,6 +28,12 @@
collectionId: number
};
+type CreateItemResult = {
+ success: boolean,
+ collectionId: number,
+ itemId: number
+};
+
export function getGenericResult(events: EventRecord[]): GenericResult {
let result: GenericResult = {
success: false
@@ -57,6 +65,27 @@
return result;
}
+function getCreateItemResult(events: EventRecord[]): CreateItemResult {
+ let success = false;
+ let collectionId: number = 0;
+ let itemId: number = 0;
+ events.forEach(({ phase, event: { data, method, section } }) => {
+ // console.log(` ${phase}: ${section}.${method}:: ${data}`);
+ if (method == 'ExtrinsicSuccess') {
+ success = true;
+ } else if ((section == 'nft') && (method == 'ItemCreated')) {
+ collectionId = parseInt(data[0].toString());
+ itemId = parseInt(data[1].toString());
+ }
+ });
+ let result: CreateItemResult = {
+ success,
+ collectionId,
+ itemId
+ }
+ return result;
+}
+
export async function createCollectionExpectSuccess(name: string, description: string, tokenPrefix: string, mode: string): Promise<number> {
let collectionId: number = 0;
await usingApi(async (api) => {
@@ -123,20 +152,214 @@
return unused;
}
-export async function createItemExpectSuccess(collectionId: number, createMode: string, senderSeed: string = '//Alice') {
+function getDestroyResult(events: EventRecord[]): boolean {
+ let success: boolean = false;
+ events.forEach(({ phase, event: { data, method, section } }) => {
+ // console.log(` ${phase}: ${section}.${method}:: ${data}`);
+ if (method == 'ExtrinsicSuccess') {
+ success = true;
+ }
+ });
+ return success;
+}
+
+export async function destroyCollectionExpectFailure(collectionId: number, senderSeed: string = '//Alice') {
+ await usingApi(async (api) => {
+ // Run the DestroyCollection transaction
+ const alicePrivateKey = privateKey(senderSeed);
+ const tx = api.tx.nft.destroyCollection(collectionId);
+ const events = await submitTransactionAsync(alicePrivateKey, tx);
+ const result = getDestroyResult(events);
+
+ // What to expect
+ expect(result).to.be.false;
+ });
+}
+
+export async function destroyCollectionExpectSuccess(collectionId: number, senderSeed: string = '//Alice') {
+ await usingApi(async (api) => {
+ // Run the DestroyCollection transaction
+ const alicePrivateKey = privateKey(senderSeed);
+ const tx = api.tx.nft.destroyCollection(collectionId);
+ const events = await submitTransactionAsync(alicePrivateKey, tx);
+ const result = getDestroyResult(events);
+
+ // Get the collection
+ const collection: any = (await api.query.nft.collection(collectionId)).toJSON();
+
+ // What to expect
+ expect(result).to.be.true;
+ expect(collection).to.be.not.null;
+ expect(collection.Owner).to.be.equal(nullPublicKey);
+ });
+}
+
+export async function setCollectionSponsorExpectSuccess(collectionId: number, sponsor: string) {
await usingApi(async (api) => {
- const AItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString());
+ // Run the transaction
+ const alicePrivateKey = privateKey('//Alice');
+ const tx = api.tx.nft.setCollectionSponsor(collectionId, sponsor);
+ const events = await submitTransactionAsync(alicePrivateKey, 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.Sponsor.toString()).to.be.equal(sponsor.toString());
+ expect(collection.SponsorConfirmed).to.be.false;
+ });
+}
+
+export async function setCollectionSponsorExpectFailure(collectionId: number, sponsor: string, senderSeed: string = '//Alice') {
+ await usingApi(async (api) => {
+
+ // Run the transaction
+ const alicePrivateKey = privateKey(senderSeed);
+ const tx = api.tx.nft.setCollectionSponsor(collectionId, sponsor);
+ const events = await submitTransactionAsync(alicePrivateKey, tx);
+ const result = getGenericResult(events);
+
+ // What to expect
+ expect(result.success).to.be.false;
+ });
+}
+
+export async function confirmSponsorshipExpectSuccess(collectionId: number, senderSeed: string = '//Alice') {
+ await usingApi(async (api) => {
+
+ // Run the transaction
const sender = privateKey(senderSeed);
- const tx = api.tx.nft.createItem(collectionId, sender.address, createMode);
+ const tx = api.tx.nft.confirmSponsorship(collectionId);
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.Sponsor).to.be.equal(sender.address);
+ expect(collection.SponsorConfirmed).to.be.true;
+ });
+}
+
+export async function confirmSponsorshipExpectFailure(collectionId: number, senderSeed: string = '//Alice') {
+ await usingApi(async (api) => {
+
+ // Run the transaction
+ const sender = privateKey(senderSeed);
+ const tx = api.tx.nft.confirmSponsorship(collectionId);
+ const events = await submitTransactionAsync(sender, tx);
+ const result = getGenericResult(events);
+
+ // What to expect
+ expect(result.success).to.be.false;
+ });
+}
+
+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(sender: IKeyringPair, collectionId: number, createMode: string, owner: string = '') {
+ 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);
+
+ if (owner === '') owner = sender.address;
+
+ 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(sender: IKeyringPair, collectionId: number) {
+ await usingApi(async (api) => {
+
+ // Run the transaction
+ 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');
+ });
+}
+
+export async function enablePublicMintingExpectSuccess(sender: IKeyringPair, collectionId: number) {
+ await usingApi(async (api) => {
+
+ // Run the transaction
+ const tx = api.tx.nft.setMintPermission(collectionId, true);
+ 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.MintMode).to.be.equal(true);
});
}
+
+export async function addToWhiteListExpectSuccess(sender: IKeyringPair, collectionId: number, address: string) {
+ await usingApi(async (api) => {
+
+ // Run the transaction
+ const tx = api.tx.nft.addToWhiteList(collectionId, address);
+ 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.MintMode).to.be.equal(true);
+ });
+}
+