123456789101112131415161718import {readFile} from 'fs/promises';1920import type {IKeyringPair} from '@polkadot/types/types';21import {evmToAddress} from '@polkadot/util-crypto';2223import {Contract} from 'web3-eth-contract';2425import {usingEthPlaygrounds, expect, itEth} from '@unique/test-utils/eth/util.js';26import {EthUniqueHelper} from '@unique/test-utils/eth/index.js';27import type {CompiledContract} from '@unique/test-utils/eth/types.js';28import {requirePalletsOrSkip, Pallets, makeNames} from '@unique/test-utils/util.js';2930const {dirname} = makeNames(import.meta.url);3132let compiledFractionalizer: CompiledContract;3334const EVM_ABI_DIR = `${dirname}/../../../evm-abi`;3536const compileContract = async (helper: EthUniqueHelper): Promise<CompiledContract> => {37 if(!compiledFractionalizer) {38 compiledFractionalizer = await helper.ethContract.compile('Fractionalizer', (await readFile(`${dirname}/Fractionalizer.sol`)).toString(), [39 {solPath: 'api/CollectionHelpers.sol', fsPath: `${EVM_ABI_DIR}/api/CollectionHelpers.sol`},40 {solPath: 'api/ContractHelpers.sol', fsPath: `${EVM_ABI_DIR}/api/ContractHelpers.sol`},41 {solPath: 'api/UniqueRefungibleToken.sol', fsPath: `${EVM_ABI_DIR}/api/UniqueRefungibleToken.sol`},42 {solPath: 'api/UniqueRefungible.sol', fsPath: `${EVM_ABI_DIR}/api/UniqueRefungible.sol`},43 {solPath: 'api/UniqueNFT.sol', fsPath: `${EVM_ABI_DIR}/api/UniqueNFT.sol`},44 ]);45 }46 return compiledFractionalizer;47};484950const deployContract = async (helper: EthUniqueHelper, owner: string): Promise<Contract> => {51 const compiled = await compileContract(helper);52 return await helper.ethContract.deployByAbi(owner, compiled.abi, compiled.object);53};545556const initContract = async (helper: EthUniqueHelper, owner: string): Promise<{contract: Contract, rftCollectionAddress: string}> => {57 const fractionalizer = await deployContract(helper, owner);58 const amount = 10n * helper.balance.getOneTokenNominal();59 const web3 = helper.getWeb3();60 await web3.eth.sendTransaction({from: owner, to: fractionalizer.options.address, value: `${amount}`, gas: helper.eth.DEFAULT_GAS});61 const result = await fractionalizer.methods.createAndSetRFTCollection('A', 'B', 'C').send({value: Number(2n * helper.balance.getOneTokenNominal())});62 const rftCollectionAddress = result.events.RFTCollectionSet.returnValues._collection;63 return {contract: fractionalizer, rftCollectionAddress};64};6566const mintRFTToken = async (helper: EthUniqueHelper, owner: string, fractionalizer: Contract, amount: bigint): Promise<{67 nftCollectionAddress: string, nftTokenId: number, rftTokenAddress: string68}> => {69 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');70 const nftContract = await helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);71 const mintResult = await nftContract.methods.mint(owner).send({from: owner});72 const nftTokenId = mintResult.events.Transfer.returnValues.tokenId;7374 await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner});75 await nftContract.methods.approve(fractionalizer.options.address, nftTokenId).send({from: owner});76 const result = await fractionalizer.methods.nft2rft(nftCollection.collectionAddress, nftTokenId, amount).send({from: owner});77 const {_collection, _tokenId, _rftToken} = result.events.Fractionalized.returnValues;78 return {79 nftCollectionAddress: _collection,80 nftTokenId: _tokenId,81 rftTokenAddress: _rftToken,82 };83};848586describe('Fractionalizer contract usage', () => {87 let donor: IKeyringPair;8889 before(async function() {90 await usingEthPlaygrounds(async (helper: EthUniqueHelper, privateKey) => {91 requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);92 donor = await privateKey({url: import.meta.url});93 });94 });9596 itEth('Set RFT collection', async ({helper}) => {97 const owner = await helper.eth.createAccountWithBalance(donor);98 const fractionalizer = await deployContract(helper, owner);99 const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');100 const rftContract = await helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner);101102 const fractionalizerAddressCross = helper.ethCrossAccount.fromAddress(fractionalizer.options.address);103 await rftContract.methods.addCollectionAdminCross(fractionalizerAddressCross).send({from: owner});104 const result = await fractionalizer.methods.setRFTCollection(rftCollection.collectionAddress).send({from: owner});105 expect(result.events).to.be.like({106 RFTCollectionSet: {107 returnValues: {108 _collection: rftCollection.collectionAddress,109 },110 },111 });112 });113114 itEth('Mint RFT collection', async ({helper}) => {115 const owner = await helper.eth.createAccountWithBalance(donor);116 const fractionalizer = await deployContract(helper, owner);117 await helper.balance.transferToSubstrate(donor, evmToAddress(fractionalizer.options.address), 10n * helper.balance.getOneTokenNominal());118119 const result = await fractionalizer.methods.createAndSetRFTCollection('A', 'B', 'C').send({from: owner, value: Number(2n * helper.balance.getOneTokenNominal())});120 expect(result.events).to.be.like({121 RFTCollectionSet: {},122 });123 expect(result.events.RFTCollectionSet.returnValues._collection).to.be.ok;124 });125126 itEth('Set Allowlist', async ({helper}) => {127 const owner = await helper.eth.createAccountWithBalance(donor);128 const {contract: fractionalizer} = await initContract(helper, owner);129 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');130131 const result1 = await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner});132 expect(result1.events).to.be.like({133 AllowListSet: {134 returnValues: {135 _collection: nftCollection.collectionAddress,136 _status: true,137 },138 },139 });140 const result2 = await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, false).send({from: owner});141 expect(result2.events).to.be.like({142 AllowListSet: {143 returnValues: {144 _collection: nftCollection.collectionAddress,145 _status: false,146 },147 },148 });149 });150151 itEth('NFT to RFT', async ({helper}) => {152 const owner = await helper.eth.createAccountWithBalance(donor);153154 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');155 const nftContract = await helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);156 const mintResult = await nftContract.methods.mint(owner).send({from: owner});157 const nftTokenId = mintResult.events.Transfer.returnValues.tokenId;158159 const {contract: fractionalizer} = await initContract(helper, owner);160161 await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner});162 await nftContract.methods.approve(fractionalizer.options.address, nftTokenId).send({from: owner});163 const result = await fractionalizer.methods.nft2rft(nftCollection.collectionAddress, nftTokenId, 100).send({from: owner});164 expect(result.events).to.be.like({165 Fractionalized: {166 returnValues: {167 _collection: nftCollection.collectionAddress,168 _tokenId: nftTokenId,169 _amount: '100',170 },171 },172 });173 const rftTokenAddress = result.events.Fractionalized.returnValues._rftToken;174175 176 const rftTokenContract = helper.ethNativeContract.rftToken(rftTokenAddress);177 expect(await rftTokenContract.methods.balanceOf(owner).call()).to.equal('100');178 });179180 itEth('RFT to NFT', async ({helper}) => {181 const owner = await helper.eth.createAccountWithBalance(donor);182183 const {contract: fractionalizer, rftCollectionAddress} = await initContract(helper, owner);184 const {rftTokenAddress, nftCollectionAddress, nftTokenId} = await mintRFTToken(helper, owner, fractionalizer, 100n);185186 const {collectionId, tokenId} = helper.ethAddress.extractTokenId(rftTokenAddress);187 const refungibleAddress = helper.ethAddress.fromCollectionId(collectionId);188 expect(rftCollectionAddress).to.be.equal(refungibleAddress);189 const refungibleTokenContract = await helper.ethNativeContract.rftToken(rftTokenAddress, owner);190 await refungibleTokenContract.methods.approve(fractionalizer.options.address, 100).send({from: owner});191 const result = await fractionalizer.methods.rft2nft(refungibleAddress, tokenId).send({from: owner});192 expect(result.events).to.be.like({193 Defractionalized: {194 returnValues: {195 _rftToken: rftTokenAddress,196 _nftCollection: nftCollectionAddress,197 _nftTokenId: nftTokenId,198 },199 },200 });201 });202203 itEth('Test fractionalizer NFT <-> RFT mapping ', async ({helper}) => {204 const owner = await helper.eth.createAccountWithBalance(donor);205206 const {contract: fractionalizer, rftCollectionAddress} = await initContract(helper, owner);207 const {rftTokenAddress, nftCollectionAddress, nftTokenId} = await mintRFTToken(helper, owner, fractionalizer, 100n);208209 const {collectionId, tokenId} = helper.ethAddress.extractTokenId(rftTokenAddress);210 const refungibleAddress = helper.ethAddress.fromCollectionId(collectionId);211 expect(rftCollectionAddress).to.be.equal(refungibleAddress);212 const refungibleTokenContract = await helper.ethNativeContract.rftToken(rftTokenAddress, owner);213 await refungibleTokenContract.methods.approve(fractionalizer.options.address, 100).send({from: owner});214215 const rft2nft = await fractionalizer.methods.rft2nftMapping(rftTokenAddress).call();216 expect(rft2nft).to.be.like({217 _collection: nftCollectionAddress,218 _tokenId: nftTokenId,219 });220221 const nft2rft = await fractionalizer.methods.nft2rftMapping(nftCollectionAddress, nftTokenId).call();222 expect(nft2rft).to.be.eq(tokenId.toString());223 });224});225226227228describe('Negative Integration Tests for fractionalizer', () => {229 let donor: IKeyringPair;230231 before(async function() {232 await usingEthPlaygrounds(async (helper: EthUniqueHelper, privateKey) => {233 requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);234 donor = await privateKey({url: import.meta.url});235 });236 });237238 itEth('call setRFTCollection twice', async ({helper}) => {239 const owner = await helper.eth.createAccountWithBalance(donor);240 const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');241 const refungibleContract = await helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner);242243 const fractionalizer = await deployContract(helper, owner);244 const fractionalizerAddressCross = helper.ethCrossAccount.fromAddress(fractionalizer.options.address);245 await refungibleContract.methods.addCollectionAdminCross(fractionalizerAddressCross).send({from: owner});246 await fractionalizer.methods.setRFTCollection(rftCollection.collectionAddress).send({from: owner});247248 await expect(fractionalizer.methods.setRFTCollection(rftCollection.collectionAddress).call())249 .to.be.rejectedWith(/RFT collection is already set$/g);250 });251252 itEth('call setRFTCollection with NFT collection', async ({helper}) => {253 const owner = await helper.eth.createAccountWithBalance(donor);254 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');255 const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);256257 const fractionalizer = await deployContract(helper, owner);258 const fractionalizerAddressCross = helper.ethCrossAccount.fromAddress(fractionalizer.options.address);259 await nftContract.methods.addCollectionAdminCross(fractionalizerAddressCross).send({from: owner});260261 await expect(fractionalizer.methods.setRFTCollection(nftCollection.collectionAddress).call())262 .to.be.rejectedWith(/Wrong collection type. Collection is not refungible.$/g);263 });264265 itEth('call setRFTCollection while not collection admin', async ({helper}) => {266 const owner = await helper.eth.createAccountWithBalance(donor);267 const fractionalizer = await deployContract(helper, owner);268 const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');269270 await expect(fractionalizer.methods.setRFTCollection(rftCollection.collectionAddress).call())271 .to.be.rejectedWith(/Fractionalizer contract should be an admin of the collection$/g);272 });273274 itEth('call setRFTCollection after createAndSetRFTCollection', async ({helper}) => {275 const owner = await helper.eth.createAccountWithBalance(donor);276 const fractionalizer = await deployContract(helper, owner);277 await helper.balance.transferToSubstrate(donor, evmToAddress(fractionalizer.options.address), 10n * helper.balance.getOneTokenNominal());278279 const result = await fractionalizer.methods.createAndSetRFTCollection('A', 'B', 'C').send({from: owner, value: Number(2n * helper.balance.getOneTokenNominal())});280 const collectionIdAddress = result.events.RFTCollectionSet.returnValues._collection;281282 await expect(fractionalizer.methods.setRFTCollection(collectionIdAddress).call())283 .to.be.rejectedWith(/RFT collection is already set$/g);284 });285286 itEth('call nft2rft without setting RFT collection for contract', async ({helper}) => {287 const owner = await helper.eth.createAccountWithBalance(donor);288289 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');290 const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);291 const mintResult = await nftContract.methods.mint(owner).send({from: owner});292 const nftTokenId = mintResult.events.Transfer.returnValues.tokenId;293294 const fractionalizer = await deployContract(helper, owner);295296 await expect(fractionalizer.methods.nft2rft(nftCollection.collectionAddress, nftTokenId, 100).call())297 .to.be.rejectedWith(/RFT collection is not set$/g);298 });299300 itEth('call nft2rft while not owner of NFT token', async ({helper}) => {301 const owner = await helper.eth.createAccountWithBalance(donor);302 const nftOwner = await helper.eth.createAccountWithBalance(donor);303304 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');305 const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);306 const mintResult = await nftContract.methods.mint(owner).send({from: owner});307 const nftTokenId = mintResult.events.Transfer.returnValues.tokenId;308 await nftContract.methods.transfer(nftOwner, 1).send({from: owner});309310311 const {contract: fractionalizer} = await initContract(helper, owner);312 await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner});313314 await expect(fractionalizer.methods.nft2rft(nftCollection.collectionAddress, nftTokenId, 100).call({from: owner}))315 .to.be.rejectedWith(/Only token owner could fractionalize it$/g);316 });317318 itEth('call nft2rft while not in list of allowed accounts', async ({helper}) => {319 const owner = await helper.eth.createAccountWithBalance(donor);320321 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');322 const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);323 const mintResult = await nftContract.methods.mint(owner).send({from: owner});324 const nftTokenId = mintResult.events.Transfer.returnValues.tokenId;325326 const {contract: fractionalizer} = await initContract(helper, owner);327328 await nftContract.methods.approve(fractionalizer.options.address, nftTokenId).send({from: owner});329 await expect(fractionalizer.methods.nft2rft(nftCollection.collectionAddress, nftTokenId, 100).call())330 .to.be.rejectedWith(/Fractionalization of this collection is not allowed by admin$/g);331 });332333 itEth('call nft2rft while fractionalizer doesnt have approval for nft token', async ({helper}) => {334 const owner = await helper.eth.createAccountWithBalance(donor);335336 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');337 const nftContract = await helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);338 const mintResult = await nftContract.methods.mint(owner).send({from: owner});339 const nftTokenId = mintResult.events.Transfer.returnValues.tokenId;340341 const {contract: fractionalizer} = await initContract(helper, owner);342343 await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner});344 await expect(fractionalizer.methods.nft2rft(nftCollection.collectionAddress, nftTokenId, 100).call())345 .to.be.rejectedWith(/ApprovedValueTooLow$/g);346 });347348 itEth('call rft2nft without setting RFT collection for contract', async ({helper}) => {349 const owner = await helper.eth.createAccountWithBalance(donor);350351 const fractionalizer = await deployContract(helper, owner);352 const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');353 const refungibleContract = await helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner);354 const mintResult = await refungibleContract.methods.mint(owner).send({from: owner});355 const rftTokenId = mintResult.events.Transfer.returnValues.tokenId;356357 await expect(fractionalizer.methods.rft2nft(rftCollection.collectionAddress, rftTokenId).call({from: owner}))358 .to.be.rejectedWith(/RFT collection is not set$/g);359 });360361 itEth('call rft2nft for RFT token that is not from configured RFT collection', async ({helper}) => {362 const owner = await helper.eth.createAccountWithBalance(donor);363364 const {contract: fractionalizer} = await initContract(helper, owner);365 const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');366 const refungibleContract = helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner);367 const mintResult = await refungibleContract.methods.mint(owner).send({from: owner});368 const rftTokenId = mintResult.events.Transfer.returnValues.tokenId;369370 await expect(fractionalizer.methods.rft2nft(rftCollection.collectionAddress, rftTokenId).call())371 .to.be.rejectedWith(/Wrong RFT collection$/g);372 });373374 itEth('call rft2nft for RFT token that was not minted by fractionalizer contract', async ({helper}) => {375 const owner = await helper.eth.createAccountWithBalance(donor);376 const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');377 const refungibleContract = helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner);378379 const fractionalizer = await deployContract(helper, owner);380381 const fractionalizerAddressCross = helper.ethCrossAccount.fromAddress(fractionalizer.options.address);382 await refungibleContract.methods.addCollectionAdminCross(fractionalizerAddressCross).send({from: owner});383 await fractionalizer.methods.setRFTCollection(rftCollection.collectionAddress).send({from: owner});384385 const mintResult = await refungibleContract.methods.mint(owner).send({from: owner});386 const rftTokenId = mintResult.events.Transfer.returnValues.tokenId;387388 await expect(fractionalizer.methods.rft2nft(rftCollection.collectionAddress, rftTokenId).call())389 .to.be.rejectedWith(/No corresponding NFT token found$/g);390 });391392 itEth('call rft2nft without owning all RFT pieces', async ({helper}) => {393 const owner = await helper.eth.createAccountWithBalance(donor);394 const receiver = await helper.eth.createAccountWithBalance(donor);395396 const {contract: fractionalizer, rftCollectionAddress} = await initContract(helper, owner);397 const {rftTokenAddress} = await mintRFTToken(helper, owner, fractionalizer, 100n);398399 const {tokenId} = helper.ethAddress.extractTokenId(rftTokenAddress);400 const refungibleTokenContract = helper.ethNativeContract.rftToken(rftTokenAddress, owner);401 await refungibleTokenContract.methods.transfer(receiver, 50).send({from: owner});402 await refungibleTokenContract.methods.approve(fractionalizer.options.address, 50).send({from: receiver});403 await expect(fractionalizer.methods.rft2nft(rftCollectionAddress, tokenId).call({from: receiver}))404 .to.be.rejectedWith(/Not all pieces are owned by the caller$/g);405 });406407 itEth('send QTZ/UNQ to contract from non owner', async ({helper}) => {408 const owner = await helper.eth.createAccountWithBalance(donor);409 const payer = await helper.eth.createAccountWithBalance(donor);410411 const fractionalizer = await deployContract(helper, owner);412 const amount = 10n * helper.balance.getOneTokenNominal();413 const web3 = helper.getWeb3();414 await expect(web3.eth.sendTransaction({from: payer, to: fractionalizer.options.address, value: `${amount}`, gas: helper.eth.DEFAULT_GAS})).to.be.rejected;415 });416417 itEth('fractionalize NFT with NFT transfers disallowed', async ({helper}) => {418 const nftCollection = await helper.nft.mintCollection(donor, {name: 'A', description: 'B', tokenPrefix: 'C'});419420 const owner = await helper.eth.createAccountWithBalance(donor);421 const nftToken = await nftCollection.mintToken(donor, {Ethereum: owner});422 await helper.executeExtrinsic(donor, 'api.tx.unique.setTransfersEnabledFlag', [nftCollection.collectionId, false], true);423 const nftCollectionAddress = helper.ethAddress.fromCollectionId(nftCollection.collectionId);424 const {contract: fractionalizer} = await initContract(helper, owner);425 await fractionalizer.methods.setNftCollectionIsAllowed(nftCollectionAddress, true).send({from: owner});426427 const nftContract = await helper.ethNativeContract.collection(nftCollectionAddress, 'nft', owner);428 await nftContract.methods.approve(fractionalizer.options.address, nftToken.tokenId).send({from: owner});429 await expect(fractionalizer.methods.nft2rft(nftCollectionAddress, nftToken.tokenId, 100).call())430 .to.be.rejectedWith(/TransferNotAllowed$/g);431 });432433 itEth('fractionalize NFT with RFT transfers disallowed', async ({helper}) => {434 const owner = await helper.eth.createAccountWithBalance(donor);435436 const rftCollection = await helper.rft.mintCollection(donor, {name: 'A', description: 'B', tokenPrefix: 'C'});437 const rftCollectionAddress = helper.ethAddress.fromCollectionId(rftCollection.collectionId);438 const fractionalizer = await deployContract(helper, owner);439 await rftCollection.addAdmin(donor, {Ethereum: fractionalizer.options.address});440441 await fractionalizer.methods.setRFTCollection(rftCollectionAddress).send({from: owner});442 await helper.executeExtrinsic(donor, 'api.tx.unique.setTransfersEnabledFlag', [rftCollection.collectionId, false], true);443444 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');445 const nftContract = await helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);446 const mintResult = await nftContract.methods.mint(owner).send({from: owner});447 const nftTokenId = mintResult.events.Transfer.returnValues.tokenId;448449 await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner});450 await nftContract.methods.approve(fractionalizer.options.address, nftTokenId).send({from: owner});451452 await expect(fractionalizer.methods.nft2rft(nftCollection.collectionAddress, nftTokenId, 100n).call())453 .to.be.rejectedWith(/TransferNotAllowed$/g);454 });455});