git.delta.rocks / unique-network / refs/commits / 70496198518d

difftreelog

feat add test for market v2 deployment

Grigoriy Simonov2023-05-30parent: #cf59f49.patch.diff
in: master

8 files changed

modifiedtests/package.jsondiffbeforeafterboth
before · tests/package.json
1{2  "name": "unique-tests",3  "version": "1.0.0",4  "description": "Unique Chain Tests",5  "main": "",6  "devDependencies": {7    "@polkadot/typegen": "10.7.2",8    "@types/chai": "^4.3.3",9    "@types/chai-as-promised": "^7.1.5",10    "@types/chai-like": "^1.1.1",11    "@types/chai-subset": "^1.3.3",12    "@types/mocha": "^10.0.0",13    "@types/node": "^20.2.3",14    "@typescript-eslint/eslint-plugin": "^5.47.0",15    "@typescript-eslint/parser": "^5.47.0",16    "chai": "^4.3.6",17    "chai-subset": "^1.6.0",18    "eslint": "^8.25.0",19    "eslint-plugin-mocha": "^10.1.0",20    "mocha": "^10.1.0",21    "ts-node": "^10.9.1",22    "typescript": "^5.0.4"23  },24  "mocha": {25    "timeout": 9999999,26    "require": [27      "ts-node/register"28    ]29  },30  "scripts": {31    "lint": "eslint --ext .ts,.js src/",32    "fix": "yarn lint --fix",33    "setup": "ts-node --esm ./src/util/globalSetup.ts",34    "setIdentities": "ts-node ./src/util/identitySetter.ts",35    "checkRelayIdentities": "ts-node ./src/util/relayIdentitiesChecker.ts",36    "_test": "yarn setup && mocha --timeout 9999999 --loader=ts-node/esm.mjs",37    "_testParallel": "yarn setup && mocha --timeout 9999999 --parallel --loader=ts-node/esm.mjs",38    "test": "yarn _test './src/**/*.*test.ts'",39    "testParallelFull": "yarn testParallel && yarn testSequential",40    "testParallel": "yarn _testParallel './src/**/*.test.ts'",41    "testSequential": "yarn _test './src/**/*.seqtest.ts'",42    "testStructure": "yarn _test ./**/nesting/*.*test.ts",43    "testEth": "yarn _test './**/eth/**/*.*test.ts'",44    "testEthNesting": "yarn _test './**/eth/nesting/**/*.*test.ts'",45    "testEthFractionalizer": "yarn _test './**/eth/fractionalizer/**/*.*test.ts'",46    "testEthMarketplace": "yarn _test './**/eth/marketplace/**/*.*test.ts'",47    "testSub": "yarn _test './**/sub/**/*.*test.ts'",48    "testSubNesting": "yarn _test './**/sub/nesting/**/*.*test.ts'",49    "testEvent": "yarn _test ./src/check-event/*.*test.ts",50    "testEthPayable": "yarn _test './**/eth/payable.test.ts'",51    "testEvmCoder": "yarn _test './**/eth/evmCoder.test.ts'",52    "testNesting": "yarn _test ./**/nest.test.ts",53    "testUnnesting": "yarn _test ./**/unnest.test.ts",54    "testProperties": "yarn _test ./**/collectionProperties.*test.ts ./**/tokenProperties.*test.ts ./**/getPropertiesRpc.test.ts",55    "testCollectionProperties": "yarn _test ./**/collectionProperties.*test.ts",56    "testTokenProperties": "yarn _test ./**/tokenProperties.*test.ts",57    "testMigration": "yarn _test ./**/nesting/migration-check.test.ts",58    "testAddCollectionAdmin": "yarn _test ./**/addCollectionAdmin.test.ts",59    "testSetCollectionLimits": "yarn _test ./**/setCollectionLimits.test.ts",60    "testChangeCollectionOwner": "yarn _test ./**/change-collection-owner.test.ts",61    "testSetCollectionSponsor": "yarn _test ./**/setCollectionSponsor.test.ts",62    "testConfirmSponsorship": "yarn _test ./**/confirmSponsorship.test.ts",63    "testRemoveCollectionAdmin": "yarn _test ./**/removeCollectionAdmin.test.ts",64    "testRemoveCollectionSponsor": "yarn _test ./**/removeCollectionSponsor.test.ts",65    "testAllowLists": "yarn _test ./**/allowLists.test.ts",66    "testConnection": "yarn _test ./**/connection.test.ts",67    "testContracts": "yarn _test ./**/contracts.test.ts",68    "testCreateItem": "yarn _test ./**/createItem.test.ts",69    "testCreateMultipleItems": "yarn _test ./**/createMultipleItems.test.ts",70    "testCreateMultipleItemsEx": "yarn _test ./**/createMultipleItemsEx.test.ts",71    "testApprove": "yarn _test ./**/approve.test.ts",72    "testTransferFrom": "yarn _test ./**/transferFrom.test.ts",73    "testCreateCollection": "yarn _test ./**/createCollection.test.ts",74    "testDestroyCollection": "yarn _test ./**/destroyCollection.test.ts",75    "testToggleContractAllowList": "yarn _test ./**/toggleContractAllowList.test.ts",76    "testAddToContractAllowList": "yarn _test ./**/addToContractAllowList.test.ts",77    "testTransfer": "yarn _test ./**/transfer.test.ts",78    "testBurnItem": "yarn _test ./**/burnItem.test.ts",79    "testAdminTransferAndBurn": "yarn _test ./**/adminTransferAndBurn.test.ts",80    "testSetPermissions": "yarn _test ./**/setPermissions.test.ts",81    "testCreditFeesToTreasury": "yarn _test ./**/creditFeesToTreasury.seqtest.ts",82    "testContractSponsoring": "yarn _test ./**/eth/contractSponsoring.test.ts",83    "testEnableContractSponsoring": "yarn _test ./**/enableContractSponsoring.test.ts",84    "testRemoveFromContractAllowList": "yarn _test ./**/removeFromContractAllowList.test.ts",85    "testSetContractSponsoringRateLimit": "yarn _test ./**/setContractSponsoringRateLimit.test.ts",86    "testSetOffchainSchema": "yarn _test ./**/setOffchainSchema.test.ts",87    "testNextSponsoring": "yarn _test ./**/nextSponsoring.test.ts",88    "testOverflow": "yarn _test ./**/overflow.test.ts",89    "testMaintenance": "yarn _test ./**/maintenance.seqtest.ts",90    "testInflation": "yarn _test ./**/inflation.seqtest.ts",91    "testScheduler": "yarn _test ./**/scheduler.seqtest.ts",92    "testSchedulingEVM": "yarn _test ./**/eth/scheduling.test.ts",93    "testPalletPresence": "yarn _test ./**/pallet-presence.test.ts",94    "testEnableDisableTransfers": "yarn _test ./**/enableDisableTransfer.test.ts",95    "testLimits": "yarn _test ./**/limits.test.ts",96    "testEthCreateNFTCollection": "yarn _test ./**/eth/createNFTCollection.test.ts",97    "testEthCreateRFTCollection": "yarn _test ./**/eth/createRFTCollection.test.ts",98    "testEthNFT": "yarn _test ./**/eth/nonFungible.test.ts",99    "testRFT": "yarn _test ./**/refungible.test.ts",100    "testEthRFT": "yarn _test ./**/eth/reFungible.test.ts ./**/eth/reFungibleToken.test.ts",101    "testFT": "yarn _test ./**/fungible.test.ts",102    "testEthFT": "yarn _test ./**/eth/fungible.test.ts",103    "testRPC": "yarn _test ./**/rpc.test.ts",104    "testPromotion": "yarn _test ./**/appPromotion/*test.ts",105    "testApiConsts": "yarn _test ./**/apiConsts.test.ts",106    "testCollators": "RUN_COLLATOR_TESTS=1 yarn _test ./**/collator-selection/**.*test.ts --timeout 49999999",107    "testCollatorSelection": "RUN_COLLATOR_TESTS=1 yarn _test ./**/collatorSelection.*test.ts --timeout 49999999",108    "testIdentity": "RUN_COLLATOR_TESTS=1 yarn _test ./**/identity.*test.ts --timeout 49999999",109    "testXcmUnique": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmUnique.test.ts",110    "testXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmQuartz.test.ts",111    "testXcmOpal": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmOpal.test.ts",112    "testXcmTransferAcala": "yarn _test ./**/xcm/xcmTransferAcala.test.ts acalaId=2000 uniqueId=5000",113    "testXcmTransferStatemine": "yarn _test ./**/xcm/xcmTransferStatemine.test.ts statemineId=1000 uniqueId=5000",114    "testXcmTransferMoonbeam": "yarn _test ./**/xcm/xcmTransferMoonbeam.test.ts",115    "benchMintingFee": "ts-node src/benchmarks/mintFee/benchmark.ts",116    "load": "yarn _test './**/*.load.ts'",117    "loadTransfer": "ts-node src/transfer.nload.ts",118    "polkadot-types-fetch-metadata": "curl -H 'Content-Type: application/json' -d '{\"id\":\"1\", \"jsonrpc\":\"2.0\", \"method\": \"state_getMetadata\", \"params\":[]}' http://localhost:9933 > src/interfaces/metadata.json",119    "polkadot-types-from-defs": "ts-node --esm ./node_modules/.bin/polkadot-types-from-defs --endpoint src/interfaces/metadata.json --input src/interfaces/ --package .",120    "polkadot-types-from-chain": "ts-node --esm ./node_modules/.bin/polkadot-types-from-chain --endpoint src/interfaces/metadata.json --output src/interfaces/ --package .",121    "polkadot-types": "echo \"export default {}\" > src/interfaces/lookup.ts && yarn polkadot-types-fetch-metadata && yarn polkadot-types-from-defs && yarn polkadot-types-from-defs && yarn polkadot-types-from-chain",122    "generateEnv": "ts-node --esm ./src/generateEnv.ts"123  },124  "author": "",125  "license": "SEE LICENSE IN ../LICENSE",126  "homepage": "",127  "dependencies": {128    "@polkadot/api": "10.7.2",129    "@polkadot/rpc-core": "^10.7.2",130    "@polkadot/util": "12.2.1",131    "@polkadot/util-crypto": "12.2.1",132    "@polkadot/wasm-crypto-asmjs": "^7.2.1",133    "@polkadot/wasm-crypto-wasm": "^7.2.1",134    "chai-as-promised": "^7.1.1",135    "chai-like": "^1.1.1",136    "csv-writer": "^1.6.0",137    "find-process": "^1.4.7",138    "solc": "0.8.17",139    "web3": "1.10.0"140  },141  "resolutions": {142    "decode-uri-component": "^0.2.1"143  },144  "type": "module"145}
after · tests/package.json
1{2  "name": "unique-tests",3  "version": "1.0.0",4  "description": "Unique Chain Tests",5  "main": "",6  "devDependencies": {7    "@polkadot/typegen": "10.7.2",8    "@types/chai": "^4.3.3",9    "@types/chai-as-promised": "^7.1.5",10    "@types/chai-like": "^1.1.1",11    "@types/chai-subset": "^1.3.3",12    "@types/mocha": "^10.0.0",13    "@types/node": "^20.2.3",14    "@typescript-eslint/eslint-plugin": "^5.47.0",15    "@typescript-eslint/parser": "^5.47.0",16    "chai": "^4.3.6",17    "chai-subset": "^1.6.0",18    "eslint": "^8.25.0",19    "eslint-plugin-mocha": "^10.1.0",20    "mocha": "^10.1.0",21    "ts-node": "^10.9.1",22    "typescript": "^5.0.4"23  },24  "mocha": {25    "timeout": 9999999,26    "require": [27      "ts-node/register"28    ]29  },30  "scripts": {31    "lint": "eslint --ext .ts,.js src/",32    "fix": "yarn lint --fix",33    "setup": "ts-node --esm ./src/util/globalSetup.ts",34    "setIdentities": "ts-node ./src/util/identitySetter.ts",35    "checkRelayIdentities": "ts-node ./src/util/relayIdentitiesChecker.ts",36    "_test": "yarn setup && mocha --timeout 9999999 --loader=ts-node/esm.mjs",37    "_testParallel": "yarn setup && mocha --timeout 9999999 --parallel --loader=ts-node/esm.mjs",38    "test": "yarn _test './src/**/*.*test.ts'",39    "testParallelFull": "yarn testParallel && yarn testSequential",40    "testParallel": "yarn _testParallel './src/**/*.test.ts'",41    "testSequential": "yarn _test './src/**/*.seqtest.ts'",42    "testStructure": "yarn _test ./**/nesting/*.*test.ts",43    "testEth": "yarn _test './**/eth/**/*.*test.ts'",44    "testEthNesting": "yarn _test './**/eth/nesting/**/*.*test.ts'",45    "testEthFractionalizer": "yarn _test './**/eth/fractionalizer/**/*.*test.ts'",46    "testEthMarketplace": "yarn _test './**/eth/marketplace/**/*.*test.ts'",47    "testSub": "yarn _test './**/sub/**/*.*test.ts'",48    "testSubNesting": "yarn _test './**/sub/nesting/**/*.*test.ts'",49    "testEvent": "yarn _test ./src/check-event/*.*test.ts",50    "testEthPayable": "yarn _test './**/eth/payable.test.ts'",51    "testEvmCoder": "yarn _test './**/eth/evmCoder.test.ts'",52    "testNesting": "yarn _test ./**/nest.test.ts",53    "testUnnesting": "yarn _test ./**/unnest.test.ts",54    "testProperties": "yarn _test ./**/collectionProperties.*test.ts ./**/tokenProperties.*test.ts ./**/getPropertiesRpc.test.ts",55    "testCollectionProperties": "yarn _test ./**/collectionProperties.*test.ts",56    "testTokenProperties": "yarn _test ./**/tokenProperties.*test.ts",57    "testMigration": "yarn _test ./**/nesting/migration-check.test.ts",58    "testAddCollectionAdmin": "yarn _test ./**/addCollectionAdmin.test.ts",59    "testSetCollectionLimits": "yarn _test ./**/setCollectionLimits.test.ts",60    "testChangeCollectionOwner": "yarn _test ./**/change-collection-owner.test.ts",61    "testSetCollectionSponsor": "yarn _test ./**/setCollectionSponsor.test.ts",62    "testConfirmSponsorship": "yarn _test ./**/confirmSponsorship.test.ts",63    "testRemoveCollectionAdmin": "yarn _test ./**/removeCollectionAdmin.test.ts",64    "testRemoveCollectionSponsor": "yarn _test ./**/removeCollectionSponsor.test.ts",65    "testAllowLists": "yarn _test ./**/allowLists.test.ts",66    "testConnection": "yarn _test ./**/connection.test.ts",67    "testContracts": "yarn _test ./**/contracts.test.ts",68    "testCreateItem": "yarn _test ./**/createItem.test.ts",69    "testCreateMultipleItems": "yarn _test ./**/createMultipleItems.test.ts",70    "testCreateMultipleItemsEx": "yarn _test ./**/createMultipleItemsEx.test.ts",71    "testApprove": "yarn _test ./**/approve.test.ts",72    "testTransferFrom": "yarn _test ./**/transferFrom.test.ts",73    "testCreateCollection": "yarn _test ./**/createCollection.test.ts",74    "testDestroyCollection": "yarn _test ./**/destroyCollection.test.ts",75    "testToggleContractAllowList": "yarn _test ./**/toggleContractAllowList.test.ts",76    "testAddToContractAllowList": "yarn _test ./**/addToContractAllowList.test.ts",77    "testTransfer": "yarn _test ./**/transfer.test.ts",78    "testBurnItem": "yarn _test ./**/burnItem.test.ts",79    "testAdminTransferAndBurn": "yarn _test ./**/adminTransferAndBurn.test.ts",80    "testSetPermissions": "yarn _test ./**/setPermissions.test.ts",81    "testCreditFeesToTreasury": "yarn _test ./**/creditFeesToTreasury.seqtest.ts",82    "testContractSponsoring": "yarn _test ./**/eth/contractSponsoring.test.ts",83    "testEnableContractSponsoring": "yarn _test ./**/enableContractSponsoring.test.ts",84    "testRemoveFromContractAllowList": "yarn _test ./**/removeFromContractAllowList.test.ts",85    "testSetContractSponsoringRateLimit": "yarn _test ./**/setContractSponsoringRateLimit.test.ts",86    "testSetOffchainSchema": "yarn _test ./**/setOffchainSchema.test.ts",87    "testNextSponsoring": "yarn _test ./**/nextSponsoring.test.ts",88    "testOverflow": "yarn _test ./**/overflow.test.ts",89    "testMaintenance": "yarn _test ./**/maintenance.seqtest.ts",90    "testInflation": "yarn _test ./**/inflation.seqtest.ts",91    "testScheduler": "yarn _test ./**/scheduler.seqtest.ts",92    "testSchedulingEVM": "yarn _test ./**/eth/scheduling.test.ts",93    "testPalletPresence": "yarn _test ./**/pallet-presence.test.ts",94    "testEnableDisableTransfers": "yarn _test ./**/enableDisableTransfer.test.ts",95    "testLimits": "yarn _test ./**/limits.test.ts",96    "testEthCreateNFTCollection": "yarn _test ./**/eth/createNFTCollection.test.ts",97    "testEthCreateRFTCollection": "yarn _test ./**/eth/createRFTCollection.test.ts",98    "testEthNFT": "yarn _test ./**/eth/nonFungible.test.ts",99    "testRFT": "yarn _test ./**/refungible.test.ts",100    "testEthRFT": "yarn _test ./**/eth/reFungible.test.ts ./**/eth/reFungibleToken.test.ts",101    "testFT": "yarn _test ./**/fungible.test.ts",102    "testEthFT": "yarn _test ./**/eth/fungible.test.ts",103    "testRPC": "yarn _test ./**/rpc.test.ts",104    "testPromotion": "yarn _test ./**/appPromotion/*test.ts",105    "testApiConsts": "yarn _test ./**/apiConsts.test.ts",106    "testCollators": "RUN_COLLATOR_TESTS=1 yarn _test ./**/collator-selection/**.*test.ts --timeout 49999999",107    "testCollatorSelection": "RUN_COLLATOR_TESTS=1 yarn _test ./**/collatorSelection.*test.ts --timeout 49999999",108    "testIdentity": "RUN_COLLATOR_TESTS=1 yarn _test ./**/identity.*test.ts --timeout 49999999",109    "testXcmUnique": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmUnique.test.ts",110    "testXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmQuartz.test.ts",111    "testXcmOpal": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmOpal.test.ts",112    "testXcmTransferAcala": "yarn _test ./**/xcm/xcmTransferAcala.test.ts acalaId=2000 uniqueId=5000",113    "testXcmTransferStatemine": "yarn _test ./**/xcm/xcmTransferStatemine.test.ts statemineId=1000 uniqueId=5000",114    "testXcmTransferMoonbeam": "yarn _test ./**/xcm/xcmTransferMoonbeam.test.ts",115    "benchMintingFee": "ts-node src/benchmarks/mintFee/benchmark.ts",116    "load": "yarn _test './**/*.load.ts'",117    "loadTransfer": "ts-node src/transfer.nload.ts",118    "polkadot-types-fetch-metadata": "curl -H 'Content-Type: application/json' -d '{\"id\":\"1\", \"jsonrpc\":\"2.0\", \"method\": \"state_getMetadata\", \"params\":[]}' http://localhost:9933 > src/interfaces/metadata.json",119    "polkadot-types-from-defs": "ts-node --esm ./node_modules/.bin/polkadot-types-from-defs --endpoint src/interfaces/metadata.json --input src/interfaces/ --package .",120    "polkadot-types-from-chain": "ts-node --esm ./node_modules/.bin/polkadot-types-from-chain --endpoint src/interfaces/metadata.json --output src/interfaces/ --package .",121    "polkadot-types": "echo \"export default {}\" > src/interfaces/lookup.ts && yarn polkadot-types-fetch-metadata && yarn polkadot-types-from-defs && yarn polkadot-types-from-defs && yarn polkadot-types-from-chain",122    "generateEnv": "ts-node --esm ./src/generateEnv.ts"123  },124  "author": "",125  "license": "SEE LICENSE IN ../LICENSE",126  "homepage": "",127  "dependencies": {128    "@openzeppelin/contracts": "^4.9.0",129    "@polkadot/api": "10.7.2",130    "@polkadot/rpc-core": "^10.7.2",131    "@polkadot/util": "12.2.1",132    "@polkadot/util-crypto": "12.2.1",133    "@polkadot/wasm-crypto-asmjs": "^7.2.1",134    "@polkadot/wasm-crypto-wasm": "^7.2.1",135    "chai-like": "^1.1.1",136    "csv-writer": "^1.6.0",137    "find-process": "^1.4.7",138    "solc": "0.8.17",139    "web3": "1.10.0"140  },141  "resolutions": {142    "decode-uri-component": "^0.2.1"143  },144  "type": "module"145}
addedtests/src/eth/marketplace-v2/Market.soldiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/marketplace-v2/Market.sol
@@ -0,0 +1,364 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity 0.8.17;
+
+import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
+import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
+import { UniqueNFT, CrossAddress } from "@unique-nft/solidity-interfaces/contracts/UniqueNFT.sol";
+import "@unique-nft/solidity-interfaces/contracts/CollectionHelpers.sol";
+import "./royalty/UniqueRoyaltyHelper.sol";
+
+contract Market {
+    using ERC165Checker for address;
+
+    struct Order {
+      uint32 id;
+      uint32 collectionId;
+      uint32 tokenId;
+      uint32 amount;
+      uint256 price;
+      CrossAddress seller;
+    }
+
+    uint32 public constant version = 0;
+    bytes4 private constant InterfaceId_ERC721 = 0x80ac58cd;
+    bytes4 private constant InterfaceId_ERC165 = 0x5755c3f2;
+    CollectionHelpers private constant collectionHelpers =
+        CollectionHelpers(0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F);
+
+    mapping(uint32 => mapping(uint32 => Order)) orders;
+    uint32 private idCount = 1;
+    uint32 public marketFee;
+    uint64 public ctime;
+    address selfAddress;
+    address public ownerAddress;
+    mapping(address => bool) public admins;
+
+    event TokenIsUpForSale(uint32 version, Order item);
+    event TokenRevoke(uint32 version, Order item, uint32 amount);
+    event TokenIsApproved(uint32 version, Order item);
+    event TokenIsPurchased(
+      uint32 version,
+      Order item,
+      uint32 salesAmount,
+      CrossAddress buyer,
+      RoyaltyAmount[] royalties
+    );
+    event Log(string message);
+
+    error InvalidArgument(string info);
+    error InvalidMarketFee();
+    error SellerIsNotOwner();
+    error TokenIsAlreadyOnSale();
+    error TokenIsNotApproved();
+    error CollectionNotFound();
+    error CollectionNotSupportedERC721();
+    error OrderNotFound();
+    error TooManyAmountRequested();
+    error NotEnoughMoneyError();
+    error FailTransferToken(string reason);
+
+    modifier onlyOwner() {
+      require(msg.sender == ownerAddress, "Only owner can");
+      _;
+    }
+
+    modifier onlyAdmin() {
+      require(msg.sender == ownerAddress || admins[msg.sender], "Only admin can");
+      _;
+    }
+
+    modifier validCrossAddress(address eth, uint256 sub) {
+      if (eth == address(0) && sub == 0) {
+        revert InvalidArgument("Ethereum and Substrate addresses cannot be null at the same time");
+      }
+
+      if (eth != address(0) && sub != 0) {
+        revert InvalidArgument("Ethereum and Substrate addresses cannot be not null at the same time");
+      }
+
+      _;
+    }
+
+    constructor(uint32 fee, uint64 timestamp) {
+        marketFee = fee;
+        ctime = timestamp;
+
+        if (marketFee == 0 || marketFee >= 100) {
+            revert InvalidMarketFee();
+        }
+
+        ownerAddress = msg.sender;
+        selfAddress = address(this);
+    }
+
+    function getErc721(uint32 collectionId) private view returns (IERC721) {
+        address collectionAddress = collectionHelpers.collectionAddress(
+            collectionId
+        );
+
+        uint size;
+        assembly {
+            size := extcodesize(collectionAddress)
+        }
+
+        if (size == 0) {
+            revert CollectionNotFound();
+        }
+
+        if (!collectionAddress.supportsInterface(InterfaceId_ERC721)) {
+            revert CollectionNotSupportedERC721();
+        }
+
+        return IERC721(collectionAddress);
+    }
+
+    // ################################################################
+    // Set new contract owner                                         #
+    // ################################################################
+
+    function setOwner() public onlyOwner {
+        ownerAddress = msg.sender;
+    }
+
+    // ################################################################
+    // Add new admin                                                  #
+    // ################################################################
+
+    function addAdmin(address admin) public onlyAdmin {
+      admins[admin] = true;
+    }
+
+    // ################################################################
+    // Remove admin                                                  #
+    // ################################################################
+
+    function removeAdmin(address admin) public onlyAdmin {
+      delete admins[admin];
+    }
+
+    // ################################################################
+    // Place a token for sale                                         #
+    // ################################################################
+
+    function put(
+        uint32 collectionId,
+        uint32 tokenId,
+        uint256 price,
+        uint32 amount,
+        CrossAddress memory seller
+    ) public validCrossAddress(seller.eth, seller.sub) {
+        if (price == 0) {
+          revert InvalidArgument("price must not be zero");
+        }
+        if (amount == 0) {
+          revert InvalidArgument("amount must not be zero");
+        }
+
+        if (orders[collectionId][tokenId].price > 0) {
+            revert TokenIsAlreadyOnSale();
+        }
+
+        IERC721 erc721 = getErc721(collectionId);
+
+        if (erc721.ownerOf(tokenId) != msg.sender) {
+          revert SellerIsNotOwner();
+        }
+
+        if (erc721.getApproved(tokenId) != selfAddress) {
+          revert TokenIsNotApproved();
+        }
+
+        Order memory order = Order(
+            0,
+            collectionId,
+            tokenId,
+            amount,
+            price,
+            seller
+        );
+
+        order.id = idCount++;
+        orders[collectionId][tokenId] = order;
+
+        emit TokenIsUpForSale(version, order);
+    }
+
+    // ################################################################
+    // Get order                                                      #
+    // ################################################################
+
+    function getOrder(
+        uint32 collectionId,
+        uint32 tokenId
+    ) external view returns (Order memory) {
+        return orders[collectionId][tokenId];
+    }
+
+    // ################################################################
+    // Revoke the token from the sale                                 #
+    // ################################################################
+
+    function revoke(
+        uint32 collectionId,
+        uint32 tokenId,
+        uint32 amount
+    ) external {
+        if (amount == 0) {
+          revert InvalidArgument("amount must not be zero");
+        }
+
+        Order memory order = orders[collectionId][tokenId];
+
+        if (order.price == 0) {
+          revert OrderNotFound();
+        }
+
+        if (amount > order.amount) {
+          revert TooManyAmountRequested();
+        }
+
+        IERC721 erc721 = getErc721(collectionId);
+
+        address ethAddress;
+        if (order.seller.eth != address(0)) {
+          ethAddress = order.seller.eth;
+        } else {
+          ethAddress = payable(address(uint160(order.seller.sub >> 96)));
+        }
+        if (erc721.ownerOf(tokenId) != ethAddress) {
+          revert SellerIsNotOwner();
+        }
+
+        order.amount -= amount;
+        if (order.amount == 0) {
+            delete orders[collectionId][tokenId];
+        } else {
+            orders[collectionId][tokenId] = order;
+        }
+
+        emit TokenRevoke(version, order, amount);
+    }
+
+    // ################################################################
+    // Check approved                                                 #
+    // ################################################################
+
+    function checkApproved(uint32 collectionId, uint32 tokenId) public onlyAdmin {
+        Order memory order = orders[collectionId][tokenId];
+        if (order.price == 0) {
+            revert OrderNotFound();
+        }
+
+        IERC721 erc721 = getErc721(collectionId);
+
+        if (erc721.getApproved(tokenId) != selfAddress) {
+          uint32 amount = order.amount;
+          order.amount = 0;
+          emit TokenRevoke(version, order, amount);
+
+          delete orders[collectionId][tokenId];
+        } else {
+          emit TokenIsApproved(version, order);
+        }
+    }
+
+    // ################################################################
+    // Buy a token                                                    #
+    // ################################################################
+
+    function buy(
+        uint32 collectionId,
+        uint32 tokenId,
+        uint32 amount,
+        CrossAddress memory buyer
+    ) public payable validCrossAddress(buyer.eth, buyer.sub) {
+        if (msg.value == 0) {
+          revert InvalidArgument("msg.value must not be zero");
+        }
+        if (amount == 0) {
+          revert InvalidArgument("amount must not be zero");
+        }
+
+        Order memory order = orders[collectionId][tokenId];
+        if (order.price == 0) {
+            revert OrderNotFound();
+        }
+
+        if (amount > order.amount) {
+            revert TooManyAmountRequested();
+        }
+
+        uint256 totalValue = order.price * amount;
+        uint256 feeValue = (totalValue * marketFee) / 100;
+
+        if (msg.value < totalValue) {
+            revert NotEnoughMoneyError();
+        }
+
+        IERC721 erc721 = getErc721(order.collectionId);
+        if (erc721.getApproved(tokenId) != selfAddress) {
+          revert TokenIsNotApproved();
+        }
+
+        order.amount -= amount;
+        if (order.amount == 0) {
+            delete orders[collectionId][tokenId];
+        } else {
+            orders[collectionId][tokenId] = order;
+        }
+
+        address collectionAddress = collectionHelpers.collectionAddress(collectionId);
+        UniqueNFT nft = UniqueNFT(collectionAddress);
+
+        nft.transferFromCross(
+          order.seller,
+          buyer,
+          order.tokenId
+        );
+
+        (uint256 totalRoyalty, RoyaltyAmount[] memory royalties) = sendRoyalties(collectionAddress, tokenId, totalValue);
+
+        sendMoney(order.seller, totalValue - feeValue - totalRoyalty);
+
+        if (msg.value > totalValue) {
+            // todo, send money to signer or buyer ?
+            payable(msg.sender).transfer(msg.value - totalValue);
+        }
+
+        emit TokenIsPurchased(version, order, amount, buyer, royalties);
+    }
+
+    function sendMoney(CrossAddress memory to, uint256 money) private {
+      address payable eth;
+      if (to.eth != address(0)) {
+        eth = payable(to.eth);
+      } else {
+        eth = payable(address(uint160(to.sub >> 96)));
+      }
+      eth.transfer(money);
+    }
+
+    function sendRoyalties(address collection, uint tokenId, uint sellPrice) private returns (uint256, RoyaltyAmount[] memory) {
+      RoyaltyAmount[] memory royalties = UniqueRoyaltyHelper.calculate(collection, tokenId, sellPrice);
+
+      uint256 totalRoyalty = 0;
+
+      for (uint256 i=0; i<royalties.length; i++) {
+        RoyaltyAmount memory royalty = royalties[i];
+
+        totalRoyalty += royalty.amount;
+
+        sendMoney(royalty.crossAddress, royalty.amount);
+      }
+
+      return (totalRoyalty, royalties);
+    }
+
+    function withdraw(address transferTo) public onlyOwner {
+        uint256 balance = selfAddress.balance;
+
+        if (balance > 0) {
+            payable(transferTo).transfer(balance);
+        }
+    }
+}
\ No newline at end of file
addedtests/src/eth/marketplace-v2/marketplace.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/marketplace-v2/marketplace.test.ts
@@ -0,0 +1,78 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+import {IKeyringPair} from '@polkadot/types/types';
+import {readFile} from 'fs/promises';
+import {itEth, usingEthPlaygrounds} from '../util';
+import {makeNames} from '../../util';
+
+const {dirname} = makeNames(import.meta.url);
+
+describe('Market V2 Contract', () => {
+  let donor: IKeyringPair;
+
+  before(async () => {
+    await usingEthPlaygrounds(async (_helper, privateKey) => {
+      donor = await privateKey({url: import.meta.url});
+    });
+  });
+
+  itEth('Deploy', async ({helper}) => {
+    const matcherOwner = await helper.eth.createAccountWithBalance(donor, 600n);
+
+    await helper.ethContract.deployByCode(
+      matcherOwner,
+      'Market',
+      (await readFile(`${dirname}/Market.sol`)).toString(),
+      [
+        {
+          solPath: '@unique-nft/solidity-interfaces/contracts/UniqueNFT.sol',
+          fsPath: `${dirname}/../api/UniqueNFT.sol`,
+        },
+        {
+          solPath: '@openzeppelin/contracts/utils/introspection/IERC165.sol',
+          fsPath: `${dirname}/../../../node_modules/@openzeppelin/contracts/utils/introspection/IERC165.sol`,
+        },
+        {
+          solPath: '@openzeppelin/contracts/utils/introspection/ERC165Checker.sol',
+          fsPath: `${dirname}/../../../node_modules/@openzeppelin/contracts/utils/introspection/ERC165Checker.sol`,
+        },
+        {
+          solPath: '@openzeppelin/contracts/token/ERC721/IERC721.sol',
+          fsPath: `${dirname}/../../../node_modules/@openzeppelin/contracts/token/ERC721/IERC721.sol`,
+        },
+        {
+          solPath: '@unique-nft/solidity-interfaces/contracts/CollectionHelpers.sol',
+          fsPath: `${dirname}/../api/CollectionHelpers.sol`,
+        },
+        {
+          solPath: 'royalty/UniqueRoyaltyHelper.sol',
+          fsPath: `${dirname}/royalty/UniqueRoyaltyHelper.sol`,
+        },
+        {
+          solPath: 'royalty/UniqueRoyalty.sol',
+          fsPath: `${dirname}/royalty/UniqueRoyalty.sol`,
+        },
+        {
+          solPath: 'royalty/LibPart.sol',
+          fsPath: `${dirname}/royalty/LibPart.sol`,
+        },
+      ],
+      15000000,
+      [1, 0],
+    );
+  });
+});
addedtests/src/eth/marketplace-v2/royalty/LibPart.soldiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/marketplace-v2/royalty/LibPart.sol
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "./UniqueRoyalty.sol";
+
+library LibPart {
+    bytes32 public constant TYPE_HASH = keccak256("Part(address account,uint96 value)");
+
+    struct Part {
+        address payable account;
+        uint96 value;
+    }
+
+    function hash(Part memory part) internal pure returns (bytes32) {
+        return keccak256(abi.encode(TYPE_HASH, part.account, part.value));
+    }
+}
+
+library LibPartAdapter {
+    function encode(LibPart.Part[] memory parts) internal pure returns (bytes memory) {
+        if (parts.length == 0) return "";
+
+        uint256[] memory encoded = new uint256[](parts.length * 2);
+
+        for (uint i = 0; i < parts.length; i++) {
+            encoded[i * 2] = 0x0100000000000000000000000000000000000000000000040000000000000000 | uint256(parts[i].value);
+            encoded[i * 2 + 1] = uint256(uint160(address(parts[i].account)));
+        }
+
+        return abi.encodePacked(encoded);
+    }
+
+    function decode(bytes memory b) internal pure returns (LibPart.Part[] memory) {
+        if (b.length == 0) return new LibPart.Part[](0);
+
+        require((b.length % (32 * 2)) == 0, "Invalid bytes length, expected (32 * 2) * UniqueRoyaltyParts count");
+        uint partsCount = b.length / (32 * 2);
+        uint numbersCount = partsCount * 2;
+
+        LibPart.Part[] memory parts = new LibPart.Part[](partsCount);
+
+        // need this because numbers encoded via abi.encodePacked
+        bytes memory prefix = new bytes(64);
+
+        assembly {
+            mstore(add(prefix, 32), 32)
+            mstore(add(prefix, 64), numbersCount)
+        }
+
+        uint256[] memory encoded = abi.decode(bytes.concat(prefix, b), (uint256[]));
+
+        for (uint i = 0; i < partsCount; i++) {
+            uint96 value = uint96(encoded[i * 2] & 0xFFFFFFFFFFFFFFFF);
+            address account = address(uint160(encoded[i * 2 + 1]));
+
+            parts[i] = LibPart.Part({
+                account: payable(account),
+                value: value
+            });
+        }
+
+        return parts;
+    }
+}
+
+library LibPartAdapterComplex {
+    function decodeSafe(bytes memory data) internal pure returns (LibPart.Part[] memory) {
+        return fromUniqueRoyalties(UniqueRoyalty.decode(data));
+    }
+
+    function encodeSafe(LibPart.Part[] memory parts) internal pure returns (bytes memory) {
+        return UniqueRoyalty.encode(toUniqueRoyalties(parts));
+    }
+
+    function fromUniqueRoyalties(UniqueRoyaltyPart[] memory royalties) internal pure returns (LibPart.Part[] memory) {
+        LibPart.Part[] memory parts = new LibPart.Part[](royalties.length);
+
+        for (uint i = 0; i < royalties.length; i++) {
+            uint96 value = royalties[i].decimals >= 4
+            ? uint96(royalties[i].value * (10 ** (royalties[i].decimals - 4)))
+            : uint96(royalties[i].value / (10 ** (4 - royalties[i].decimals)));
+
+            parts[i] = LibPart.Part({
+                account: payable(CrossAddressLib.toAddress(royalties[i].crossAddress)),
+                value: value
+            });
+        }
+
+        return parts;
+    }
+
+    function toUniqueRoyalties(LibPart.Part[] memory parts) internal pure returns (UniqueRoyaltyPart[] memory) {
+        UniqueRoyaltyPart[] memory royalties = new UniqueRoyaltyPart[](parts.length);
+
+        for (uint i = 0; i < parts.length; i++) {
+            royalties[i] = UniqueRoyaltyPart({
+                version: 1,
+                value: uint64(parts[i].value),
+                decimals: 4,
+                crossAddress: CrossAddress({
+                    sub: 0,
+                    eth: parts[i].account
+                }),
+                isPrimarySaleOnly: false
+            });
+        }
+
+        return royalties;
+    }
+}
\ No newline at end of file
addedtests/src/eth/marketplace-v2/royalty/UniqueRoyalty.soldiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/marketplace-v2/royalty/UniqueRoyalty.sol
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity >=0.8.17;
+
+import { CrossAddress } from "@unique-nft/solidity-interfaces/contracts/UniqueNFT.sol";
+
+struct UniqueRoyaltyPart {
+    uint8 version;
+    uint8 decimals;
+    uint64 value;
+    bool isPrimarySaleOnly;
+    CrossAddress crossAddress;
+}
+
+library CrossAddressLib {
+    function toAddress(CrossAddress memory crossAddress) internal pure returns (address) {
+        return crossAddress.eth != address(0) ? crossAddress.eth : address(uint160(crossAddress.sub >> 96));
+    }
+}
+
+library UniqueRoyalty {
+    uint private constant DECIMALS_OFFSET = 4 * 16;
+    uint private constant ADDRESS_TYPE_OFFSET = 4 * (16 + 2);           // 0 - eth, 1 - sub
+    uint private constant ROYALTY_TYPE_OFFSET = 4 * (16 + 2 + 1);       // 0 - default, 1 - primary sale only
+    uint private constant VERSION_OFFSET = 4 * (16 + 2 + 1 + 1 + 42);
+
+    uint private constant PART_LENGTH = 32 * 2;
+
+    function decode(bytes memory b) internal pure returns (UniqueRoyaltyPart[] memory) {
+        if (b.length == 0) return new UniqueRoyaltyPart[](0);
+
+        require((b.length % PART_LENGTH) == 0, "Invalid bytes length, expected (32 * 2) * UniqueRoyaltyParts count");
+        uint partsCount = b.length / PART_LENGTH;
+        uint numbersCount = partsCount * 2;
+
+        UniqueRoyaltyPart[] memory parts = new UniqueRoyaltyPart[](partsCount);
+
+        // need this because numbers encoded via abi.encodePacked
+        bytes memory prefix = new bytes(64);
+
+        assembly {
+            mstore(add(prefix, 32), 32)
+            mstore(add(prefix, 64), numbersCount)
+        }
+
+        uint256[] memory encoded = abi.decode(bytes.concat(prefix, b), (uint256[]));
+
+        for (uint i = 0; i < partsCount; i++) {
+            parts[i] = decodePart(encoded[i * 2], encoded[i * 2 + 1]);
+        }
+
+        return parts;
+    }
+
+    function encode(UniqueRoyaltyPart[] memory parts) internal pure returns (bytes memory) {
+        if (parts.length == 0) return "";
+
+        uint256[] memory encoded = new uint256[](parts.length * 2);
+
+        for (uint i = 0; i < parts.length; i++) {
+            (uint256 encodedMeta, uint256 encodedAddress) = encodePart(parts[i]);
+
+            encoded[i * 2] = encodedMeta;
+            encoded[i * 2 + 1] = encodedAddress;
+        }
+
+        return abi.encodePacked(encoded);
+    }
+
+    function decodePart(bytes memory b) internal pure returns (UniqueRoyaltyPart memory) {
+        require(b.length == PART_LENGTH, "Invalid bytes length, expected 32 * 2");
+
+        uint256[2] memory encoded = abi.decode(b, (uint256[2]));
+
+        return decodePart(encoded[0], encoded[1]);
+    }
+
+    function decodePart(
+        uint256 _meta,
+        uint256 _address
+    ) internal pure returns (UniqueRoyaltyPart memory) {
+        uint256 version = _meta >> VERSION_OFFSET;
+        bool isPrimarySaleOnly = (_meta & (1 << ROYALTY_TYPE_OFFSET)) > 0;
+        bool isEthereumAddress = (_meta & (1 << ADDRESS_TYPE_OFFSET)) == 0;
+        uint256 decimals = (_meta >> 4 * 16) & 0xFF;
+        uint256 value = _meta & 0xFFFFFFFFFFFFFFFF;
+
+        CrossAddress memory crossAddress = isEthereumAddress
+            ? CrossAddress({ sub: 0, eth: address(uint160(_address)) })
+            : CrossAddress({ sub: _address, eth: address(0) });
+
+        UniqueRoyaltyPart memory royaltyPart = UniqueRoyaltyPart({
+            version: uint8(version),
+            decimals: uint8(decimals),
+            value: uint64(value),
+            isPrimarySaleOnly: isPrimarySaleOnly,
+            crossAddress: crossAddress
+        });
+
+        return royaltyPart;
+    }
+
+    function encodePart(UniqueRoyaltyPart memory royaltyPart) internal pure returns (uint256, uint256) {
+        uint256 encodedMeta = 0;
+        uint256 encodedAddress = 0;
+
+        encodedMeta |= uint256(royaltyPart.version) << VERSION_OFFSET;
+        if (royaltyPart.isPrimarySaleOnly) encodedMeta |= 1 << ROYALTY_TYPE_OFFSET;
+
+        if (royaltyPart.crossAddress.eth == address(0x0)) {
+            encodedMeta |= 1 << ADDRESS_TYPE_OFFSET;
+            encodedAddress = royaltyPart.crossAddress.sub;
+        } else {
+            encodedAddress = uint256(uint160(royaltyPart.crossAddress.eth));
+        }
+
+        encodedMeta |= uint256(royaltyPart.decimals) << DECIMALS_OFFSET;
+        encodedMeta |= uint256(royaltyPart.value);
+
+        return (encodedMeta, encodedAddress);
+    }
+}
\ No newline at end of file
addedtests/src/eth/marketplace-v2/royalty/UniqueRoyaltyHelper.soldiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/marketplace-v2/royalty/UniqueRoyaltyHelper.sol
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity >=0.8.17;
+
+import "./UniqueRoyalty.sol";
+import "./LibPart.sol";
+
+string constant ROYALTIES_PROPERTY = "royalties";
+
+interface ICollection {
+    function collectionProperty(string memory key) external view returns (bytes memory);
+    function property(uint256 tokenId, string memory key) external view returns (bytes memory);
+}
+
+struct RoyaltyAmount {
+    CrossAddress crossAddress;
+    uint amount;
+}
+
+library UniqueRoyaltyHelper {
+    function encodePart(UniqueRoyaltyPart memory part) internal pure returns (bytes memory) {
+        (uint256 encodedMeta, uint256 encodedAddress) = UniqueRoyalty.encodePart(part);
+
+        return abi.encodePacked(encodedMeta, encodedAddress);
+    }
+
+    function decodePart(bytes memory data) internal pure returns (UniqueRoyaltyPart memory) {
+        return UniqueRoyalty.decodePart(data);
+    }
+
+    // todo - implement smth better - check royalties sum is lte 100%
+    function validatePart(bytes memory b) internal pure returns (bool isValid) {
+        isValid = b.length == 64;
+    }
+
+    function encode(UniqueRoyaltyPart[] memory royalties) internal pure returns (bytes memory) {
+        return UniqueRoyalty.encode(royalties);
+    }
+
+    function decode(bytes memory data) internal pure returns (UniqueRoyaltyPart[] memory) {
+        return UniqueRoyalty.decode(data);
+    }
+
+    // todo - implement smth better - check royalties sum is lte 100%
+    function validate(bytes memory b) internal pure returns (bool) {
+        return b.length % 64 == 0;
+    }
+
+    function getTokenRoyalty(address collection, uint tokenId) internal view returns (UniqueRoyaltyPart[] memory) {
+        try ICollection(collection).property(tokenId, ROYALTIES_PROPERTY) returns (bytes memory encoded) {
+            return UniqueRoyalty.decode(encoded);
+        } catch {
+            return new UniqueRoyaltyPart[](0);
+        }
+    }
+
+    function getCollectionRoyalty(address collection) internal view returns (UniqueRoyaltyPart[] memory) {
+        try ICollection(collection).collectionProperty(ROYALTIES_PROPERTY) returns (bytes memory encoded) {
+            return UniqueRoyalty.decode(encoded);
+        } catch {
+            return new UniqueRoyaltyPart[](0);
+        }
+    }
+
+    function getRoyalty(address collection, uint tokenId) internal view returns (UniqueRoyaltyPart[] memory royalty) {
+        royalty = getTokenRoyalty(collection, tokenId);
+
+        if (royalty.length == 0) {
+            royalty = getCollectionRoyalty(collection);
+        }
+    }
+
+    function calculateRoyalties(UniqueRoyaltyPart[] memory royalties, bool isPrimarySale, uint sellPrice) internal pure returns (RoyaltyAmount[] memory) {
+        RoyaltyAmount[] memory royaltyAmounts = new RoyaltyAmount[](royalties.length);
+        uint amountsCount = 0;
+
+        for (uint i = 0; i < royalties.length; i++) {
+            if (isPrimarySale == royalties[i].isPrimarySaleOnly) {
+                uint amount = (sellPrice * royalties[i].value) / (10 ** (royalties[i].decimals));
+
+                royaltyAmounts[amountsCount] = RoyaltyAmount({
+                    crossAddress: royalties[i].crossAddress,
+                    amount: amount
+                });
+
+                amountsCount += 1;
+            }
+        }
+
+        // shrink royaltyAmounts to amountsCount length
+        assembly { mstore(royaltyAmounts, amountsCount) }
+
+        return royaltyAmounts;
+    }
+
+    function calculateForPrimarySale(address collection, uint tokenId, uint sellPrice) internal view returns (RoyaltyAmount[] memory) {
+        UniqueRoyaltyPart[] memory royalties = getRoyalty(collection, tokenId);
+
+        return calculateRoyalties(royalties, true, sellPrice);
+    }
+
+    function calculate(address collection, uint tokenId, uint sellPrice) internal view returns (RoyaltyAmount[] memory) {
+        UniqueRoyaltyPart[] memory royalties = getRoyalty(collection, tokenId);
+
+        return calculateRoyalties(royalties, false, sellPrice);
+    }
+}
\ No newline at end of file
addedtests/src/eth/marketplace-v2/utils.soldiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/marketplace-v2/utils.sol
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.17;
+
+contract Utils {
+    function toString(address account) public pure returns (string memory) {
+        return toString(abi.encodePacked(account));
+    }
+
+    function toString(bool value) public pure returns (string memory) {
+        return value ? "true" : "false";
+    }
+
+    function toString(uint256 value) public pure returns (string memory) {
+        return toString(abi.encodePacked(value));
+    }
+
+    function toString(bytes32 value) public pure returns (string memory) {
+        return toString(abi.encodePacked(value));
+    }
+
+    function toString(bytes memory data) public pure returns (string memory) {
+        bytes memory alphabet = "0123456789abcdef";
+
+        bytes memory str = new bytes(2 + data.length * 2);
+        str[0] = "0";
+        str[1] = "x";
+        for (uint i = 0; i < data.length; i++) {
+            str[2 + i * 2] = alphabet[uint(uint8(data[i] >> 4))];
+            str[3 + i * 2] = alphabet[uint(uint8(data[i] & 0x0f))];
+        }
+        return string(str);
+    }
+}
\ No newline at end of file
modifiedtests/src/eth/util/playgrounds/unique.dev.tsdiffbeforeafterboth
--- a/tests/src/eth/util/playgrounds/unique.dev.ts
+++ b/tests/src/eth/util/playgrounds/unique.dev.ts
@@ -102,19 +102,19 @@
     };
   }
 
-  async deployByCode(signer: string, name: string, src: string, imports?: ContractImports[], gas?: number): Promise<Contract> {
+  async deployByCode(signer: string, name: string, src: string, imports?: ContractImports[], gas?: number, args?: any[]): Promise<Contract> {
     const compiledContract = await this.compile(name, src, imports);
-    return this.deployByAbi(signer, compiledContract.abi, compiledContract.object, gas);
+    return this.deployByAbi(signer, compiledContract.abi, compiledContract.object, gas, args);
   }
 
-  async deployByAbi(signer: string, abi: any, object: string, gas?: number): Promise<Contract> {
+  async deployByAbi(signer: string, abi: any, object: string, gas?: number, args?: any[]): Promise<Contract> {
     const web3 = this.helper.getWeb3();
     const contract = new web3.eth.Contract(abi, undefined, {
       data: object,
       from: signer,
       gas: gas ?? this.helper.eth.DEFAULT_GAS,
     });
-    return await contract.deploy({data: object}).send({from: signer});
+    return await contract.deploy({data: object, arguments: args}).send({from: signer});
   }
 
 }