From 0b44e9f57a8aafed501eb45c469f43fb280c70d4 Mon Sep 17 00:00:00 2001 From: Grigoriy Simonov Date: Fri, 23 Dec 2022 11:25:15 +0000 Subject: [PATCH] feat: add collection sponsoring for rft --- --- a/runtime/common/config/pallets/app_promotion.rs +++ b/runtime/common/config/pallets/app_promotion.rs @@ -22,7 +22,7 @@ use frame_support::{parameter_types, PalletId}; use sp_arithmetic::Perbill; use up_common::{ - constants::{UNIQUE, RELAY_DAYS, DAYS}, + constants::{UNIQUE, RELAY_DAYS}, types::Balance, }; --- a/runtime/common/ethereum/sponsoring.rs +++ b/runtime/common/ethereum/sponsoring.rs @@ -32,14 +32,22 @@ Config as FungibleConfig, erc::{UniqueFungibleCall, ERC20Call}, }; -use pallet_refungible::Config as RefungibleConfig; +use pallet_refungible::{ + Config as RefungibleConfig, + erc::UniqueRefungibleCall, + erc_token::{RefungibleTokenHandle, UniqueRefungibleTokenCall}, + RefungibleHandle, +}; use pallet_unique::Config as UniqueConfig; use sp_std::prelude::*; -use up_data_structs::{CollectionMode, CreateItemData, CreateNftData, TokenId}; +use up_data_structs::{ + CollectionMode, CreateItemData, CreateNftData, mapping::TokenAddressMapping, TokenId, +}; use up_sponsorship::SponsorshipHandler; - use crate::{Runtime, runtime_common::sponsoring::*}; +mod refungible; + pub type EvmSponsorshipHandler = ( UniqueEthSponsorshipHandler, pallet_evm_contract_helpers::HelpersContractSponsoring, @@ -53,80 +61,155 @@ who: &T::CrossAccountId, call_context: &CallContext, ) -> Option { - let collection_id = map_eth_to_id(&call_context.contract_address)?; - let collection = >::new(collection_id)?; - let sponsor = collection.sponsorship.sponsor()?.clone(); - let (method_id, mut reader) = AbiReader::new_call(&call_context.input).ok()?; - Some(T::CrossAccountId::from_sub(match &collection.mode { - CollectionMode::NFT => { - let call = >::parse(method_id, &mut reader).ok()??; - match call { - UniqueNFTCall::TokenProperties(TokenPropertiesCall::SetProperty { - token_id, - key, - value, - .. - }) => { - let token_id: TokenId = token_id.try_into().ok()?; - withdraw_set_token_property::( + if let Some(collection_id) = map_eth_to_id(&call_context.contract_address) { + let collection = >::new(collection_id)?; + let sponsor = collection.sponsorship.sponsor()?.clone(); + let (method_id, mut reader) = AbiReader::new_call(&call_context.input).ok()?; + Some(T::CrossAccountId::from_sub(match &collection.mode { + CollectionMode::NFT => { + let call = >::parse(method_id, &mut reader).ok()??; + match call { + UniqueNFTCall::TokenProperties(TokenPropertiesCall::SetProperty { + token_id, + key, + value, + .. + }) => { + let token_id: TokenId = token_id.try_into().ok()?; + withdraw_set_token_property::( + &collection, + &who, + &token_id, + key.len() + value.len(), + ) + .map(|()| sponsor) + } + UniqueNFTCall::ERC721UniqueExtensions( + ERC721UniqueExtensionsCall::Transfer { token_id, .. }, + ) => { + let token_id: TokenId = token_id.try_into().ok()?; + withdraw_transfer::(&collection, &who, &token_id).map(|()| sponsor) + } + UniqueNFTCall::ERC721UniqueMintable( + ERC721UniqueMintableCall::Mint { .. } + | ERC721UniqueMintableCall::MintCheckId { .. } + | ERC721UniqueMintableCall::MintWithTokenUri { .. } + | ERC721UniqueMintableCall::MintWithTokenUriCheckId { .. }, + ) => withdraw_create_item::( &collection, &who, - &token_id, - key.len() + value.len(), + &CreateItemData::NFT(CreateNftData::default()), ) - .map(|()| sponsor) - } - UniqueNFTCall::ERC721UniqueExtensions( - ERC721UniqueExtensionsCall::Transfer { token_id, .. }, - ) => { - let token_id: TokenId = token_id.try_into().ok()?; - withdraw_transfer::(&collection, &who, &token_id).map(|()| sponsor) - } - UniqueNFTCall::ERC721UniqueMintable( - ERC721UniqueMintableCall::Mint { .. } - | ERC721UniqueMintableCall::MintCheckId { .. } - | ERC721UniqueMintableCall::MintWithTokenUri { .. } - | ERC721UniqueMintableCall::MintWithTokenUriCheckId { .. }, - ) => withdraw_create_item::( - &collection, - &who, - &CreateItemData::NFT(CreateNftData::default()), - ) - .map(|()| sponsor), - UniqueNFTCall::ERC721(ERC721Call::TransferFrom { token_id, from, .. }) => { - let token_id: TokenId = token_id.try_into().ok()?; - let from = T::CrossAccountId::from_eth(from); - withdraw_transfer::(&collection, &from, &token_id).map(|()| sponsor) - } - UniqueNFTCall::ERC721(ERC721Call::Approve { token_id, .. }) => { - let token_id: TokenId = token_id.try_into().ok()?; - withdraw_approve::(&collection, who.as_sub(), &token_id) - .map(|()| sponsor) + .map(|()| sponsor), + UniqueNFTCall::ERC721(ERC721Call::TransferFrom { + token_id, from, .. + }) => { + let token_id: TokenId = token_id.try_into().ok()?; + let from = T::CrossAccountId::from_eth(from); + withdraw_transfer::(&collection, &from, &token_id).map(|()| sponsor) + } + UniqueNFTCall::ERC721(ERC721Call::Approve { token_id, .. }) => { + let token_id: TokenId = token_id.try_into().ok()?; + withdraw_approve::(&collection, who.as_sub(), &token_id) + .map(|()| sponsor) + } + _ => None, } - _ => None, } - } - CollectionMode::Fungible(_) => { - let call = >::parse(method_id, &mut reader).ok()??; - #[allow(clippy::single_match)] - match call { - UniqueFungibleCall::ERC20(ERC20Call::Transfer { .. }) => { - withdraw_transfer::(&collection, who, &TokenId::default()) - .map(|()| sponsor) - } - UniqueFungibleCall::ERC20(ERC20Call::TransferFrom { from, .. }) => { - let from = T::CrossAccountId::from_eth(from); - withdraw_transfer::(&collection, &from, &TokenId::default()) - .map(|()| sponsor) - } - UniqueFungibleCall::ERC20(ERC20Call::Approve { .. }) => { - withdraw_approve::(&collection, who.as_sub(), &TokenId::default()) - .map(|()| sponsor) + CollectionMode::ReFungible => { + let call = >::parse(method_id, &mut reader).ok()??; + refungible::call_sponsor(call, collection, who).map(|()| sponsor) + } + CollectionMode::Fungible(_) => { + let call = >::parse(method_id, &mut reader).ok()??; + match call { + UniqueFungibleCall::ERC20(ERC20Call::Transfer { .. }) => { + withdraw_transfer::(&collection, who, &TokenId::default()) + .map(|()| sponsor) + } + UniqueFungibleCall::ERC20(ERC20Call::TransferFrom { from, .. }) => { + let from = T::CrossAccountId::from_eth(from); + withdraw_transfer::(&collection, &from, &TokenId::default()) + .map(|()| sponsor) + } + UniqueFungibleCall::ERC20(ERC20Call::Approve { .. }) => { + withdraw_approve::(&collection, who.as_sub(), &TokenId::default()) + .map(|()| sponsor) + } + _ => None, } - _ => None, } - } - _ => None, - }?)) + }?)) + } else { + let (collection_id, token_id) = + T::EvmTokenAddressMapping::address_to_token(&call_context.contract_address)?; + let collection = >::new(collection_id)?; + let sponsor = collection.sponsorship.sponsor()?.clone(); + let rft_collection = RefungibleHandle::cast(collection); + let token = RefungibleTokenHandle(rft_collection, token_id); + let (method_id, mut reader) = AbiReader::new_call(&call_context.input).ok()?; + let call = >::parse(method_id, &mut reader).ok()??; + Some(T::CrossAccountId::from_sub( + refungible::token_call_sponsor(call, token, who).map(|()| sponsor)?, + )) + } + } +} + +mod common { + use super::*; + + use pallet_common::erc::{CollectionCall}; + + pub fn collection_call_sponsor( + call: CollectionCall, + _collection: CollectionHandle, + _who: &T::CrossAccountId, + ) -> Option<()> + where + T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig, + { + use CollectionCall::*; + + match call { + // Readonly + ERC165Call(_, _) + | CollectionProperty { .. } + | CollectionProperties { .. } + | HasCollectionPendingSponsor + | CollectionSponsor + | ContractAddress + | AllowlistedCross { .. } + | IsOwnerOrAdminEth { .. } + | IsOwnerOrAdminCross { .. } + | CollectionOwner + | CollectionAdmins + | UniqueCollectionType => None, + + // Not sponsored + AddToCollectionAllowList { .. } + | AddToCollectionAllowListCross { .. } + | RemoveFromCollectionAllowList { .. } + | RemoveFromCollectionAllowListCross { .. } + | AddCollectionAdminCross { .. } + | RemoveCollectionAdminCross { .. } + | AddCollectionAdmin { .. } + | RemoveCollectionAdmin { .. } + | SetNestingBool { .. } + | SetNesting { .. } + | SetCollectionAccess { .. } + | SetCollectionMintMode { .. } + | SetOwner { .. } + | ChangeCollectionOwnerCross { .. } + | SetCollectionProperty { .. } + | SetCollectionProperties { .. } + | DeleteCollectionProperty { .. } + | DeleteCollectionProperties { .. } + | SetCollectionSponsor { .. } + | SetCollectionSponsorCross { .. } + | ConfirmCollectionSponsorship + | RemoveCollectionSponsor + | SetIntLimit { .. } => None, + } } } --- /dev/null +++ b/runtime/common/ethereum/sponsoring/refungible.rs @@ -0,0 +1,359 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +//! Implements EVM sponsoring logic via TransactionValidityHack + +use core::convert::TryInto; +use pallet_common::CollectionHandle; +use pallet_evm::account::CrossAccountId; +use pallet_fungible::Config as FungibleConfig; +use pallet_refungible::Config as RefungibleConfig; +use pallet_nonfungible::Config as NonfungibleConfig; +use pallet_unique::Config as UniqueConfig; +use up_data_structs::{CreateItemData, CreateNftData, TokenId}; + +use super::common; +use crate::runtime_common::sponsoring::*; + +use pallet_refungible::{ + erc::{ + ERC721BurnableCall, ERC721Call, ERC721EnumerableCall, ERC721MetadataCall, + ERC721UniqueExtensionsCall, ERC721UniqueMintableCall, TokenPropertiesCall, + UniqueRefungibleCall, + }, + erc_token::{ + ERC1633Call, ERC20Call, ERC20UniqueExtensionsCall, RefungibleTokenHandle, + UniqueRefungibleTokenCall, + }, +}; + +pub fn call_sponsor( + call: UniqueRefungibleCall, + collection: CollectionHandle, + who: &T::CrossAccountId, +) -> Option<()> +where + T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig, +{ + use UniqueRefungibleCall::*; + + match call { + // Readonly + ERC165Call(_, _) => None, + + ERC721Enumerable(call) => erc721::enumerable_call_sponsor(call, collection, who), + ERC721Burnable(call) => erc721::burnable_call_sponsor(call, collection, who), + ERC721Metadata(call) => erc721::metadata_call_sponsor(call, collection, who), + Collection(call) => common::collection_call_sponsor(call, collection, who), + ERC721(call) => erc721::call_sponsor(call, collection, who), + ERC721UniqueExtensions(call) => { + erc721::unique_extensions_call_sponsor(call, collection, who) + } + ERC721UniqueMintable(call) => erc721::unique_mintable_call_sponsor(call, collection, who), + TokenProperties(call) => token_properties_call_sponsor(call, collection, who), + } +} + +pub fn token_properties_call_sponsor( + call: TokenPropertiesCall, + collection: CollectionHandle, + who: &T::CrossAccountId, +) -> Option<()> +where + T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig, +{ + use TokenPropertiesCall::*; + + match call { + // Readonly + ERC165Call(_, _) | Property { .. } => None, + + // Not sponsored + SetTokenPropertyPermission { .. } + | SetProperties { .. } + | DeleteProperty { .. } + | DeleteProperties { .. } => None, + + SetProperty { + token_id, + key, + value, + .. + } => { + let token_id: TokenId = token_id.try_into().ok()?; + withdraw_set_token_property::(&collection, &who, &token_id, key.len() + value.len()) + } + } +} + +pub fn token_call_sponsor( + call: UniqueRefungibleTokenCall, + token: RefungibleTokenHandle, + who: &T::CrossAccountId, +) -> Option<()> +where + T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig, +{ + use UniqueRefungibleTokenCall::*; + + match call { + // Readonly + ERC165Call(_, _) => None, + + ERC20(call) => erc20::call_sponsor(call, token, who), + ERC20UniqueExtensions(call) => erc20::unique_extensions_call_sponsor(call, token, who), + ERC1633(call) => erc1633::call_sponsor(call, token, who), + } +} + +mod erc721 { + use super::*; + + pub fn call_sponsor( + call: ERC721Call, + collection: CollectionHandle, + who: &T::CrossAccountId, + ) -> Option<()> + where + T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig, + { + use ERC721Call::*; + + match call { + // Readonly + ERC165Call(_, _) + | BalanceOf { .. } + | OwnerOf { .. } + | GetApproved { .. } + | IsApprovedForAll { .. } + | CollectionHelperAddress => None, + + // Not sponsored + SafeTransferFromWithData { .. } + | SafeTransferFrom { .. } + | SetApprovalForAll { .. } => None, + + TransferFrom { token_id, from, .. } => { + let token_id: TokenId = token_id.try_into().ok()?; + let from = T::CrossAccountId::from_eth(from); + withdraw_transfer::(&collection, &from, &token_id) + } + Approve { _token_id, .. } => { + let token_id: TokenId = _token_id.try_into().ok()?; + withdraw_approve::(&collection, who.as_sub(), &token_id) + } + } + } + + pub fn enumerable_call_sponsor( + call: ERC721EnumerableCall, + _collection: CollectionHandle, + _who: &T::CrossAccountId, + ) -> Option<()> + where + T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig, + { + use ERC721EnumerableCall::*; + + match call { + // Readonly + ERC165Call(_, _) | TokenByIndex { .. } | TokenOfOwnerByIndex { .. } | TotalSupply => { + None + } + } + } + + pub fn burnable_call_sponsor( + call: ERC721BurnableCall, + _collection: CollectionHandle, + _who: &T::CrossAccountId, + ) -> Option<()> + where + T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig, + { + use ERC721BurnableCall::*; + + match call { + // Readonly + ERC165Call(_, _) => None, + + // Not sponsored + Burn { .. } => None, + } + } + + pub fn metadata_call_sponsor( + call: ERC721MetadataCall, + _collection: CollectionHandle, + _who: &T::CrossAccountId, + ) -> Option<()> + where + T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig, + { + use ERC721MetadataCall::*; + + match call { + // Readonly + ERC165Call(_, _) | NameProxy | SymbolProxy | TokenUri { .. } => None, + } + } + + pub fn unique_extensions_call_sponsor( + call: ERC721UniqueExtensionsCall, + collection: CollectionHandle, + who: &T::CrossAccountId, + ) -> Option<()> + where + T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig, + { + use ERC721UniqueExtensionsCall::*; + + match call { + // Readonly + ERC165Call(_, _) + | Name + | Symbol + | Description + | CrossOwnerOf { .. } + | Properties { .. } + | NextTokenId + | TokenContractAddress { .. } => None, + + // Not sponsored + TransferCross { .. } + | TransferFromCross { .. } + | BurnFrom { .. } + | BurnFromCross { .. } + | MintBulk { .. } + | MintBulkWithTokenUri { .. } => None, + + Transfer { token_id, .. } => { + let token_id: TokenId = token_id.try_into().ok()?; + withdraw_transfer::(&collection, &who, &token_id) + } + } + } + + pub fn unique_mintable_call_sponsor( + call: ERC721UniqueMintableCall, + collection: CollectionHandle, + who: &T::CrossAccountId, + ) -> Option<()> + where + T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig, + { + use ERC721UniqueMintableCall::*; + + match call { + // Readonly + ERC165Call(_, _) | MintingFinished => None, + + // Not sponsored + FinishMinting => None, + + Mint { .. } + | MintCheckId { .. } + | MintWithTokenUri { .. } + | MintWithTokenUriCheckId { .. } => withdraw_create_item::( + &collection, + &who, + &CreateItemData::NFT(CreateNftData::default()), + ), + } + } +} + +mod erc20 { + use super::*; + + pub fn call_sponsor( + call: ERC20Call, + token: RefungibleTokenHandle, + who: &T::CrossAccountId, + ) -> Option<()> + where + T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig, + { + use ERC20Call::*; + + match call { + // Readonly + ERC165Call(_, _) + | Name + | Symbol + | TotalSupply + | Decimals + | BalanceOf { .. } + | Allowance { .. } => None, + + Transfer { .. } => { + let RefungibleTokenHandle(handle, token_id) = token; + let token_id = token_id.try_into().ok()?; + withdraw_transfer::(&handle, &who, &token_id) + } + TransferFrom { from, .. } => { + let RefungibleTokenHandle(handle, token_id) = token; + let token_id = token_id.try_into().ok()?; + let from = T::CrossAccountId::from_eth(from); + withdraw_transfer::(&handle, &from, &token_id) + } + Approve { .. } => { + let RefungibleTokenHandle(handle, token_id) = token; + let token_id = token_id.try_into().ok()?; + withdraw_approve::(&handle, who.as_sub(), &token_id) + } + } + } + + pub fn unique_extensions_call_sponsor( + call: ERC20UniqueExtensionsCall, + _token: RefungibleTokenHandle, + _who: &T::CrossAccountId, + ) -> Option<()> + where + T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig, + { + use ERC20UniqueExtensionsCall::*; + + match call { + // Readonly + ERC165Call(_, _) => None, + + // Not sponsored + BurnFrom { .. } | Repartition { .. } => None, + } + } +} + +mod erc1633 { + use super::*; + + pub fn call_sponsor( + call: ERC1633Call, + _token: RefungibleTokenHandle, + _who: &T::CrossAccountId, + ) -> Option<()> + where + T: UniqueConfig + FungibleConfig + NonfungibleConfig + RefungibleConfig, + { + use ERC1633Call::*; + + match call { + // Readonly + ERC165Call(_, _) | ParentToken | ParentTokenId => None, + } + } +} --- a/tests/src/eth/collectionSponsoring.test.ts +++ b/tests/src/eth/collectionSponsoring.test.ts @@ -15,10 +15,10 @@ // along with Unique Network. If not, see . import {IKeyringPair} from '@polkadot/types/types'; -import {usingPlaygrounds} from '../util/index'; +import {Pallets, requirePalletsOrSkip, usingPlaygrounds} from '../util/index'; import {itEth, expect} from './util'; -describe('evm collection sponsoring', () => { +describe('evm nft collection sponsoring', () => { let donor: IKeyringPair; let alice: IKeyringPair; let nominal: bigint; @@ -317,3 +317,529 @@ expect(collectionSponsor).to.deep.eq({Unconfirmed: sponsorSub.address}); }); }); + +describe('evm RFT collection sponsoring', () => { + let donor: IKeyringPair; + let alice: IKeyringPair; + let nominal: bigint; + + before(async function() { + await usingPlaygrounds(async (helper, privateKey) => { + requirePalletsOrSkip(this, helper, [Pallets.ReFungible]); + donor = await privateKey({filename: __filename}); + [alice] = await helper.arrange.createAccounts([100n], donor); + nominal = helper.balance.getOneTokenNominal(); + }); + }); + + itEth('sponsors mint transactions', async ({helper}) => { + const collection = await helper.rft.mintCollection(alice, {tokenPrefix: 'spnr', permissions: {mintMode: true}}); + await collection.setSponsor(alice, alice.address); + await collection.confirmSponsorship(alice); + + const minter = helper.eth.createAccount(); + expect(await helper.balance.getEthereum(minter)).to.equal(0n); + + const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId); + const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', minter); + + await collection.addToAllowList(alice, {Ethereum: minter}); + + const result = await contract.methods.mint(minter).send(); + + const events = helper.eth.normalizeEvents(result.events); + expect(events).to.deep.include({ + address: collectionAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: minter, + tokenId: '1', + }, + }); + }); + + // TODO: Temprorary off. Need refactor + // 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.createRFTCollection('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.collectionSponsor().call({from: owner}); + // expect(bigIntToSub(api, BigInt(sponsorTuple[1]))).to.be.eq(sponsor.address); + // }); + + // Soft-deprecated + itEth('[eth] Remove sponsor', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner); + + let result = await collectionHelpers.methods.createRFTCollection('Sponsor collection', '1', '1').send({value: Number(2n * nominal)}); + const collectionIdAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); + const sponsor = await helper.eth.createAccountWithBalance(donor); + const collectionEvm = helper.ethNativeContract.collection(collectionIdAddress, 'rft', owner, true); + + 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.collectionSponsor().call({from: owner}); + expect(sponsorTuple.field_0).to.be.eq('0x0000000000000000000000000000000000000000'); + }); + + itEth('[cross] Remove sponsor', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner); + + let result = await collectionHelpers.methods.createRFTCollection('Sponsor collection', '1', '1').send({value: Number(2n * nominal)}); + const collectionIdAddress = helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); + const sponsor = await helper.eth.createAccountWithBalance(donor); + const sponsorCross = helper.ethCrossAccount.fromAddress(sponsor); + const collectionEvm = helper.ethNativeContract.collection(collectionIdAddress, 'rft', owner); + + expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false; + result = await collectionEvm.methods.setCollectionSponsorCross(sponsorCross).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.collectionSponsor().call({from: owner}); + expect(sponsorTuple.field_0).to.be.eq('0x0000000000000000000000000000000000000000'); + }); + + // Soft-deprecated + itEth('[eth] Sponsoring collection from evm address via access list', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const {collectionId, collectionAddress} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner, 'Sponsor collection', '1', '1', ''); + + const collection = helper.rft.getCollectionObject(collectionId); + const sponsor = await helper.eth.createAccountWithBalance(donor); + const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner, true); + + await collectionEvm.methods.setCollectionSponsor(sponsor).send({from: owner}); + let collectionData = (await collection.getData())!; + expect(collectionData.raw.sponsorship.Unconfirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true)); + await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); + + await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor}); + collectionData = (await collection.getData())!; + expect(collectionData.raw.sponsorship.Confirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true)); + + const user = helper.eth.createAccount(); + const nextTokenId = await collectionEvm.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + + const oldPermissions = (await collection.getData())!.raw.permissions; // (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman(); + expect(oldPermissions.mintMode).to.be.false; + expect(oldPermissions.access).to.be.equal('Normal'); + + 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 newPermissions = (await collection.getData())!.raw.permissions; // (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman(); + expect(newPermissions.mintMode).to.be.true; + expect(newPermissions.access).to.be.equal('AllowList'); + + const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner)); + const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor)); + + { + const result = await collectionEvm.methods.mintWithTokenURI(user, 'Test URI').send({from: user}); + const events = helper.eth.normalizeEvents(result.events); + + expect(events).to.deep.include({ + address: collectionAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: user, + tokenId: '1', + }, + }); + + const ownerBalanceAfter = await helper.balance.getSubstrate(await helper.address.ethToSubstrate(owner)); + const sponsorBalanceAfter = await helper.balance.getSubstrate(await helper.address.ethToSubstrate(sponsor)); + + expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); + expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter); + expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true; + } + }); + + itEth('[cross] Sponsoring collection from evm address via access list', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const {collectionId, collectionAddress} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner, 'Sponsor collection', '1', '1', ''); + + const collection = helper.rft.getCollectionObject(collectionId); + const sponsor = await helper.eth.createAccountWithBalance(donor); + const sponsorCross = helper.ethCrossAccount.fromAddress(sponsor); + const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); + + await collectionEvm.methods.setCollectionSponsorCross(sponsorCross).send({from: owner}); + let collectionData = (await collection.getData())!; + expect(collectionData.raw.sponsorship.Unconfirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true)); + await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); + + await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor}); + collectionData = (await collection.getData())!; + expect(collectionData.raw.sponsorship.Confirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true)); + + const user = helper.eth.createAccount(); + const userCross = helper.ethCrossAccount.fromAddress(user); + const nextTokenId = await collectionEvm.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + + const oldPermissions = (await collection.getData())!.raw.permissions; // (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman(); + expect(oldPermissions.mintMode).to.be.false; + expect(oldPermissions.access).to.be.equal('Normal'); + + await collectionEvm.methods.setCollectionAccess(1 /*'AllowList'*/).send({from: owner}); + await collectionEvm.methods.addToCollectionAllowListCross(userCross).send({from: owner}); + await collectionEvm.methods.setCollectionMintMode(true).send({from: owner}); + + const newPermissions = (await collection.getData())!.raw.permissions; // (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman(); + expect(newPermissions.mintMode).to.be.true; + expect(newPermissions.access).to.be.equal('AllowList'); + + const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner)); + const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor)); + + { + const result = await collectionEvm.methods.mintWithTokenURI(user, 'Test URI').send({from: user}); + const events = helper.eth.normalizeEvents(result.events); + + expect(events).to.deep.include({ + address: collectionAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: user, + tokenId: '1', + }, + }); + + const ownerBalanceAfter = await helper.balance.getSubstrate(await helper.address.ethToSubstrate(owner)); + const sponsorBalanceAfter = await helper.balance.getSubstrate(await helper.address.ethToSubstrate(sponsor)); + + expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); + expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter); + expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true; + } + }); + + // TODO: Temprorary off. Need refactor + // 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.createERC721MetadataCompatibleRFTCollection('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; + // } + // }); + + // Soft-deprecated + itEth('[eth] Check that transaction via EVM spend money from sponsor address', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const {collectionAddress, collectionId} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner,'Sponsor collection', '1', '1', ''); + const collection = helper.rft.getCollectionObject(collectionId); + const sponsor = await helper.eth.createAccountWithBalance(donor); + const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner, true); + + await collectionEvm.methods.setCollectionSponsor(sponsor).send(); + let collectionData = (await collection.getData())!; + expect(collectionData.raw.sponsorship.Unconfirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true)); + await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); + + const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor, true); + await sponsorCollection.methods.confirmCollectionSponsorship().send(); + collectionData = (await collection.getData())!; + expect(collectionData.raw.sponsorship.Confirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true)); + + const user = helper.eth.createAccount(); + await collectionEvm.methods.addCollectionAdmin(user).send(); + + const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner)); + const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor)); + + const userCollectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', user, true); + + const result = await userCollectionEvm.methods.mintWithTokenURI(user, 'Test URI').send(); + const tokenId = result.events.Transfer.returnValues.tokenId; + + const events = helper.eth.normalizeEvents(result.events); + const address = helper.ethAddress.fromCollectionId(collectionId); + + expect(events).to.deep.include({ + address, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: user, + tokenId: '1', + }, + }); + expect(await userCollectionEvm.methods.tokenURI(tokenId).call()).to.be.equal('Test URI'); + + const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner)); + expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore); + const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor)); + expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; + }); + + itEth('[cross] Check that transaction via EVM spend money from sponsor address', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const {collectionAddress, collectionId} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner,'Sponsor collection', '1', '1', ''); + const collection = helper.rft.getCollectionObject(collectionId); + const sponsor = await helper.eth.createAccountWithBalance(donor); + const sponsorCross = helper.ethCrossAccount.fromAddress(sponsor); + const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); + + await collectionEvm.methods.setCollectionSponsorCross(sponsorCross).send(); + let collectionData = (await collection.getData())!; + expect(collectionData.raw.sponsorship.Unconfirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true)); + await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); + + const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor); + await sponsorCollection.methods.confirmCollectionSponsorship().send(); + collectionData = (await collection.getData())!; + expect(collectionData.raw.sponsorship.Confirmed).to.be.eq(helper.address.ethToSubstrate(sponsor, true)); + + const user = helper.eth.createAccount(); + const userCross = helper.ethCrossAccount.fromAddress(user); + await collectionEvm.methods.addCollectionAdminCross(userCross).send(); + + const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner)); + const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor)); + + const userCollectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', user); + + const result = await userCollectionEvm.methods.mintWithTokenURI(user, 'Test URI').send(); + const tokenId = result.events.Transfer.returnValues.tokenId; + + const events = helper.eth.normalizeEvents(result.events); + const address = helper.ethAddress.fromCollectionId(collectionId); + + expect(events).to.deep.include({ + address, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: user, + tokenId: '1', + }, + }); + expect(await userCollectionEvm.methods.tokenURI(tokenId).call()).to.be.equal('Test URI'); + + const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner)); + expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore); + const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor)); + expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; + }); +}); + +describe('evm RFT token sponsoring', () => { + let donor: IKeyringPair; + + before(async function() { + await usingPlaygrounds(async (helper, privateKey) => { + requirePalletsOrSkip(this, helper, [Pallets.ReFungible]); + donor = await privateKey({filename: __filename}); + }); + }); + + itEth('[cross] Check that transfer via EVM spend money from sponsor address', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const {collectionAddress, collectionId} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner,'Sponsor collection', '1', '1', ''); + const sponsor = await helper.eth.createAccountWithBalance(donor); + const sponsorCross = helper.ethCrossAccount.fromAddress(sponsor); + const receiver = await helper.eth.createAccountWithBalance(donor); + const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); + + await collectionEvm.methods.setCollectionSponsorCross(sponsorCross).send(); + + const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor); + await sponsorCollection.methods.confirmCollectionSponsorship().send(); + + const user = await helper.eth.createAccountWithBalance(donor); + const userCross = helper.ethCrossAccount.fromAddress(user); + await collectionEvm.methods.addCollectionAdminCross(userCross).send(); + + const userCollectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', user); + + const result = await userCollectionEvm.methods.mintWithTokenURI(user, 'Test URI').send(); + const tokenId = result.events.Transfer.returnValues.tokenId; + + const tokenContract = helper.ethNativeContract.rftTokenById(collectionId, tokenId, user); + await tokenContract.methods.repartition(2).send(); + + const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner)); + const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor)); + const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user)); + + await tokenContract.methods.transfer(receiver, 1).send(); + + const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner)); + expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore); + const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor)); + expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; + const userBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user)); + expect(userBalanceAfter).to.be.eq(userBalanceBefore); + }); + + itEth('[cross] Check that approve via EVM spend money from sponsor address', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const {collectionAddress, collectionId} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner,'Sponsor collection', '1', '1', ''); + const sponsor = await helper.eth.createAccountWithBalance(donor); + const sponsorCross = helper.ethCrossAccount.fromAddress(sponsor); + const receiver = await helper.eth.createAccountWithBalance(donor); + const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); + + await collectionEvm.methods.setCollectionSponsorCross(sponsorCross).send(); + + const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor); + await sponsorCollection.methods.confirmCollectionSponsorship().send(); + + const user = await helper.eth.createAccountWithBalance(donor); + const userCross = helper.ethCrossAccount.fromAddress(user); + await collectionEvm.methods.addCollectionAdminCross(userCross).send(); + + const userCollectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', user); + + const result = await userCollectionEvm.methods.mintWithTokenURI(user, 'Test URI').send(); + const tokenId = result.events.Transfer.returnValues.tokenId; + + const tokenContract = helper.ethNativeContract.rftTokenById(collectionId, tokenId, user); + await tokenContract.methods.repartition(2).send(); + + const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner)); + const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor)); + const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user)); + + await tokenContract.methods.approve(receiver, 1).send(); + + const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner)); + expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore); + const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor)); + expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; + const userBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user)); + expect(userBalanceAfter).to.be.eq(userBalanceBefore); + }); + + + itEth('[cross] Check that transferFrom via EVM spend money from sponsor address', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const {collectionAddress, collectionId} = await helper.eth.createERC721MetadataCompatibleRFTCollection(owner,'Sponsor collection', '1', '1', ''); + const sponsor = await helper.eth.createAccountWithBalance(donor); + const sponsorCross = helper.ethCrossAccount.fromAddress(sponsor); + const receiver = await helper.eth.createAccountWithBalance(donor); + const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); + + await collectionEvm.methods.setCollectionSponsorCross(sponsorCross).send(); + + const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor); + await sponsorCollection.methods.confirmCollectionSponsorship().send(); + + const user = await helper.eth.createAccountWithBalance(donor); + const userCross = helper.ethCrossAccount.fromAddress(user); + await collectionEvm.methods.addCollectionAdminCross(userCross).send(); + + const userCollectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', user); + + const result = await userCollectionEvm.methods.mintWithTokenURI(user, 'Test URI').send(); + const tokenId = result.events.Transfer.returnValues.tokenId; + + const tokenContract = helper.ethNativeContract.rftTokenById(collectionId, tokenId, user); + await tokenContract.methods.repartition(2).send(); + await tokenContract.methods.approve(receiver, 1).send(); + + const ownerBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner)); + const sponsorBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor)); + const userBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user)); + const receiverBalanceBefore = await helper.balance.getSubstrate(helper.address.ethToSubstrate(receiver)); + + const receiverTokenContract = helper.ethNativeContract.rftTokenById(collectionId, tokenId, receiver); + await receiverTokenContract.methods.transferFrom(user, receiver, 1).send(); + + const receiverBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(receiver)); + expect(receiverBalanceAfter).to.be.eq(receiverBalanceBefore); + const ownerBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(owner)); + expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore); + const sponsorBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(sponsor)); + expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; + const userBalanceAfter = await helper.balance.getSubstrate(helper.address.ethToSubstrate(user)); + expect(userBalanceAfter).to.be.eq(userBalanceBefore); + }); +}); -- gitstuff