--- a/pallets/common/src/erc.rs +++ b/pallets/common/src/erc.rs @@ -26,7 +26,7 @@ use sp_std::vec::Vec; use up_data_structs::{ AccessMode, CollectionMode, CollectionPermissions, OwnerRestrictedSet, Property, - SponsoringRateLimit, + SponsoringRateLimit, SponsorshipState, }; use alloc::format; @@ -63,7 +63,7 @@ #[solidity_interface(name = Collection)] impl CollectionHandle where - T::AccountId: From<[u8; 32]>, + T::AccountId: From<[u8; 32]> + AsRef<[u8]>, { /// Set collection property. /// @@ -128,6 +128,25 @@ save(self) } + /// Set the substrate sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Substrate address of the sponsor from whose account funds will be debited for operations with the contract. + fn set_collection_sponsor_substrate(&mut self, caller: caller, sponsor: uint256) -> Result { + check_is_owner_or_admin(caller, self)?; + + let sponsor = convert_uint256_to_cross_account::(sponsor); + self.set_sponsor(sponsor.as_sub().clone()) + .map_err(dispatch_to_evm::)?; + save(self) + } + + // /// Whether there is a pending sponsor. + fn has_collection_pending_sponsor(&self) -> Result { + Ok(matches!(self.collection.sponsorship, SponsorshipState::Unconfirmed(_))) + } + /// Collection sponsorship confirmation. /// /// @dev After setting the sponsor for the collection, it must be confirmed with this function. @@ -142,6 +161,26 @@ save(self) } + /// Remove collection sponsor. + fn remove_collection_sponsor(&mut self, caller: caller) -> Result { + check_is_owner_or_admin(caller, self)?; + self.remove_sponsor().map_err(dispatch_to_evm::)?; + save(self) + } + + /// Get current sponsor. + /// + /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + fn get_collection_sponsor(&self) -> Result<(address, uint256)> { + let sponsor = match self.collection.sponsorship { + SponsorshipState::Disabled | SponsorshipState::Unconfirmed(_) => return Ok(Default::default()), + SponsorshipState::Confirmed(ref sponsor) => sponsor, + }; + let sponsor = T::CrossAccountId::from_sub(sponsor.clone()); + let sponsor_sub = convert_cross_account_to_uint256::(&sponsor)?; + Ok((*sponsor.as_eth(), sponsor_sub)) + } + /// Set limits for the collection. /// @dev Throws error if limit not found. /// @param limit Name of the limit. Valid names: --- a/pallets/common/src/eth.rs +++ b/pallets/common/src/eth.rs @@ -58,3 +58,14 @@ let slice = from.as_sub().as_ref(); uint256::from_big_endian(slice) } + +/// Convert `uint256` to `CrossAccountId`. +pub fn convert_uint256_to_cross_account(from: uint256) -> T::CrossAccountId +where + T::AccountId: From<[u8; 32]>, +{ + let mut new_admin_arr: [u8; 32] = Default::default(); + from.to_big_endian(&mut new_admin_arr); + let account_id = T::AccountId::from(new_admin_arr); + T::CrossAccountId::from_sub(account_id) +} --- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -231,6 +231,12 @@ Ok(true) } + /// Remove collection sponsor. + pub fn remove_sponsor(&mut self) -> DispatchResult { + self.collection.sponsorship = SponsorshipState::Disabled; + Ok(()) + } + /// Checks that the collection was created with, and must be operated upon through **Unique API**. /// Now check only the `external_collection` flag and if it's **true**, then return [`Error::CollectionIsExternal`] error. pub fn check_is_internal(&self) -> DispatchResult { @@ -732,12 +738,7 @@ /// Get the effective limits for the collection. pub fn effective_collection_limits(collection: CollectionId) -> Option { - let collection = >::get(collection); - if collection.is_none() { - return None; - } - - let collection = collection.unwrap(); + let collection = >::get(collection)?; let limits = collection.limits; let effective_limits = CollectionLimits { account_token_ownership_limit: Some(limits.account_token_ownership_limit()), --- a/pallets/fungible/src/erc.rs +++ b/pallets/fungible/src/erc.rs @@ -154,14 +154,14 @@ Collection(common_mut, CollectionHandle), ) )] -impl FungibleHandle where T::AccountId: From<[u8; 32]> {} +impl FungibleHandle where T::AccountId: From<[u8; 32]> + AsRef<[u8]> {} generate_stubgen!(gen_impl, UniqueFungibleCall<()>, true); generate_stubgen!(gen_iface, UniqueFungibleCall<()>, false); impl CommonEvmHandler for FungibleHandle where - T::AccountId: From<[u8; 32]>, + T::AccountId: From<[u8; 32]> + AsRef<[u8]>, { const CODE: &'static [u8] = include_bytes!("./stubs/UniqueFungible.raw"); --- a/pallets/nonfungible/src/erc.rs +++ b/pallets/nonfungible/src/erc.rs @@ -740,7 +740,7 @@ TokenProperties, ) )] -impl NonfungibleHandle where T::AccountId: From<[u8; 32]> {} +impl NonfungibleHandle where T::AccountId: From<[u8; 32]> + AsRef<[u8]> {} // Not a tests, but code generators generate_stubgen!(gen_impl, UniqueNFTCall<()>, true); @@ -748,7 +748,7 @@ impl CommonEvmHandler for NonfungibleHandle where - T::AccountId: From<[u8; 32]>, + T::AccountId: From<[u8; 32]> + AsRef<[u8]>, { const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw"); --- a/pallets/refungible/src/erc.rs +++ b/pallets/refungible/src/erc.rs @@ -789,7 +789,7 @@ TokenProperties, ) )] -impl RefungibleHandle where T::AccountId: From<[u8; 32]> {} +impl RefungibleHandle where T::AccountId: From<[u8; 32]> + AsRef<[u8]> {} // Not a tests, but code generators generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true); @@ -797,7 +797,7 @@ impl CommonEvmHandler for RefungibleHandle where - T::AccountId: From<[u8; 32]>, + T::AccountId: From<[u8; 32]> + AsRef<[u8]>, { const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw"); fn call( --- a/runtime/common/dispatch.rs +++ b/runtime/common/dispatch.rs @@ -124,7 +124,7 @@ + pallet_fungible::Config + pallet_nonfungible::Config + pallet_refungible::Config, - T::AccountId: From<[u8; 32]>, + T::AccountId: From<[u8; 32]> + AsRef<[u8]>, { fn is_reserved(target: &H160) -> bool { map_eth_to_id(target).is_some() --- a/tests/src/eth/collectionSponsoring.test.ts +++ b/tests/src/eth/collectionSponsoring.test.ts @@ -1,8 +1,10 @@ import {addToAllowListExpectSuccess, confirmSponsorshipExpectSuccess, createCollectionExpectSuccess, enablePublicMintingExpectSuccess, getDetailedCollectionInfo, setCollectionSponsorExpectSuccess} from '../util/helpers'; -import {itWeb3, createEthAccount, collectionIdToAddress, GAS_ARGS, normalizeEvents, createEthAccountWithBalance, evmCollectionHelpers, getCollectionAddressFromResult, evmCollection, ethBalanceViaSub} from './util/helpers'; +import {itWeb3, createEthAccount, collectionIdToAddress, GAS_ARGS, normalizeEvents, createEthAccountWithBalance, evmCollectionHelpers, getCollectionAddressFromResult, evmCollection, ethBalanceViaSub, subToEth} from './util/helpers'; import nonFungibleAbi from './nonFungibleAbi.json'; import {expect} from 'chai'; import {evmToAddress} from '@polkadot/util-crypto'; +import {submitTransactionAsync} from '../substrate/substrate-api'; +import getBalance from '../substrate/get-balance'; describe('evm collection sponsoring', () => { itWeb3('sponsors mint transactions', async ({web3, privateKeyWrapper}) => { @@ -38,6 +40,47 @@ ]); }); + itWeb3('Set substrate sponsor', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + 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 = privateKeyWrapper('//Alice'); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + + expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false; + result = await collectionEvm.methods.setCollectionSponsorSubstrate(sponsor.addressRaw).send({from: owner}); + expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.true; + + const confirmTx = await api.tx.unique.confirmSponsorship(collectionId); + await submitTransactionAsync(sponsor, confirmTx); + expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false; + + const sponsorTuple = await collectionEvm.methods.getCollectionSponsor().call({from: owner}); + expect(sponsorTuple.field_0).to.be.eq(subToEth(sponsor.address)); + }); + + itWeb3('Remove sponsor', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); + const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + + expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false; + result = await collectionEvm.methods.setCollectionSponsor(sponsor).send({from: owner}); + expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.true; + + await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor}); + expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false; + + await collectionEvm.methods.removeCollectionSponsor().send({from: owner}); + + const sponsorTuple = await collectionEvm.methods.getCollectionSponsor().call({from: owner}); + expect(sponsorTuple.field_0).to.be.eq('0x0000000000000000000000000000000000000000'); + }); + itWeb3('Sponsoring collection from evm address via access list', async ({api, web3, privateKeyWrapper}) => { const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const collectionHelpers = evmCollectionHelpers(web3, owner); @@ -107,6 +150,61 @@ } }); + itWeb3('Sponsoring collection from substrate address via access list', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + const result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const sponsor = privateKeyWrapper('//Alice'); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + + await collectionEvm.methods.setCollectionSponsorSubstrate(sponsor.addressRaw).send({from: owner}); + + const confirmTx = await api.tx.unique.confirmSponsorship(collectionId); + await submitTransactionAsync(sponsor, confirmTx); + + const user = createEthAccount(web3); + const nextTokenId = await collectionEvm.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + + await collectionEvm.methods.setCollectionAccess(1 /*'AllowList'*/).send({from: owner}); + await collectionEvm.methods.addToCollectionAllowList(user).send({from: owner}); + await collectionEvm.methods.setCollectionMintMode(true).send({from: owner}); + + const ownerBalanceBefore = await ethBalanceViaSub(api, owner); + const sponsorBalanceBefore = (await getBalance(api, [sponsor.address]))[0]; + + { + const nextTokenId = await collectionEvm.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + const result = await collectionEvm.methods.mintWithTokenURI( + user, + nextTokenId, + 'Test URI', + ).send({from: user}); + const events = normalizeEvents(result.events); + + expect(events).to.be.deep.equal([ + { + address: collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: user, + tokenId: nextTokenId, + }, + }, + ]); + + const ownerBalanceAfter = await ethBalanceViaSub(api, owner); + const sponsorBalanceAfter = (await getBalance(api, [sponsor.address]))[0]; + + expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); + expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter); + expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true; + } + }); + itWeb3('Check that transaction via EVM spend money from sponsor address', async ({api, web3, privateKeyWrapper}) => { const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const collectionHelpers = evmCollectionHelpers(web3, owner);