From dc9fdbfccd6a56d8fe0a7006a7bf601f06451ee9 Mon Sep 17 00:00:00 2001 From: Greg Zaitsev Date: Thu, 17 Jun 2021 16:28:39 +0000 Subject: [PATCH] Merge pull request #148 from usetech-llc/feature/NFTPAR-373_chain_extensions Chain extensions --- --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,6 +3,9 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive && \ apt-get -y install --no-install-recommends libssl-dev pkg-config libclang-dev clang +RUN curl -L -o- https://github.com/WebAssembly/binaryen/releases/download/version_101/binaryen-version_101-x86_64-linux.tar.gz | \ + tar xz --strip-components=1 -C /usr + USER vscode RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash && \ @@ -11,4 +14,5 @@ nvm install v12.20.1 && \ rustup toolchain install nightly-2021-03-01 && \ rustup default nightly-2021-03-01 && \ - rustup target add wasm32-unknown-unknown \ No newline at end of file + rustup target add wasm32-unknown-unknown && \ + cargo install cargo-contract --- a/Cargo.lock +++ b/Cargo.lock @@ -5395,37 +5395,37 @@ [[package]] name = "pallet-scheduler" version = "3.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.3#c94e0cdfe5556680dca1996004751eeb114755d7" dependencies = [ "frame-benchmarking", "frame-support", "frame-system", "log", + "nft-data-structs", + "pallet-contracts", + "pallet-nft", + "pallet-nft-transaction-payment", "parity-scale-codec", + "serde", + "sp-core", "sp-io", "sp-runtime", "sp-std", + "substrate-test-utils", ] [[package]] name = "pallet-scheduler" version = "3.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.3#c94e0cdfe5556680dca1996004751eeb114755d7" dependencies = [ "frame-benchmarking", "frame-support", "frame-system", "log", - "nft-data-structs", - "pallet-contracts", - "pallet-nft", - "pallet-nft-transaction-payment", "parity-scale-codec", - "serde", - "sp-core", "sp-io", "sp-runtime", "sp-std", - "substrate-test-utils", ] [[package]] --- a/pallets/nft/src/lib.rs +++ b/pallets/nft/src/lib.rs @@ -566,10 +566,14 @@ let sender = ensure_signed(origin)?; let collection = Self::get_collection(collection_id)?; - Self::check_owner_or_admin_permissions(&collection, sender)?; - >::insert(collection_id, address, true); - + Self::toggle_white_list_internal( + &sender, + &collection, + &address, + true, + )?; + Ok(()) } @@ -591,9 +595,13 @@ let sender = ensure_signed(origin)?; let collection = Self::get_collection(collection_id)?; - Self::check_owner_or_admin_permissions(&collection, sender)?; - >::remove(collection_id, address); + Self::toggle_white_list_internal( + &sender, + &collection, + &address, + false, + )?; Ok(()) } @@ -983,43 +991,10 @@ pub fn approve(origin, spender: T::AccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResult { let sender = ensure_signed(origin)?; - let target_collection = Self::get_collection(collection_id)?; - - Self::token_exists(&target_collection, item_id)?; - - // Transfer permissions check - let bypasses_limits = target_collection.limits.owner_can_transfer && - Self::is_owner_or_admin_permissions( - &target_collection, - sender.clone(), - ); + let collection = Self::get_collection(collection_id)?; - let allowance_limit = if bypasses_limits { - None - } else if let Some(amount) = Self::owned_amount( - sender.clone(), - &target_collection, - item_id, - ) { - Some(amount) - } else { - fail!(Error::::NoPermission); - }; - - if target_collection.access == AccessMode::WhiteList { - Self::check_white_list(&target_collection, &sender)?; - Self::check_white_list(&target_collection, &spender)?; - } - - let allowance: u128 = amount - .checked_add(>::get(collection_id, (item_id, &sender, &spender))) - .ok_or(Error::::NumOverflow)?; - if let Some(limit) = allowance_limit { - ensure!(limit >= allowance, Error::::TokenValueTooLow); - } - >::insert(collection_id, (item_id, sender.clone(), spender.clone()), allowance); + Self::approve_internal(sender, spender, &collection, item_id, amount)?; - Self::deposit_event(RawEvent::Approved(target_collection.id, item_id, sender, spender, allowance)); Ok(()) } @@ -1047,46 +1022,10 @@ pub fn transfer_from(origin, from: T::AccountId, recipient: T::AccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResult { let sender = ensure_signed(origin)?; - let target_collection = Self::get_collection(collection_id)?; - - // Check approval - let approval: u128 = >::get(collection_id, (item_id, &from, &sender)); - - // Limits check - Self::is_correct_transfer(&target_collection, &recipient)?; + let collection = Self::get_collection(collection_id)?; - // Transfer permissions check - ensure!( - approval >= value || - ( - target_collection.limits.owner_can_transfer && - Self::is_owner_or_admin_permissions(&target_collection, sender.clone()) - ), - Error::::NoPermission - ); - - if target_collection.access == AccessMode::WhiteList { - Self::check_white_list(&target_collection, &sender)?; - Self::check_white_list(&target_collection, &recipient)?; - } - - // Reduce approval by transferred amount or remove if remaining approval drops to 0 - if approval.saturating_sub(value) > 0 { - >::insert(collection_id, (item_id, &from, &sender), approval - value); - } - else { - >::remove(collection_id, (item_id, &from, &sender)); - } - - match target_collection.mode - { - CollectionMode::NFT => Self::transfer_nft(&target_collection, item_id, from.clone(), recipient.clone())?, - CollectionMode::Fungible(_) => Self::transfer_fungible(&target_collection, value, &from, &recipient)?, - CollectionMode::ReFungible => Self::transfer_refungible(&target_collection, item_id, value, from.clone(), recipient.clone())?, - _ => () - }; + Self::transfer_from_internal(sender, from, recipient, &collection, item_id, value)?; - Self::deposit_event(RawEvent::Transfer(target_collection.id, item_id, from, recipient, value)); Ok(()) } @@ -1127,24 +1066,10 @@ ) -> DispatchResult { let sender = ensure_signed(origin)?; - let target_collection = Self::get_collection(collection_id)?; - Self::token_exists(&target_collection, item_id)?; + let collection = Self::get_collection(collection_id)?; - ensure!(ChainLimit::get().custom_data_limit >= data.len() as u32, Error::::TokenVariableDataLimitExceeded); - - // Modify permissions check - ensure!(Self::is_item_owner(sender.clone(), &target_collection, item_id) || - Self::is_owner_or_admin_permissions(&target_collection, sender.clone()), - Error::::NoPermission); + Self::set_variable_meta_data_internal(sender, &collection, item_id, data)?; - match target_collection.mode - { - CollectionMode::NFT => Self::set_nft_variable_data(&target_collection, item_id, data)?, - CollectionMode::ReFungible => Self::set_re_fungible_variable_data(&target_collection, item_id, data)?, - CollectionMode::Fungible(_) => fail!(Error::::CantStoreMetadataInFungibleTokens), - _ => fail!(Error::::UnexpectedCollectionType) - }; - Ok(()) } @@ -1516,6 +1441,164 @@ Ok(()) } + pub fn approve_internal( + sender: T::AccountId, + spender: T::AccountId, + collection: &CollectionHandle, + item_id: TokenId, + amount: u128, + ) -> DispatchResult { + Self::token_exists(&collection, item_id)?; + + // Transfer permissions check + let bypasses_limits = collection.limits.owner_can_transfer && + Self::is_owner_or_admin_permissions( + &collection, + sender.clone(), + ); + + let allowance_limit = if bypasses_limits { + None + } else if let Some(amount) = Self::owned_amount( + sender.clone(), + &collection, + item_id, + ) { + Some(amount) + } else { + fail!(Error::::NoPermission); + }; + + if collection.access == AccessMode::WhiteList { + Self::check_white_list(&collection, &sender)?; + Self::check_white_list(&collection, &spender)?; + } + + let allowance: u128 = amount + .checked_add(>::get(collection.id, (item_id, &sender, &spender))) + .ok_or(Error::::NumOverflow)?; + if let Some(limit) = allowance_limit { + ensure!(limit >= allowance, Error::::TokenValueTooLow); + } + >::insert(collection.id, (item_id, &sender, &spender), allowance); + + Self::deposit_event(RawEvent::Approved(collection.id, item_id, sender, spender, allowance)); + Ok(()) + } + + pub fn transfer_from_internal( + sender: T::AccountId, + from: T::AccountId, + recipient: T::AccountId, + collection: &CollectionHandle, + item_id: TokenId, + amount: u128, + ) -> DispatchResult { + // Check approval + let approval: u128 = >::get(collection.id, (item_id, &from, &sender)); + + // Limits check + Self::is_correct_transfer(&collection, &recipient)?; + + // Transfer permissions check + ensure!( + approval >= amount || + ( + collection.limits.owner_can_transfer && + Self::is_owner_or_admin_permissions(&collection, sender.clone()) + ), + Error::::NoPermission + ); + + if collection.access == AccessMode::WhiteList { + Self::check_white_list(&collection, &sender)?; + Self::check_white_list(&collection, &recipient)?; + } + + // Reduce approval by transferred amount or remove if remaining approval drops to 0 + let allowance = approval.saturating_sub(amount); + if allowance > 0 { + >::insert(collection.id, (item_id, &from, &sender), allowance); + } else { + >::remove(collection.id, (item_id, &from, &sender)); + } + + match collection.mode { + CollectionMode::NFT => { + Self::transfer_nft(&collection, item_id, from.clone(), recipient.clone())? + } + CollectionMode::Fungible(_) => { + Self::transfer_fungible(&collection, amount, &from, &recipient)? + } + CollectionMode::ReFungible => { + Self::transfer_refungible(&collection, item_id, amount, from.clone(), recipient.clone())? + } + _ => () + }; + + Ok(()) + } + + pub fn set_variable_meta_data_internal( + sender: T::AccountId, + collection: &CollectionHandle, + item_id: TokenId, + data: Vec, + ) -> DispatchResult { + Self::token_exists(&collection, item_id)?; + + ensure!(ChainLimit::get().custom_data_limit >= data.len() as u32, Error::::TokenVariableDataLimitExceeded); + + // Modify permissions check + ensure!(Self::is_item_owner(sender.clone(), &collection, item_id) || + Self::is_owner_or_admin_permissions(&collection, sender.clone()), + Error::::NoPermission); + + match collection.mode + { + CollectionMode::NFT => Self::set_nft_variable_data(&collection, item_id, data)?, + CollectionMode::ReFungible => Self::set_re_fungible_variable_data(&collection, item_id, data)?, + CollectionMode::Fungible(_) => fail!(Error::::CantStoreMetadataInFungibleTokens), + _ => fail!(Error::::UnexpectedCollectionType) + }; + + Ok(()) + } + + pub fn create_multiple_items_internal( + sender: T::AccountId, + collection: &CollectionHandle, + owner: T::AccountId, + items_data: Vec, + ) -> DispatchResult { + Self::can_create_items_in_collection(&collection, &sender, &owner, items_data.len() as u32)?; + + for data in &items_data { + Self::validate_create_item_args(&collection, data)?; + } + for data in &items_data { + Self::create_item_no_validation(&collection, owner.clone(), data.clone())?; + } + + Ok(()) + } + + pub fn toggle_white_list_internal( + sender: &T::AccountId, + collection: &CollectionHandle, + address: &T::AccountId, + whitelisted: bool, + ) -> DispatchResult { + Self::check_owner_or_admin_permissions(&collection, sender.clone())?; + + if whitelisted { + >::insert(collection.id, address, true); + } else { + >::remove(collection.id, address); + } + + Ok(()) + } fn is_correct_transfer(collection: &CollectionHandle, recipient: &T::AccountId) -> DispatchResult { let collection_id = collection.id; --- a/runtime/src/chain_extension.rs +++ b/runtime/src/chain_extension.rs @@ -19,6 +19,8 @@ pub use pallet_nft::*; use nft_data_structs::*; +use crate::Vec; + /// Create item parameters #[derive(Debug, PartialEq, Encode, Decode)] pub struct NFTExtCreateItem { @@ -36,13 +38,54 @@ pub amount: u128, } +#[derive(Debug, PartialEq, Encode, Decode)] +pub struct NFTExtCreateMultipleItems { + pub owner: ::AccountId, + pub collection_id: u32, + pub data: Vec, +} + +#[derive(Debug, PartialEq, Encode, Decode)] +pub struct NFTExtApprove { + pub spender: ::AccountId, + pub collection_id: u32, + pub item_id: u32, + pub amount: u128, +} + +#[derive(Debug, PartialEq, Encode, Decode)] +pub struct NFTExtTransferFrom { + pub owner: ::AccountId, + pub recipient: ::AccountId, + pub collection_id: u32, + pub item_id: u32, + pub amount: u128, +} + +#[derive(Debug, PartialEq, Encode, Decode)] +pub struct NFTExtSetVariableMetaData { + pub collection_id: u32, + pub item_id: u32, + pub data: Vec, +} + +#[derive(Debug, PartialEq, Encode, Decode)] +pub struct NFTExtToggleWhiteList { + pub collection_id: u32, + pub address: ::AccountId, + pub whitelisted: bool, +} + /// The chain Extension of NFT pallet pub struct NFTExtension; +pub type NftWeightInfoOf = ::WeightInfo; + impl ChainExtension for NFTExtension { fn call(func_id: u32, env: Environment) -> Result where E: Ext, + C: pallet_nft::Config, ::AccountId: UncheckedFrom<::Hash> + AsRef<[u8]>, { // The memory of the vm stores buf in scale-codec @@ -50,37 +93,129 @@ 0 => { let mut env = env.buf_in_buf_out(); let input: NFTExtTransfer = env.read_as()?; + env.charge_weight(NftWeightInfoOf::::transfer())?; let collection = pallet_nft::Module::::get_collection(input.collection_id)?; - match pallet_nft::Module::::transfer_internal( - env.ext().caller().clone(), + pallet_nft::Module::::transfer_internal( + env.ext().address().clone(), input.recipient, &collection, input.token_id, input.amount, - ) { - Ok(_) => Ok(RetVal::Converging(func_id)), - _ => Err(DispatchError::Other("Transfer error")) - } + )?; + + Ok(RetVal::Converging(0)) }, 1 => { // Create Item let mut env = env.buf_in_buf_out(); let input: NFTExtCreateItem = env.read_as()?; + env.charge_weight(NftWeightInfoOf::::create_item(input.data.len()))?; - match pallet_nft::Module::::create_item_internal( + pallet_nft::Module::::create_item_internal( env.ext().address().clone(), input.collection_id, input.owner, input.data, - ) { - Ok(_) => Ok(RetVal::Converging(func_id)), - _ => Err(DispatchError::Other("CreateItem error")) - } + )?; + + Ok(RetVal::Converging(0)) }, + 2 => { + // Create multiple items + let mut env = env.buf_in_buf_out(); + let input: NFTExtCreateMultipleItems = env.read_as()?; + env.charge_weight(NftWeightInfoOf::::create_item( + input.data.iter() + .map(|i| i.len()) + .sum() + ))?; + + let collection = pallet_nft::Module::::get_collection(input.collection_id)?; + + pallet_nft::Module::::create_multiple_items_internal( + env.ext().address().clone(), + &collection, + input.owner, + input.data, + )?; + + Ok(RetVal::Converging(0)) + }, + 3 => { + // Approve + let mut env = env.buf_in_buf_out(); + let input: NFTExtApprove = env.read_as()?; + env.charge_weight(NftWeightInfoOf::::approve())?; + + let collection = pallet_nft::Module::::get_collection(input.collection_id)?; + + pallet_nft::Module::::approve_internal( + env.ext().address().clone(), + input.spender, + &collection, + input.item_id, + input.amount, + )?; + + Ok(RetVal::Converging(0)) + }, + 4 => { + // Transfer from + let mut env = env.buf_in_buf_out(); + let input: NFTExtTransferFrom = env.read_as()?; + env.charge_weight(NftWeightInfoOf::::transfer_from())?; + + let collection = pallet_nft::Module::::get_collection(input.collection_id)?; + + pallet_nft::Module::::transfer_from_internal( + env.ext().address().clone(), + input.owner, + input.recipient, + &collection, + input.item_id, + input.amount + )?; + + Ok(RetVal::Converging(0)) + }, + 5 => { + // Set variable metadata + let mut env = env.buf_in_buf_out(); + let input: NFTExtSetVariableMetaData = env.read_as()?; + env.charge_weight(NftWeightInfoOf::::set_variable_meta_data())?; + + let collection = pallet_nft::Module::::get_collection(input.collection_id)?; + + pallet_nft::Module::::set_variable_meta_data_internal( + env.ext().address().clone(), + &collection, + input.item_id, + input.data, + )?; + + Ok(RetVal::Converging(0)) + }, + 6 => { + // Toggle whitelist + let mut env = env.buf_in_buf_out(); + let input: NFTExtToggleWhiteList = env.read_as()?; + env.charge_weight(NftWeightInfoOf::::add_to_white_list())?; + + let collection = pallet_nft::Module::::get_collection(input.collection_id)?; + + pallet_nft::Module::::toggle_white_list_internal( + &env.ext().address().clone(), + &collection, + &input.address, + input.whitelisted, + )?; + + Ok(RetVal::Converging(0)) + } _ => { - panic!("Passed unknown func_id to test chain extension: {}", func_id); + Err(DispatchError::Other("unknown chain_extension func_id")) } } } --- a/smart_contracs/transfer/Cargo.toml +++ b/smart_contracs/transfer/Cargo.toml @@ -4,15 +4,17 @@ authors = ["[Greg Zaitsev] <[your_email]>"] edition = "2018" +[workspace] + [dependencies] -ink_primitives = { git = "https://github.com/usetech-llc/ink", branch = "unique", default-features = false } -ink_metadata = { git = "https://github.com/usetech-llc/ink", branch = "unique", default-features = false, features = ["derive"], optional = true } -ink_env = { git = "https://github.com/usetech-llc/ink", branch = "unique", default-features = false } -ink_storage = { git = "https://github.com/usetech-llc/ink", branch = "unique", default-features = false } -ink_lang = { git = "https://github.com/usetech-llc/ink", branch = "unique", default-features = false } +ink_primitives = { default-features = false } +ink_metadata = { default-features = false, features = ["derive"], optional = true } +ink_env = { default-features = false } +ink_storage = { default-features = false } +ink_lang = { default-features = false } -scale = { package = "parity-scale-codec", version = "1.3", default-features = false, features = ["derive"] } -scale-info = { version = "0.4.1", default-features = false, features = ["derive"], optional = true } +scale = { package = "parity-scale-codec", version = "2.1.1", default-features = false, features = ["derive"] } +scale-info = { version = "0.6.0", default-features = false, features = ["derive"] } [lib] name = "nft_transfer" @@ -28,6 +30,7 @@ "ink_metadata/std", "ink_env/std", "ink_storage/std", + "ink_lang/std", "ink_primitives/std", "scale/std", "scale-info/std", --- a/smart_contracs/transfer/lib.rs +++ b/smart_contracs/transfer/lib.rs @@ -1,4 +1,6 @@ #![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; +use alloc::vec::Vec; use ink_lang as ink; use ink_env::{Environment, DefaultEnvironment}; @@ -36,6 +38,24 @@ } } +#[derive(scale::Encode, scale::Decode, scale_info::TypeInfo)] +pub enum CreateItemData { + Nft { + const_data: Vec, + variable_data: Vec, + }, + Fungible { + value: u128, + }, + ReFungible { + const_data: Vec, + variable_data: Vec, + pieces: u128, + }, +} + +type DefaultAccountId = ::AccountId; + #[ink::chain_extension] pub trait NftChainExtension { type ErrorCode = NftErrorCode; @@ -43,11 +63,26 @@ /// Transfer one NFT token from sender /// #[ink(extension = 0, returns_result = false)] - fn transfer(recipient: ::AccountId, collection_id: u32, token_id: u32, amount: u128); + fn transfer(recipient: DefaultAccountId, collection_id: u32, token_id: u32, amount: u128); + #[ink(extension = 1, returns_result = false)] + fn create_item(owner: DefaultAccountId, collection_id: u32, data: CreateItemData); + #[ink(extension = 2, returns_result = false)] + fn create_multiple_items(owner: DefaultAccountId, collection_id: u32, data: Vec); + #[ink(extension = 3, returns_result = false)] + fn approve(spender: DefaultAccountId, collection_id: u32, item_id: u32, amount: u128); + #[ink(extension = 4, returns_result = false)] + fn transfer_from(owner: DefaultAccountId, recipient: DefaultAccountId, collection_id: u32, item_id: u32, amount: u128); + #[ink(extension = 5, returns_result = false)] + fn set_variable_meta_data(collection_id: u32, item_id: u32, data: Vec); + #[ink(extension = 6, returns_result = false)] + fn toggle_white_list(collection_id: u32, address: DefaultAccountId, whitelisted: bool); } -#[ink::contract(env = crate::NftEnvironment)] +#[ink::contract(env = crate::NftEnvironment, dynamic_storage_allocator = true)] mod nft_transfer { + use alloc::vec::Vec; + // use ink_storage::Vec; + use crate::CreateItemData; #[ink(storage)] pub struct NftTransfer { @@ -69,6 +104,42 @@ .extension() .transfer(recipient, collection_id, token_id, amount); } + #[ink(message)] + pub fn create_item(&mut self, recipient: AccountId, collection_id: u32, data: CreateItemData) { + let _ = self.env() + .extension() + .create_item(recipient, collection_id, data); + } + #[ink(message)] + pub fn create_multiple_items(&mut self, owner: AccountId, collection_id: u32, data: Vec) { + let _ = self.env() + .extension() + .create_multiple_items(owner, collection_id, data); + } + #[ink(message)] + pub fn approve(&mut self, spender: AccountId, collection_id: u32, item_id: u32, amount: u128) { + let _ = self.env() + .extension() + .approve(spender, collection_id, item_id, amount); + } + #[ink(message)] + pub fn transfer_from(&mut self, owner: AccountId, recipient: AccountId, collection_id: u32, item_id: u32, amount: u128) { + let _ = self.env() + .extension() + .transfer_from(owner, recipient, collection_id, item_id, amount); + } + #[ink(message)] + pub fn set_variable_meta_data(&mut self, collection_id: u32, item_id: u32, data: Vec) { + let _ = self.env() + .extension() + .set_variable_meta_data(collection_id, item_id, data); + } + #[ink(message)] + pub fn toggle_white_list(&mut self, collection_id: u32, address: AccountId, whitelisted: bool) { + let _ = self.env() + .extension() + .toggle_white_list(collection_id, address, whitelisted); + } } --- a/tests/src/contracts.test.ts +++ b/tests/src/contracts.test.ts @@ -16,9 +16,15 @@ } from "./util/contracthelpers"; import { + addToWhiteListExpectSuccess, + approveExpectSuccess, createCollectionExpectSuccess, createItemExpectSuccess, - getGenericResult + enablePublicMintingExpectSuccess, + enableWhiteListExpectSuccess, + getGenericResult, + isWhitelisted, + transferFromExpectSuccess } from "./util/helpers"; @@ -26,7 +32,7 @@ const expect = chai.expect; const value = 0; -const gasLimit = 3000n * 1000000n; +const gasLimit = 9000n * 1000000n; const marketContractAddress = '5CYN9j3YvRkqxewoxeSvRbhAym4465C57uMmX5j4yz99L5H6'; describe('Contracts', () => { @@ -53,8 +59,10 @@ expect(newContractInstance.address.toString()).to.equal(marketContractAddress); }); }); +}); - it('Can transfer NFT using smart contract.', async () => { +describe.only('Chain extensions', () => { + it('Transfer CE', async () => { await usingApi(async api => { const alice = privateKey("//Alice"); const bob = privateKey("//Bob"); @@ -63,6 +71,9 @@ const collectionId = await createCollectionExpectSuccess(); const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT'); const [contract, deployer] = await deployTransferContract(api); + const changeAdminTx = api.tx.nft.addCollectionAdmin(collectionId, contract.address); + await submitTransactionAsync(alice, changeAdminTx); + const tokenBefore: any = (await api.query.nft.nftItemList(collectionId, tokenId) as any).unwrap(); // Transfer @@ -77,4 +88,165 @@ expect(tokenAfter.Owner.toString()).to.be.equal(bob.address); }); }); + + it('Mint CE', async () => { + await usingApi(async api => { + const alice = privateKey('//Alice'); + const bob = privateKey('//Bob'); + + const collectionId = await createCollectionExpectSuccess(); + const [contract, deployer] = await deployTransferContract(api); + await enablePublicMintingExpectSuccess(alice, collectionId); + await enableWhiteListExpectSuccess(alice, collectionId); + await addToWhiteListExpectSuccess(alice, collectionId, contract.address); + await addToWhiteListExpectSuccess(alice, collectionId, bob.address); + + const transferTx = contract.tx.createItem(value, gasLimit, bob.address, collectionId, { Nft: {const_data: '0x010203', variable_data: '0x020304' }}); + const events = await submitTransactionAsync(alice, transferTx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + const tokensAfter: any = (await api.query.nft.nftItemList.entries(collectionId) as any).map((kv: any) => kv[1].toJSON()); + expect(tokensAfter).to.be.deep.equal([ + { + Owner: bob.address, + ConstData: '0x010203', + VariableData: '0x020304', + }, + ]); + }); + }); + + it('Bulk mint CE', async () => { + await usingApi(async api => { + const alice = privateKey('//Alice'); + const bob = privateKey('//Bob'); + + const collectionId = await createCollectionExpectSuccess(); + const [contract, deployer] = await deployTransferContract(api); + await enablePublicMintingExpectSuccess(alice, collectionId); + await enableWhiteListExpectSuccess(alice, collectionId); + await addToWhiteListExpectSuccess(alice, collectionId, contract.address); + await addToWhiteListExpectSuccess(alice, collectionId, bob.address); + + const transferTx = contract.tx.createMultipleItems(value, gasLimit, bob.address, collectionId, [ + { Nft: { const_data: '0x010203', variable_data: '0x020304' } }, + { Nft: { const_data: '0x010204', variable_data: '0x020305' } }, + { Nft: { const_data: '0x010205', variable_data: '0x020306' } } + ]); + const events = await submitTransactionAsync(alice, transferTx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + const tokensAfter: any = (await api.query.nft.nftItemList.entries(collectionId) as any) + .map((kv: any) => kv[1].toJSON()) + .sort((a: any, b: any) => a.ConstData.localeCompare(b.ConstData)); + expect(tokensAfter).to.be.deep.equal([ + { + Owner: bob.address, + ConstData: '0x010203', + VariableData: '0x020304', + }, + { + Owner: bob.address, + ConstData: '0x010204', + VariableData: '0x020305', + }, + { + Owner: bob.address, + ConstData: '0x010205', + VariableData: '0x020306', + }, + ]); + }); + }); + + it('Approve CE', async () => { + await usingApi(async api => { + const alice = privateKey('//Alice'); + const bob = privateKey('//Bob'); + const charlie = privateKey('//Charlie'); + + const collectionId = await createCollectionExpectSuccess(); + const [contract, deployer] = await deployTransferContract(api); + const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', contract.address); + + const transferTx = contract.tx.approve(value, gasLimit, bob.address, collectionId, tokenId, 1); + const events = await submitTransactionAsync(alice, transferTx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + await transferFromExpectSuccess(collectionId, tokenId, bob, contract.address.toString(), charlie, 1, 'NFT'); + }); + }); + + it('TransferFrom CE', async () => { + await usingApi(async api => { + const alice = privateKey('//Alice'); + const bob = privateKey('//Bob'); + const charlie = privateKey('//Charlie'); + + const collectionId = await createCollectionExpectSuccess(); + const [contract, deployer] = await deployTransferContract(api); + const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', bob.address); + await approveExpectSuccess(collectionId, tokenId, bob, contract.address.toString(), 1); + + const transferTx = contract.tx.transferFrom(value, gasLimit, bob.address, charlie.address, collectionId, tokenId, 1); + const events = await submitTransactionAsync(alice, transferTx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + const token: any = (await api.query.nft.nftItemList(collectionId, tokenId) as any).unwrap() + expect(token.Owner.toString()).to.be.equal(charlie.address); + }); + }); + + it('SetVariableMetaData CE', async () => { + await usingApi(async api => { + const alice = privateKey('//Alice'); + + const collectionId = await createCollectionExpectSuccess(); + const [contract, deployer] = await deployTransferContract(api); + const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', contract.address); + + const transferTx = contract.tx.setVariableMetaData(value, gasLimit, collectionId, tokenId, '0x121314'); + const events = await submitTransactionAsync(alice, transferTx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + const token: any = (await api.query.nft.nftItemList(collectionId, tokenId) as any).unwrap() + expect(token.VariableData.toString()).to.be.equal('0x121314'); + }); + }); + + it('ToggleWhiteList CE', async () => { + await usingApi(async api => { + const alice = privateKey('//Alice'); + const bob = privateKey('//Bob'); + + const collectionId = await createCollectionExpectSuccess(); + const [contract, deployer] = await deployTransferContract(api); + const changeAdminTx = api.tx.nft.addCollectionAdmin(collectionId, contract.address); + await submitTransactionAsync(alice, changeAdminTx); + + expect(await isWhitelisted(collectionId, bob.address)).to.be.false; + + { + const transferTx = contract.tx.toggleWhiteList(value, gasLimit, collectionId, bob.address, true); + const events = await submitTransactionAsync(alice, transferTx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + expect(await isWhitelisted(collectionId, bob.address)).to.be.true; + } + { + const transferTx = contract.tx.toggleWhiteList(value, gasLimit, collectionId, bob.address, false); + const events = await submitTransactionAsync(alice, transferTx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + + expect(await isWhitelisted(collectionId, bob.address)).to.be.false; + } + }); + }); }); --- a/tests/src/transfer_contract/metadata.json +++ b/tests/src/transfer_contract/metadata.json @@ -1,9 +1,9 @@ { "metadataVersion": "0.1.0", "source": { - "hash": "0xfbbc0729acefed9d99f93f6cbb751d12e317958fd0fe183f5781329b37f1bf6e", - "language": "ink! 3.0.0-rc2", - "compiler": "rustc 1.51.0-nightly" + "hash": "0xc6c3f47adeafe86d1674ed72c7179605787842f2f05a2d7da0dbabf3c4fa1aa8", + "language": "ink! 3.0.0-rc3", + "compiler": "rustc 1.52.0-nightly" }, "contract": { "name": "nft_transfer", @@ -24,7 +24,7 @@ "name": [ "default" ], - "selector": "0x6a3712e2" + "selector": "0xed4b9d1b" } ], "docs": [], @@ -78,7 +78,268 @@ ], "payable": false, "returnType": null, - "selector": "0xfae3a09d" + "selector": "0x84a15da1" + }, + { + "args": [ + { + "name": "recipient", + "type": { + "displayName": [ + "AccountId" + ], + "type": 1 + } + }, + { + "name": "collection_id", + "type": { + "displayName": [ + "u32" + ], + "type": 4 + } + }, + { + "name": "data", + "type": { + "displayName": [ + "CreateItemData" + ], + "type": 6 + } + } + ], + "docs": [], + "mutates": true, + "name": [ + "create_item" + ], + "payable": false, + "returnType": null, + "selector": "0xd7c3f083" + }, + { + "args": [ + { + "name": "owner", + "type": { + "displayName": [ + "AccountId" + ], + "type": 1 + } + }, + { + "name": "collection_id", + "type": { + "displayName": [ + "u32" + ], + "type": 4 + } + }, + { + "name": "data", + "type": { + "displayName": [ + "Vec" + ], + "type": 8 + } + } + ], + "docs": [], + "mutates": true, + "name": [ + "create_multiple_items" + ], + "payable": false, + "returnType": null, + "selector": "0x15f9a1eb" + }, + { + "args": [ + { + "name": "spender", + "type": { + "displayName": [ + "AccountId" + ], + "type": 1 + } + }, + { + "name": "collection_id", + "type": { + "displayName": [ + "u32" + ], + "type": 4 + } + }, + { + "name": "item_id", + "type": { + "displayName": [ + "u32" + ], + "type": 4 + } + }, + { + "name": "amount", + "type": { + "displayName": [ + "u128" + ], + "type": 5 + } + } + ], + "docs": [], + "mutates": true, + "name": [ + "approve" + ], + "payable": false, + "returnType": null, + "selector": "0x681266a0" + }, + { + "args": [ + { + "name": "owner", + "type": { + "displayName": [ + "AccountId" + ], + "type": 1 + } + }, + { + "name": "recipient", + "type": { + "displayName": [ + "AccountId" + ], + "type": 1 + } + }, + { + "name": "collection_id", + "type": { + "displayName": [ + "u32" + ], + "type": 4 + } + }, + { + "name": "item_id", + "type": { + "displayName": [ + "u32" + ], + "type": 4 + } + }, + { + "name": "amount", + "type": { + "displayName": [ + "u128" + ], + "type": 5 + } + } + ], + "docs": [], + "mutates": true, + "name": [ + "transfer_from" + ], + "payable": false, + "returnType": null, + "selector": "0x0b396f18" + }, + { + "args": [ + { + "name": "collection_id", + "type": { + "displayName": [ + "u32" + ], + "type": 4 + } + }, + { + "name": "item_id", + "type": { + "displayName": [ + "u32" + ], + "type": 4 + } + }, + { + "name": "data", + "type": { + "displayName": [ + "Vec" + ], + "type": 7 + } + } + ], + "docs": [], + "mutates": true, + "name": [ + "set_variable_meta_data" + ], + "payable": false, + "returnType": null, + "selector": "0xb0b26da2" + }, + { + "args": [ + { + "name": "collection_id", + "type": { + "displayName": [ + "u32" + ], + "type": 4 + } + }, + { + "name": "address", + "type": { + "displayName": [ + "AccountId" + ], + "type": 1 + } + }, + { + "name": "whitelisted", + "type": { + "displayName": [ + "bool" + ], + "type": 9 + } + } + ], + "docs": [], + "mutates": true, + "name": [ + "toggle_white_list" + ], + "payable": false, + "returnType": null, + "selector": "0x98574dac" } ] }, @@ -93,7 +354,8 @@ "composite": { "fields": [ { - "type": 2 + "type": 2, + "typeName": "[u8; 32]" } ] } @@ -126,6 +388,82 @@ "def": { "primitive": "u128" } + }, + { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "name": "const_data", + "type": 7, + "typeName": "Vec" + }, + { + "name": "variable_data", + "type": 7, + "typeName": "Vec" + } + ], + "name": "Nft" + }, + { + "fields": [ + { + "name": "value", + "type": 5, + "typeName": "u128" + } + ], + "name": "Fungible" + }, + { + "fields": [ + { + "name": "const_data", + "type": 7, + "typeName": "Vec" + }, + { + "name": "variable_data", + "type": 7, + "typeName": "Vec" + }, + { + "name": "pieces", + "type": 5, + "typeName": "u128" + } + ], + "name": "ReFungible" + } + ] + } + }, + "path": [ + "nft_transfer", + "CreateItemData" + ] + }, + { + "def": { + "sequence": { + "type": 3 + } + } + }, + { + "def": { + "sequence": { + "type": 6 + } + } + }, + { + "def": { + "primitive": "bool" + } } ] } \ No newline at end of file --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -636,17 +636,19 @@ export async function approveExpectSuccess(collectionId: number, - tokenId: number, owner: IKeyringPair, approved: IKeyringPair, amount: number | bigint = 1) { + tokenId: number, owner: IKeyringPair, approved: IKeyringPair | string, amount: number | bigint = 1) { + if (typeof approved !== 'string') + approved = approved.address; await usingApi(async (api: ApiPromise) => { const allowanceBefore = - await api.query.nft.allowances(collectionId, [tokenId, owner.address, approved.address]) as unknown as BN; - const approveNftTx = await api.tx.nft.approve(approved.address, collectionId, tokenId, amount); + await api.query.nft.allowances(collectionId, [tokenId, owner.address, approved] as any) as unknown as BN; + const approveNftTx = await api.tx.nft.approve(approved, collectionId, tokenId, amount); const events = await submitTransactionAsync(owner, approveNftTx); const result = getCreateItemResult(events); // tslint:disable-next-line:no-unused-expression expect(result.success).to.be.true; const allowanceAfter = - await api.query.nft.allowances(collectionId, [tokenId, owner.address, approved.address]) as unknown as BN; + await api.query.nft.allowances(collectionId, [tokenId, owner.address, approved] as any) as unknown as BN; expect(allowanceAfter.sub(allowanceBefore).toString()).to.be.equal(amount.toString()); }); } @@ -655,17 +657,19 @@ transferFromExpectSuccess(collectionId: number, tokenId: number, accountApproved: IKeyringPair, - accountFrom: IKeyringPair, + accountFrom: IKeyringPair | string, accountTo: IKeyringPair, value: number | bigint = 1, type: string = 'NFT') { + if (typeof accountFrom !== 'string') + accountFrom = accountFrom.address; await usingApi(async (api: ApiPromise) => { let balanceBefore = new BN(0); if (type === 'Fungible') { balanceBefore = await api.query.nft.balance(collectionId, accountTo.address) as unknown as BN; } const transferFromTx = await api.tx.nft.transferFrom( - accountFrom.address, accountTo.address, collectionId, tokenId, value); + accountFrom, accountTo.address, collectionId, tokenId, value); const events = await submitTransactionAsync(accountApproved, transferFromTx); const result = getCreateItemResult(events); // tslint:disable-next-line:no-unused-expression @@ -849,17 +853,13 @@ } export async function createItemExpectSuccess( - sender: IKeyringPair, collectionId: number, createMode: string, owner: string = '') { + sender: IKeyringPair, collectionId: number, createMode: string, owner: string | AccountId = sender.address) { let newItemId: number = 0; await usingApi(async (api) => { const AItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString(), 10); 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') { const createData = {fungible: {value: 10}}; @@ -887,7 +887,7 @@ } expect(collectionId).to.be.equal(result.collectionId); expect(BItemCount).to.be.equal(result.itemId); - expect(owner).to.be.equal(result.recipient); + expect(owner.toString()).to.be.equal(result.recipient); newItemId = result.itemId; }); return newItemId; @@ -974,7 +974,7 @@ return whitelisted; } -export async function addToWhiteListExpectSuccess(sender: IKeyringPair, collectionId: number, address: string) { +export async function addToWhiteListExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | AccountId) { await usingApi(async (api) => { const whiteListedBefore = (await api.query.nft.whiteList(collectionId, address)).toJSON(); -- gitstuff