From a85344747647ff0ff119a7b47f2631f2cdddf739 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Fri, 04 Jun 2021 17:36:30 +0000 Subject: [PATCH] Merge remote-tracking branch 'origin/feature/NFTPAR-373_chain_extensions' into feature/NFTPAR-366_upstream_updates --- --- 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 && \ @@ -14,4 +17,4 @@ rustup default nightly-2021-05-30 && \ rustup target add wasm32-unknown-unknown && \ rustup component add rustfmt clippy && \ - cargo install cargo-expand cargo-edit + cargo install cargo-expand cargo-edit cargo-contract --- a/runtime/src/chain_extension.rs +++ b/runtime/src/chain_extension.rs @@ -97,7 +97,7 @@ let collection = pallet_nft::Module::::get_collection(input.collection_id)?; pallet_nft::Module::::transfer_internal( - &C::CrossAccountId::from_sub(env.ext().caller().clone()), + &C::CrossAccountId::from_sub(env.ext().address().clone()), &C::CrossAccountId::from_sub(input.recipient), &collection, input.token_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,10 +16,16 @@ } from "./util/contracthelpers"; import { + addToWhiteListExpectSuccess, + approveExpectSuccess, createCollectionExpectSuccess, createItemExpectSuccess, + enablePublicMintingExpectSuccess, + enableWhiteListExpectSuccess, getGenericResult, - normalizeAccountId + normalizeAccountId, + isWhitelisted, + transferFromExpectSuccess } from "./util/helpers"; @@ -27,7 +33,7 @@ const expect = chai.expect; const value = 0; -const gasLimit = 3000n * 1000000n; +const gasLimit = 9000n * 1000000n; const marketContractAddress = '5CYN9j3YvRkqxewoxeSvRbhAym4465C57uMmX5j4yz99L5H6'; describe('Contracts', () => { @@ -54,8 +60,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"); @@ -64,6 +72,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).toJSON(); // Transfer @@ -78,4 +89,165 @@ expect(tokenAfter.Owner).to.be.deep.equal(normalizeAccountId(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.toString()); + + 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.toString()); + + 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 @@ -677,7 +677,7 @@ transferFromExpectSuccess(collectionId: number, tokenId: number, accountApproved: IKeyringPair, - accountFrom: IKeyringPair, + accountFrom: IKeyringPair | CrossAccountId, accountTo: IKeyringPair | CrossAccountId, value: number | bigint = 1, type: string = 'NFT') { @@ -688,7 +688,7 @@ balanceBefore = await api.query.nft.balance(collectionId, toSubstrateAddress(to)) as unknown as BN; } const transferFromTx = api.tx.nft.transferFrom( - normalizeAccountId(accountFrom.address), to, collectionId, tokenId, value); + normalizeAccountId(accountFrom), to, collectionId, tokenId, value); const events = await submitTransactionAsync(accountApproved, transferFromTx); const result = getCreateItemResult(events); // tslint:disable-next-line:no-unused-expression @@ -950,7 +950,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