difftreelog
feat add test for market v2 deployment
in: master
8 files changed
tests/package.jsondiffbeforeafterboth--- a/tests/package.json
+++ b/tests/package.json
@@ -125,13 +125,13 @@
"license": "SEE LICENSE IN ../LICENSE",
"homepage": "",
"dependencies": {
+ "@openzeppelin/contracts": "^4.9.0",
"@polkadot/api": "10.7.2",
"@polkadot/rpc-core": "^10.7.2",
"@polkadot/util": "12.2.1",
"@polkadot/util-crypto": "12.2.1",
"@polkadot/wasm-crypto-asmjs": "^7.2.1",
"@polkadot/wasm-crypto-wasm": "^7.2.1",
- "chai-as-promised": "^7.1.1",
"chai-like": "^1.1.1",
"csv-writer": "^1.6.0",
"find-process": "^1.4.7",
tests/src/eth/marketplace-v2/Market.soldiffbeforeafterbothno changes
tests/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],
+ );
+ });
+});
tests/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
tests/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
tests/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
tests/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
tests/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});
}
}