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 '../util/index.js';26import {EthUniqueHelper} from '../util/playgrounds/unique.dev.js';27import type {CompiledContract} from '../util/playgrounds/types.js';28import {requirePalletsOrSkip, Pallets, makeNames} from '../../util/index.js';2930const {dirname} = makeNames(import.meta.url);3132let compiledFractionalizer: CompiledContract;3334const compileContract = async (helper: EthUniqueHelper): Promise<CompiledContract> => {35 if(!compiledFractionalizer) {36 compiledFractionalizer = await helper.ethContract.compile('Fractionalizer', (await readFile(`${dirname}/Fractionalizer.sol`)).toString(), [37 {solPath: 'api/CollectionHelpers.sol', fsPath: `${dirname}/../api/CollectionHelpers.sol`},38 {solPath: 'api/ContractHelpers.sol', fsPath: `${dirname}/../api/ContractHelpers.sol`},39 {solPath: 'api/UniqueRefungibleToken.sol', fsPath: `${dirname}/../api/UniqueRefungibleToken.sol`},40 {solPath: 'api/UniqueRefungible.sol', fsPath: `${dirname}/../api/UniqueRefungible.sol`},41 {solPath: 'api/UniqueNFT.sol', fsPath: `${dirname}/../api/UniqueNFT.sol`},42 ]);43 }44 return compiledFractionalizer;45};464748const deployContract = async (helper: EthUniqueHelper, owner: string): Promise<Contract> => {49 const compiled = await compileContract(helper);50 return await helper.ethContract.deployByAbi(owner, compiled.abi, compiled.object);51};525354const initContract = async (helper: EthUniqueHelper, owner: string): Promise<{contract: Contract, rftCollectionAddress: string}> => {55 const fractionalizer = await deployContract(helper, owner);56 const amount = 10n * helper.balance.getOneTokenNominal();57 const web3 = helper.getWeb3();58 await web3.eth.sendTransaction({from: owner, to: fractionalizer.options.address, value: `${amount}`, gas: helper.eth.DEFAULT_GAS});59 const result = await fractionalizer.methods.createAndSetRFTCollection('A', 'B', 'C').send({value: Number(2n * helper.balance.getOneTokenNominal())});60 const rftCollectionAddress = result.events.RFTCollectionSet.returnValues._collection;61 return {contract: fractionalizer, rftCollectionAddress};62};6364const mintRFTToken = async (helper: EthUniqueHelper, owner: string, fractionalizer: Contract, amount: bigint): Promise<{65 nftCollectionAddress: string, nftTokenId: number, rftTokenAddress: string66}> => {67 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');68 const nftContract = await helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);69 const mintResult = await nftContract.methods.mint(owner).send({from: owner});70 const nftTokenId = mintResult.events.Transfer.returnValues.tokenId;7172 await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner});73 await nftContract.methods.approve(fractionalizer.options.address, nftTokenId).send({from: owner});74 const result = await fractionalizer.methods.nft2rft(nftCollection.collectionAddress, nftTokenId, amount).send({from: owner});75 const {_collection, _tokenId, _rftToken} = result.events.Fractionalized.returnValues;76 return {77 nftCollectionAddress: _collection,78 nftTokenId: _tokenId,79 rftTokenAddress: _rftToken,80 };81};828384describe('Fractionalizer contract usage', () => {85 let donor: IKeyringPair;8687 before(async function() {88 await usingEthPlaygrounds(async (helper: EthUniqueHelper, privateKey) => {89 requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);90 donor = await privateKey({url: import.meta.url});91 });92 });9394 itEth('Set RFT collection', async ({helper}) => {95 const owner = await helper.eth.createAccountWithBalance(donor);96 const fractionalizer = await deployContract(helper, owner);97 const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');98 const rftContract = await helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner);99100 const fractionalizerAddressCross = helper.ethCrossAccount.fromAddress(fractionalizer.options.address);101 await rftContract.methods.addCollectionAdminCross(fractionalizerAddressCross).send({from: owner});102 const result = await fractionalizer.methods.setRFTCollection(rftCollection.collectionAddress).send({from: owner});103 expect(result.events).to.be.like({104 RFTCollectionSet: {105 returnValues: {106 _collection: rftCollection.collectionAddress,107 },108 },109 });110 });111112 itEth('Mint RFT collection', async ({helper}) => {113 const owner = await helper.eth.createAccountWithBalance(donor);114 const fractionalizer = await deployContract(helper, owner);115 await helper.balance.transferToSubstrate(donor, evmToAddress(fractionalizer.options.address), 10n * helper.balance.getOneTokenNominal());116117 const result = await fractionalizer.methods.createAndSetRFTCollection('A', 'B', 'C').send({from: owner, value: Number(2n * helper.balance.getOneTokenNominal())});118 expect(result.events).to.be.like({119 RFTCollectionSet: {},120 });121 expect(result.events.RFTCollectionSet.returnValues._collection).to.be.ok;122 });123124 itEth('Set Allowlist', async ({helper}) => {125 const owner = await helper.eth.createAccountWithBalance(donor);126 const {contract: fractionalizer} = await initContract(helper, owner);127 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');128129 const result1 = await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner});130 expect(result1.events).to.be.like({131 AllowListSet: {132 returnValues: {133 _collection: nftCollection.collectionAddress,134 _status: true,135 },136 },137 });138 const result2 = await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, false).send({from: owner});139 expect(result2.events).to.be.like({140 AllowListSet: {141 returnValues: {142 _collection: nftCollection.collectionAddress,143 _status: false,144 },145 },146 });147 });148149 itEth('NFT to RFT', async ({helper}) => {150 const owner = await helper.eth.createAccountWithBalance(donor);151152 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');153 const nftContract = await helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);154 const mintResult = await nftContract.methods.mint(owner).send({from: owner});155 const nftTokenId = mintResult.events.Transfer.returnValues.tokenId;156157 const {contract: fractionalizer} = await initContract(helper, owner);158159 await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner});160 await nftContract.methods.approve(fractionalizer.options.address, nftTokenId).send({from: owner});161 const result = await fractionalizer.methods.nft2rft(nftCollection.collectionAddress, nftTokenId, 100).send({from: owner});162 expect(result.events).to.be.like({163 Fractionalized: {164 returnValues: {165 _collection: nftCollection.collectionAddress,166 _tokenId: nftTokenId,167 _amount: '100',168 },169 },170 });171 const rftTokenAddress = result.events.Fractionalized.returnValues._rftToken;172173 174 const rftTokenContract = helper.ethNativeContract.rftToken(rftTokenAddress);175 expect(await rftTokenContract.methods.balanceOf(owner).call()).to.equal('100');176 });177178 itEth('RFT to NFT', async ({helper}) => {179 const owner = await helper.eth.createAccountWithBalance(donor);180181 const {contract: fractionalizer, rftCollectionAddress} = await initContract(helper, owner);182 const {rftTokenAddress, nftCollectionAddress, nftTokenId} = await mintRFTToken(helper, owner, fractionalizer, 100n);183184 const {collectionId, tokenId} = helper.ethAddress.extractTokenId(rftTokenAddress);185 const refungibleAddress = helper.ethAddress.fromCollectionId(collectionId);186 expect(rftCollectionAddress).to.be.equal(refungibleAddress);187 const refungibleTokenContract = await helper.ethNativeContract.rftToken(rftTokenAddress, owner);188 await refungibleTokenContract.methods.approve(fractionalizer.options.address, 100).send({from: owner});189 const result = await fractionalizer.methods.rft2nft(refungibleAddress, tokenId).send({from: owner});190 expect(result.events).to.be.like({191 Defractionalized: {192 returnValues: {193 _rftToken: rftTokenAddress,194 _nftCollection: nftCollectionAddress,195 _nftTokenId: nftTokenId,196 },197 },198 });199 });200201 itEth('Test fractionalizer NFT <-> RFT mapping ', async ({helper}) => {202 const owner = await helper.eth.createAccountWithBalance(donor);203204 const {contract: fractionalizer, rftCollectionAddress} = await initContract(helper, owner);205 const {rftTokenAddress, nftCollectionAddress, nftTokenId} = await mintRFTToken(helper, owner, fractionalizer, 100n);206207 const {collectionId, tokenId} = helper.ethAddress.extractTokenId(rftTokenAddress);208 const refungibleAddress = helper.ethAddress.fromCollectionId(collectionId);209 expect(rftCollectionAddress).to.be.equal(refungibleAddress);210 const refungibleTokenContract = await helper.ethNativeContract.rftToken(rftTokenAddress, owner);211 await refungibleTokenContract.methods.approve(fractionalizer.options.address, 100).send({from: owner});212213 const rft2nft = await fractionalizer.methods.rft2nftMapping(rftTokenAddress).call();214 expect(rft2nft).to.be.like({215 _collection: nftCollectionAddress,216 _tokenId: nftTokenId,217 });218219 const nft2rft = await fractionalizer.methods.nft2rftMapping(nftCollectionAddress, nftTokenId).call();220 expect(nft2rft).to.be.eq(tokenId.toString());221 });222});223224225226describe('Negative Integration Tests for fractionalizer', () => {227 let donor: IKeyringPair;228229 before(async function() {230 await usingEthPlaygrounds(async (helper: EthUniqueHelper, privateKey) => {231 requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);232 donor = await privateKey({url: import.meta.url});233 });234 });235236 itEth('call setRFTCollection twice', async ({helper}) => {237 const owner = await helper.eth.createAccountWithBalance(donor);238 const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');239 const refungibleContract = await helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner);240241 const fractionalizer = await deployContract(helper, owner);242 const fractionalizerAddressCross = helper.ethCrossAccount.fromAddress(fractionalizer.options.address);243 await refungibleContract.methods.addCollectionAdminCross(fractionalizerAddressCross).send({from: owner});244 await fractionalizer.methods.setRFTCollection(rftCollection.collectionAddress).send({from: owner});245246 await expect(fractionalizer.methods.setRFTCollection(rftCollection.collectionAddress).call())247 .to.be.rejectedWith(/RFT collection is already set$/g);248 });249250 itEth('call setRFTCollection with NFT collection', async ({helper}) => {251 const owner = await helper.eth.createAccountWithBalance(donor);252 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');253 const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);254255 const fractionalizer = await deployContract(helper, owner);256 const fractionalizerAddressCross = helper.ethCrossAccount.fromAddress(fractionalizer.options.address);257 await nftContract.methods.addCollectionAdminCross(fractionalizerAddressCross).send({from: owner});258259 await expect(fractionalizer.methods.setRFTCollection(nftCollection.collectionAddress).call())260 .to.be.rejectedWith(/Wrong collection type. Collection is not refungible.$/g);261 });262263 itEth('call setRFTCollection while not collection admin', async ({helper}) => {264 const owner = await helper.eth.createAccountWithBalance(donor);265 const fractionalizer = await deployContract(helper, owner);266 const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');267268 await expect(fractionalizer.methods.setRFTCollection(rftCollection.collectionAddress).call())269 .to.be.rejectedWith(/Fractionalizer contract should be an admin of the collection$/g);270 });271272 itEth('call setRFTCollection after createAndSetRFTCollection', async ({helper}) => {273 const owner = await helper.eth.createAccountWithBalance(donor);274 const fractionalizer = await deployContract(helper, owner);275 await helper.balance.transferToSubstrate(donor, evmToAddress(fractionalizer.options.address), 10n * helper.balance.getOneTokenNominal());276277 const result = await fractionalizer.methods.createAndSetRFTCollection('A', 'B', 'C').send({from: owner, value: Number(2n * helper.balance.getOneTokenNominal())});278 const collectionIdAddress = result.events.RFTCollectionSet.returnValues._collection;279280 await expect(fractionalizer.methods.setRFTCollection(collectionIdAddress).call())281 .to.be.rejectedWith(/RFT collection is already set$/g);282 });283284 itEth('call nft2rft without setting RFT collection for contract', async ({helper}) => {285 const owner = await helper.eth.createAccountWithBalance(donor);286287 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');288 const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);289 const mintResult = await nftContract.methods.mint(owner).send({from: owner});290 const nftTokenId = mintResult.events.Transfer.returnValues.tokenId;291292 const fractionalizer = await deployContract(helper, owner);293294 await expect(fractionalizer.methods.nft2rft(nftCollection.collectionAddress, nftTokenId, 100).call())295 .to.be.rejectedWith(/RFT collection is not set$/g);296 });297298 itEth('call nft2rft while not owner of NFT token', async ({helper}) => {299 const owner = await helper.eth.createAccountWithBalance(donor);300 const nftOwner = await helper.eth.createAccountWithBalance(donor);301302 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');303 const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);304 const mintResult = await nftContract.methods.mint(owner).send({from: owner});305 const nftTokenId = mintResult.events.Transfer.returnValues.tokenId;306 await nftContract.methods.transfer(nftOwner, 1).send({from: owner});307308309 const {contract: fractionalizer} = await initContract(helper, owner);310 await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner});311312 await expect(fractionalizer.methods.nft2rft(nftCollection.collectionAddress, nftTokenId, 100).call({from: owner}))313 .to.be.rejectedWith(/Only token owner could fractionalize it$/g);314 });315316 itEth('call nft2rft while not in list of allowed accounts', async ({helper}) => {317 const owner = await helper.eth.createAccountWithBalance(donor);318319 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');320 const nftContract = helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);321 const mintResult = await nftContract.methods.mint(owner).send({from: owner});322 const nftTokenId = mintResult.events.Transfer.returnValues.tokenId;323324 const {contract: fractionalizer} = await initContract(helper, owner);325326 await nftContract.methods.approve(fractionalizer.options.address, nftTokenId).send({from: owner});327 await expect(fractionalizer.methods.nft2rft(nftCollection.collectionAddress, nftTokenId, 100).call())328 .to.be.rejectedWith(/Fractionalization of this collection is not allowed by admin$/g);329 });330331 itEth('call nft2rft while fractionalizer doesnt have approval for nft token', async ({helper}) => {332 const owner = await helper.eth.createAccountWithBalance(donor);333334 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');335 const nftContract = await helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);336 const mintResult = await nftContract.methods.mint(owner).send({from: owner});337 const nftTokenId = mintResult.events.Transfer.returnValues.tokenId;338339 const {contract: fractionalizer} = await initContract(helper, owner);340341 await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner});342 await expect(fractionalizer.methods.nft2rft(nftCollection.collectionAddress, nftTokenId, 100).call())343 .to.be.rejectedWith(/ApprovedValueTooLow$/g);344 });345346 itEth('call rft2nft without setting RFT collection for contract', async ({helper}) => {347 const owner = await helper.eth.createAccountWithBalance(donor);348349 const fractionalizer = await deployContract(helper, owner);350 const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');351 const refungibleContract = await helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner);352 const mintResult = await refungibleContract.methods.mint(owner).send({from: owner});353 const rftTokenId = mintResult.events.Transfer.returnValues.tokenId;354355 await expect(fractionalizer.methods.rft2nft(rftCollection.collectionAddress, rftTokenId).call({from: owner}))356 .to.be.rejectedWith(/RFT collection is not set$/g);357 });358359 itEth('call rft2nft for RFT token that is not from configured RFT collection', async ({helper}) => {360 const owner = await helper.eth.createAccountWithBalance(donor);361362 const {contract: fractionalizer} = await initContract(helper, owner);363 const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');364 const refungibleContract = helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner);365 const mintResult = await refungibleContract.methods.mint(owner).send({from: owner});366 const rftTokenId = mintResult.events.Transfer.returnValues.tokenId;367368 await expect(fractionalizer.methods.rft2nft(rftCollection.collectionAddress, rftTokenId).call())369 .to.be.rejectedWith(/Wrong RFT collection$/g);370 });371372 itEth('call rft2nft for RFT token that was not minted by fractionalizer contract', async ({helper}) => {373 const owner = await helper.eth.createAccountWithBalance(donor);374 const rftCollection = await helper.eth.createRFTCollection(owner, 'rft', 'RFT collection', 'RFT');375 const refungibleContract = helper.ethNativeContract.collection(rftCollection.collectionAddress, 'rft', owner);376377 const fractionalizer = await deployContract(helper, owner);378379 const fractionalizerAddressCross = helper.ethCrossAccount.fromAddress(fractionalizer.options.address);380 await refungibleContract.methods.addCollectionAdminCross(fractionalizerAddressCross).send({from: owner});381 await fractionalizer.methods.setRFTCollection(rftCollection.collectionAddress).send({from: owner});382383 const mintResult = await refungibleContract.methods.mint(owner).send({from: owner});384 const rftTokenId = mintResult.events.Transfer.returnValues.tokenId;385386 await expect(fractionalizer.methods.rft2nft(rftCollection.collectionAddress, rftTokenId).call())387 .to.be.rejectedWith(/No corresponding NFT token found$/g);388 });389390 itEth('call rft2nft without owning all RFT pieces', async ({helper}) => {391 const owner = await helper.eth.createAccountWithBalance(donor);392 const receiver = await helper.eth.createAccountWithBalance(donor);393394 const {contract: fractionalizer, rftCollectionAddress} = await initContract(helper, owner);395 const {rftTokenAddress} = await mintRFTToken(helper, owner, fractionalizer, 100n);396397 const {tokenId} = helper.ethAddress.extractTokenId(rftTokenAddress);398 const refungibleTokenContract = helper.ethNativeContract.rftToken(rftTokenAddress, owner);399 await refungibleTokenContract.methods.transfer(receiver, 50).send({from: owner});400 await refungibleTokenContract.methods.approve(fractionalizer.options.address, 50).send({from: receiver});401 await expect(fractionalizer.methods.rft2nft(rftCollectionAddress, tokenId).call({from: receiver}))402 .to.be.rejectedWith(/Not all pieces are owned by the caller$/g);403 });404405 itEth('send QTZ/UNQ to contract from non owner', async ({helper}) => {406 const owner = await helper.eth.createAccountWithBalance(donor);407 const payer = await helper.eth.createAccountWithBalance(donor);408409 const fractionalizer = await deployContract(helper, owner);410 const amount = 10n * helper.balance.getOneTokenNominal();411 const web3 = helper.getWeb3();412 await expect(web3.eth.sendTransaction({from: payer, to: fractionalizer.options.address, value: `${amount}`, gas: helper.eth.DEFAULT_GAS})).to.be.rejected;413 });414415 itEth('fractionalize NFT with NFT transfers disallowed', async ({helper}) => {416 const nftCollection = await helper.nft.mintCollection(donor, {name: 'A', description: 'B', tokenPrefix: 'C'});417418 const owner = await helper.eth.createAccountWithBalance(donor);419 const nftToken = await nftCollection.mintToken(donor, {Ethereum: owner});420 await helper.executeExtrinsic(donor, 'api.tx.unique.setTransfersEnabledFlag', [nftCollection.collectionId, false], true);421 const nftCollectionAddress = helper.ethAddress.fromCollectionId(nftCollection.collectionId);422 const {contract: fractionalizer} = await initContract(helper, owner);423 await fractionalizer.methods.setNftCollectionIsAllowed(nftCollectionAddress, true).send({from: owner});424425 const nftContract = await helper.ethNativeContract.collection(nftCollectionAddress, 'nft', owner);426 await nftContract.methods.approve(fractionalizer.options.address, nftToken.tokenId).send({from: owner});427 await expect(fractionalizer.methods.nft2rft(nftCollectionAddress, nftToken.tokenId, 100).call())428 .to.be.rejectedWith(/TransferNotAllowed$/g);429 });430431 itEth('fractionalize NFT with RFT transfers disallowed', async ({helper}) => {432 const owner = await helper.eth.createAccountWithBalance(donor);433434 const rftCollection = await helper.rft.mintCollection(donor, {name: 'A', description: 'B', tokenPrefix: 'C'});435 const rftCollectionAddress = helper.ethAddress.fromCollectionId(rftCollection.collectionId);436 const fractionalizer = await deployContract(helper, owner);437 await rftCollection.addAdmin(donor, {Ethereum: fractionalizer.options.address});438439 await fractionalizer.methods.setRFTCollection(rftCollectionAddress).send({from: owner});440 await helper.executeExtrinsic(donor, 'api.tx.unique.setTransfersEnabledFlag', [rftCollection.collectionId, false], true);441442 const nftCollection = await helper.eth.createNFTCollection(owner, 'nft', 'NFT collection', 'NFT');443 const nftContract = await helper.ethNativeContract.collection(nftCollection.collectionAddress, 'nft', owner);444 const mintResult = await nftContract.methods.mint(owner).send({from: owner});445 const nftTokenId = mintResult.events.Transfer.returnValues.tokenId;446447 await fractionalizer.methods.setNftCollectionIsAllowed(nftCollection.collectionAddress, true).send({from: owner});448 await nftContract.methods.approve(fractionalizer.options.address, nftTokenId).send({from: owner});449450 await expect(fractionalizer.methods.nft2rft(nftCollection.collectionAddress, nftTokenId, 100n).call())451 .to.be.rejectedWith(/TransferNotAllowed$/g);452 });453});