--- a/crates/evm-coder/src/solidity.rs +++ b/crates/evm-coder/src/solidity.rs @@ -455,7 +455,7 @@ } } -#[impl_for_tuples(0, 12)] +#[impl_for_tuples(0, 24)] impl SolidityFunctions for Tuple { for_tuples!( where #( Tuple: SolidityFunctions ),* ); --- a/pallets/common/src/erc.rs +++ b/pallets/common/src/erc.rs @@ -23,7 +23,7 @@ use pallet_evm_coder_substrate::dispatch_to_evm; use sp_core::{H160, U256, H256}; use sp_std::vec::Vec; -use up_data_structs::{Property, SponsoringRateLimit, NestingRule, OwnerRestrictedSet}; +use up_data_structs::{Property, SponsoringRateLimit, NestingRule, OwnerRestrictedSet, AccessMode}; use alloc::format; use crate::{Pallet, CollectionHandle, Config, CollectionProperties}; @@ -252,6 +252,42 @@ save(self); Ok(()) } + + fn set_access(&mut self, caller: caller, mode: string) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + self.check_is_owner_or_admin(&caller) + .map_err(dispatch_to_evm::)?; + self.collection.permissions.access = Some(match mode.as_str() { + "Normal" => AccessMode::Normal, + "AllowList" => AccessMode::AllowList, + _ => return Err("Not supported access mode".into()), + }); + save(self); + Ok(()) + } + + fn add_to_allow_list(&self, caller: caller, user: address) -> Result { + let caller = check_is_owner_or_admin(caller, self)?; + let user = T::CrossAccountId::from_eth(user); + >::toggle_allowlist(self, &caller, &user, true) + .map_err(dispatch_to_evm::)?; + Ok(()) + } + + fn remove_from_allow_list(&self, caller: caller, user: address) -> Result { + let caller = check_is_owner_or_admin(caller, self)?; + let user = T::CrossAccountId::from_eth(user); + >::toggle_allowlist(self, &caller, &user, false) + .map_err(dispatch_to_evm::)?; + Ok(()) + } + + fn set_mint_mode(&mut self, caller: caller, mode: bool) -> Result { + check_is_owner_or_admin(caller, self)?; + self.collection.permissions.mint_mode = Some(mode); + save(self); + Ok(()) + } } fn check_is_owner(caller: caller, collection: &CollectionHandle) -> Result<()> { @@ -262,6 +298,14 @@ Ok(()) } +fn check_is_owner_or_admin(caller: caller, collection: &CollectionHandle) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + collection + .check_is_owner_or_admin(&caller) + .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; + Ok(caller) +} + fn save(collection: &CollectionHandle) { >::insert(collection.id, collection.collection.clone()); } --- a/pallets/nonfungible/src/stubs/UniqueNFT.sol +++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol @@ -51,105 +51,6 @@ event MintingFinished(); } -// Selector: 3a54513b -contract Collection is Dummy, ERC165 { - // Selector: setCollectionProperty(string,bytes) 2f073f66 - function setCollectionProperty(string memory key, bytes memory value) - public - { - require(false, stub_error); - key; - value; - dummy = 0; - } - - // Selector: deleteCollectionProperty(string) 7b7debce - function deleteCollectionProperty(string memory key) public { - require(false, stub_error); - key; - dummy = 0; - } - - // Throws error if key not found - // - // Selector: collectionProperty(string) cf24fd6d - function collectionProperty(string memory key) - public - view - returns (bytes memory) - { - require(false, stub_error); - key; - dummy; - return hex""; - } - - // Selector: ethSetSponsor(address) 8f9af356 - function ethSetSponsor(address sponsor) public { - require(false, stub_error); - sponsor; - dummy = 0; - } - - // Selector: ethConfirmSponsorship() a8580d1a - function ethConfirmSponsorship() public { - require(false, stub_error); - dummy = 0; - } - - // Selector: setLimit(string,uint32) 68db30ca - function setLimit(string memory limit, uint32 value) public { - require(false, stub_error); - limit; - value; - dummy = 0; - } - - // Selector: setLimit(string,bool) ea67e4c2 - function setLimit(string memory limit, bool value) public { - require(false, stub_error); - limit; - value; - dummy = 0; - } - - // Selector: contractAddress() f6b4dfb4 - function contractAddress() public view returns (address) { - require(false, stub_error); - dummy; - return 0x0000000000000000000000000000000000000000; - } - - // Selector: addAdmin(address) 70480275 - function addAdmin(address newAdmin) public view { - require(false, stub_error); - newAdmin; - dummy; - } - - // Selector: removeAdmin(address) 1785f53c - function removeAdmin(address admin) public view { - require(false, stub_error); - admin; - dummy; - } - - // Selector: setNesting(bool) e8fc50dd - function setNesting(bool enable) public { - require(false, stub_error); - enable; - dummy = 0; - } - - // Selector: setNesting(bool,address[]) 7df12a9a - function setNesting(bool enable, address[] memory collections) public { - require(false, stub_error); - enable; - collections; - dummy = 0; - } -} - // Selector: 41369377 contract TokenProperties is Dummy, ERC165 { // Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa @@ -479,6 +380,133 @@ } } +// Selector: f56cd7fa +contract Collection is Dummy, ERC165 { + // Selector: setCollectionProperty(string,bytes) 2f073f66 + function setCollectionProperty(string memory key, bytes memory value) + public + { + require(false, stub_error); + key; + value; + dummy = 0; + } + + // Selector: deleteCollectionProperty(string) 7b7debce + function deleteCollectionProperty(string memory key) public { + require(false, stub_error); + key; + dummy = 0; + } + + // Throws error if key not found + // + // Selector: collectionProperty(string) cf24fd6d + function collectionProperty(string memory key) + public + view + returns (bytes memory) + { + require(false, stub_error); + key; + dummy; + return hex""; + } + + // Selector: ethSetSponsor(address) 8f9af356 + function ethSetSponsor(address sponsor) public { + require(false, stub_error); + sponsor; + dummy = 0; + } + + // Selector: ethConfirmSponsorship() a8580d1a + function ethConfirmSponsorship() public { + require(false, stub_error); + dummy = 0; + } + + // Selector: setLimit(string,uint32) 68db30ca + function setLimit(string memory limit, uint32 value) public { + require(false, stub_error); + limit; + value; + dummy = 0; + } + + // Selector: setLimit(string,bool) ea67e4c2 + function setLimit(string memory limit, bool value) public { + require(false, stub_error); + limit; + value; + dummy = 0; + } + + // Selector: contractAddress() f6b4dfb4 + function contractAddress() public view returns (address) { + require(false, stub_error); + dummy; + return 0x0000000000000000000000000000000000000000; + } + + // Selector: addAdmin(address) 70480275 + function addAdmin(address newAdmin) public view { + require(false, stub_error); + newAdmin; + dummy; + } + + // Selector: removeAdmin(address) 1785f53c + function removeAdmin(address admin) public view { + require(false, stub_error); + admin; + dummy; + } + + // Selector: setNesting(bool) e8fc50dd + function setNesting(bool enable) public { + require(false, stub_error); + enable; + dummy = 0; + } + + // Selector: setNesting(bool,address[]) 7df12a9a + function setNesting(bool enable, address[] memory collections) public { + require(false, stub_error); + enable; + collections; + dummy = 0; + } + + // Selector: setAccess(string) 488f56aa + function setAccess(string memory mode) public { + require(false, stub_error); + mode; + dummy = 0; + } + + // Selector: addToAllowList(address) 31f59102 + function addToAllowList(address user) public view { + require(false, stub_error); + user; + dummy; + } + + // Selector: removeFromAllowList(address) eba8dabc + function removeFromAllowList(address user) public view { + require(false, stub_error); + user; + dummy; + } + + // Selector: setMintMode(bool) 5dea9bd5 + function setMintMode(bool mode) public { + require(false, stub_error); + mode; + dummy = 0; + } +} + contract UniqueNFT is Dummy, ERC165, --- a/tests/src/eth/api/UniqueNFT.sol +++ b/tests/src/eth/api/UniqueNFT.sol @@ -42,51 +42,6 @@ event MintingFinished(); } -// Selector: 3a54513b -interface Collection is Dummy, ERC165 { - // Selector: setCollectionProperty(string,bytes) 2f073f66 - function setCollectionProperty(string memory key, bytes memory value) - external; - - // Selector: deleteCollectionProperty(string) 7b7debce - function deleteCollectionProperty(string memory key) external; - - // Throws error if key not found - // - // Selector: collectionProperty(string) cf24fd6d - function collectionProperty(string memory key) - external - view - returns (bytes memory); - - // Selector: ethSetSponsor(address) 8f9af356 - function ethSetSponsor(address sponsor) external; - - // Selector: ethConfirmSponsorship() a8580d1a - function ethConfirmSponsorship() external; - - // Selector: setLimit(string,uint32) 68db30ca - function setLimit(string memory limit, uint32 value) external; - - // Selector: setLimit(string,bool) ea67e4c2 - function setLimit(string memory limit, bool value) external; - - // Selector: contractAddress() f6b4dfb4 - function contractAddress() external view returns (address); - - // Selector: addAdmin(address) 70480275 - function addAdmin(address newAdmin) external view; - - // Selector: removeAdmin(address) 1785f53c - function removeAdmin(address admin) external view; - - // Selector: setNesting(bool) e8fc50dd - function setNesting(bool enable) external; - - // Selector: setNesting(bool,address[]) 7df12a9a - function setNesting(bool enable, address[] memory collections) external; -} - // Selector: 41369377 interface TokenProperties is Dummy, ERC165 { // Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa @@ -258,6 +213,63 @@ returns (bool); } +// Selector: f56cd7fa +interface Collection is Dummy, ERC165 { + // Selector: setCollectionProperty(string,bytes) 2f073f66 + function setCollectionProperty(string memory key, bytes memory value) + external; + + // Selector: deleteCollectionProperty(string) 7b7debce + function deleteCollectionProperty(string memory key) external; + + // Throws error if key not found + // + // Selector: collectionProperty(string) cf24fd6d + function collectionProperty(string memory key) + external + view + returns (bytes memory); + + // Selector: ethSetSponsor(address) 8f9af356 + function ethSetSponsor(address sponsor) external; + + // Selector: ethConfirmSponsorship() a8580d1a + function ethConfirmSponsorship() external; + + // Selector: setLimit(string,uint32) 68db30ca + function setLimit(string memory limit, uint32 value) external; + + // Selector: setLimit(string,bool) ea67e4c2 + function setLimit(string memory limit, bool value) external; + + // Selector: contractAddress() f6b4dfb4 + function contractAddress() external view returns (address); + + // Selector: addAdmin(address) 70480275 + function addAdmin(address newAdmin) external view; + + // Selector: removeAdmin(address) 1785f53c + function removeAdmin(address admin) external view; + + // Selector: setNesting(bool) e8fc50dd + function setNesting(bool enable) external; + + // Selector: setNesting(bool,address[]) 7df12a9a + function setNesting(bool enable, address[] memory collections) external; + + // Selector: setAccess(string) 488f56aa + function setAccess(string memory mode) external; + + // Selector: addToAllowList(address) 31f59102 + function addToAllowList(address user) external view; + + // Selector: removeFromAllowList(address) eba8dabc + function removeFromAllowList(address user) external view; + + // Selector: setMintMode(bool) 5dea9bd5 + function setMintMode(bool mode) external; +} + interface UniqueNFT is Dummy, ERC165, --- a/tests/src/eth/contractSponsoring.test.ts +++ b/tests/src/eth/contractSponsoring.test.ts @@ -31,6 +31,7 @@ evmCollectionHelpers, getCollectionAddressFromResult, evmCollection, + ethBalanceViaSub, } from './util/helpers'; import { addCollectionAdminExpectSuccess, @@ -221,66 +222,51 @@ }); //TODO: CORE-302 add eth methods - itWeb3.skip('Sponsoring evm address from substrate collection', async ({api, web3}) => { + itWeb3('Sponsoring collection from evm address via access list', async ({api, web3}) => { const owner = await createEthAccountWithBalance(api, web3); const collectionHelpers = evmCollectionHelpers(web3, owner); let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); const sponsor = await createEthAccountWithBalance(api, web3); const collectionEvm = evmCollection(web3, owner, collectionIdAddress); - result = await collectionEvm.methods.ethSetSponsor(sponsor).send(); + result = await collectionEvm.methods.ethSetSponsor(sponsor).send({from: owner}); let collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; expect(collectionSub.sponsorship.isUnconfirmed).to.be.true; expect(collectionSub.sponsorship.asUnconfirmed.toHuman()).to.be.eq(evmToAddress(sponsor)); await expect(collectionEvm.methods.ethConfirmSponsorship().call()).to.be.rejectedWith('Caller is not set as sponsor'); - const sponsorCollection = evmCollection(web3, sponsor, collectionIdAddress); - await sponsorCollection.methods.ethConfirmSponsorship().send(); + await collectionEvm.methods.ethConfirmSponsorship().send({from: sponsor}); collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; expect(collectionSub.sponsorship.isConfirmed).to.be.true; expect(collectionSub.sponsorship.asConfirmed.toHuman()).to.be.eq(evmToAddress(sponsor)); const user = createEthAccount(web3); - const userContract = evmCollection(web3, user, collectionIdAddress); - const nextTokenId = await userContract.methods.nextTokenId().call(); - + const nextTokenId = await collectionEvm.methods.nextTokenId().call(); expect(nextTokenId).to.be.equal('1'); - await expect(userContract.methods.mintWithTokenURI( - user, - nextTokenId, - 'Test URI', - ).call()).to.be.rejectedWith('PublicMintingNotAllowed'); - - // TODO: add this methods to eth - // { - // const tx = api.tx.unique.setPublicAccessMode(collectionId, 'AllowList'); - // const events = await submitTransactionAsync(owner, tx); - // const result = getCreateCollectionResult(events); - // expect(result.success).to.be.true; - // } - // { - // const tx = api.tx.unique.addToAllowList(collectionId, {Ethereum: userEth}); - // const events = await submitTransactionAsync(owner, tx); - // const result = getCreateCollectionResult(events); - // expect(result.success).to.be.true; - // } - // { - // const tx = api.tx.unique.setMintPermission(collectionId, true); - // const events = await submitTransactionAsync(owner, tx); - // const result = getCreateCollectionResult(events); - // expect(result.success).to.be.true; - // } + const oldPermissions = (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman(); + expect(oldPermissions.mintMode).to.be.false; + expect(oldPermissions.access).to.be.equal('Normal'); + + await collectionEvm.methods.setAccess('AllowList').send({from: owner}); + await collectionEvm.methods.addToAllowList(user).send({from: owner}); + await collectionEvm.methods.setMintMode(true).send({from: owner}); + + const newPermissions = (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman(); + expect(newPermissions.mintMode).to.be.true; + expect(newPermissions.access).to.be.equal('AllowList'); + // const [alicesBalanceBefore] = await getBalance(api, [alicesPublicKey]); { - const nextTokenId = await userContract.methods.nextTokenId().call(); + const nextTokenId = await collectionEvm.methods.nextTokenId().call(); expect(nextTokenId).to.be.equal('1'); - const result = await userContract.methods.mintWithTokenURI( + const result = await collectionEvm.methods.mintWithTokenURI( user, nextTokenId, 'Test URI', - ).send(); + ).call({from: user}); + console.log(result); const events = normalizeEvents(result.events); expect(events).to.be.deep.equal([ @@ -295,37 +281,63 @@ }, ]); - expect(await userContract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); + expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); } }); - //TODO: CORE-302 add eth methods - itWeb3.skip('Check that transaction via EVM spend money from substrate address', async ({api, web3, privateKeyWrapper}) => { - const owner = privateKeyWrapper('//Alice'); - const user = privateKeyWrapper(`//User/${Date.now()}`); - const userEth = subToEth(user.address); - const collectionId = await createCollectionExpectSuccess(); - await addCollectionAdminExpectSuccess(owner, collectionId, {Ethereum: userEth}); - await transferBalanceTo(api, owner, user.address); + itWeb3('Check that transaction via EVM spend money from sponsor address', async ({api, web3}) => { + const owner = await createEthAccountWithBalance(api, web3); + const collectionHelpers = evmCollectionHelpers(web3, owner); + let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const sponsor = await createEthAccountWithBalance(api, web3); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + result = await collectionEvm.methods.ethSetSponsor(sponsor).send(); + let collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + expect(collectionSub.sponsorship.isUnconfirmed).to.be.true; + expect(collectionSub.sponsorship.asUnconfirmed.toHuman()).to.be.eq(evmToAddress(sponsor)); + await expect(collectionEvm.methods.ethConfirmSponsorship().call()).to.be.rejectedWith('Caller is not set as sponsor'); + const sponsorCollection = evmCollection(web3, sponsor, collectionIdAddress); + await sponsorCollection.methods.ethConfirmSponsorship().send(); + collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + expect(collectionSub.sponsorship.isConfirmed).to.be.true; + expect(collectionSub.sponsorship.asConfirmed.toHuman()).to.be.eq(evmToAddress(sponsor)); + + const user = createEthAccount(web3); + await collectionEvm.methods.addAdmin(user).send(); + + const ownerBalanceBefore = await ethBalanceViaSub(api, owner); + const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor); + + + const userCollectionEvm = evmCollection(web3, user, collectionIdAddress); + const nextTokenId = await userCollectionEvm.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + result = await userCollectionEvm.methods.mintWithTokenURI( + user, + nextTokenId, + 'Test URI', + ).send(); + const events = normalizeEvents(result.events); const address = collectionIdToAddress(collectionId); - const contract = new web3.eth.Contract(nonFungibleAbi as any, address, {from: userEth, ...GAS_ARGS}); - const [userBalanceBefore] = await getBalance(api, [user.address]); - - { - const nextTokenId = await contract.methods.nextTokenId().call(); - expect(nextTokenId).to.be.equal('1'); - await executeEthTxOnSub(web3, api, user, contract, m => m.mintWithTokenURI( - userEth, - nextTokenId, - 'Test URI', - )); - - expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); - } - - const [userBalanceAfter] = await getBalance(api, [user.address]); - expect(userBalanceAfter < userBalanceBefore).to.be.true; + expect(events).to.be.deep.equal([ + { + address, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: user, + tokenId: nextTokenId, + }, + }, + ]); + expect(await userCollectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); + + const ownerBalanceAfter = await ethBalanceViaSub(api, owner); + expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore); + const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor); + expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; }); }); --- a/tests/src/eth/nonFungibleAbi.json +++ b/tests/src/eth/nonFungibleAbi.json @@ -91,6 +91,15 @@ }, { "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "addToAllowList", + "outputs": [], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "approved", "type": "address" }, { "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], @@ -300,6 +309,15 @@ }, { "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "removeFromAllowList", + "outputs": [], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "from", "type": "address" }, { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "tokenId", "type": "uint256" } @@ -322,6 +340,13 @@ "type": "function" }, { + "inputs": [{ "internalType": "string", "name": "mode", "type": "string" }], + "name": "setAccess", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [ { "internalType": "address", "name": "operator", "type": "address" }, { "internalType": "bool", "name": "approved", "type": "bool" } @@ -362,6 +387,13 @@ "type": "function" }, { + "inputs": [{ "internalType": "bool", "name": "mode", "type": "bool" }], + "name": "setMintMode", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [ { "internalType": "bool", "name": "enable", "type": "bool" }, {