git.delta.rocks / unique-network / refs/commits / 42f7f34eac79

difftreelog

feat(refungible-pallet) fractionalizer contract

Grigoriy Simonov2022-08-11parent: #eb17630.patch.diff
in: master

7 files changed

modifiedtests/package.jsondiffbeforeafterboth
--- a/tests/package.json
+++ b/tests/package.json
@@ -8,6 +8,7 @@
     "@polkadot/typegen": "8.7.2-15",
     "@types/chai": "^4.3.1",
     "@types/chai-as-promised": "^7.1.5",
+    "@types/chai-like": "^1.1.1",
     "@types/mocha": "^9.1.1",
     "@types/node": "^17.0.35",
     "@typescript-eslint/eslint-plugin": "^5.26.0",
@@ -96,6 +97,7 @@
     "@polkadot/util-crypto": "9.4.1",
     "bignumber.js": "^9.0.2",
     "chai-as-promised": "^7.1.1",
+    "chai-like": "^1.1.1",
     "find-process": "^1.4.7",
     "solc": "0.8.14-fixed",
     "web3": "^1.7.3"
addedtests/src/eth/fractionalizer/Fractionalizer.bindiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/fractionalizer/Fractionalizer.bin
@@ -0,0 +1 @@
+60c0604052600a60805269526546756e6769626c6560b01b60a0527fcdb72fd4e1d0d6d4eebd7ab142113ec2b4b06ddb24324db5c287ef01ab484d6b60055534801561004a57600080fd5b50600480546001600160a01b031916331790556113cd8061006c6000396000f3fe608060405234801561001057600080fd5b50600436106100575760003560e01c8063115091401461005c578063b12527f814610071578063d470e60f14610084578063dbc38ad214610097578063f0d4cf5f146100aa575b600080fd5b61006f61006a366004610f34565b6100bd565b005b61006f61007f366004610f66565b6102b8565b61006f610092366004610f9f565b610345565b61006f6100a5366004610fcb565b610722565b61006f6100b8366004611062565b610dc4565b6004546001600160a01b031633146100f05760405162461bcd60e51b81526004016100e7906110fc565b60405180910390fd5b6000546001600160a01b0316156101495760405162461bcd60e51b815260206004820152601d60248201527f52465420636f6c6c656374696f6e20697320616c72656164792073657400000060448201526064016100e7565b60008190506000816001600160a01b031663d34b55b86040518163ffffffff1660e01b81526004016000604051808303816000875af1158015610190573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526101b8919081019061113a565b90506005548180519060200120146101e25760405162461bcd60e51b81526004016100e79061120a565b816001600160a01b03166304a460536040518163ffffffff1660e01b81526004016020604051808303816000875af1158015610222573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102469190611267565b6102625760405162461bcd60e51b81526004016100e79061120a565b600080546001600160a01b0319166001600160a01b0385169081179091556040519081527f7186a599bf2297b1f4c8957d30b0965291eee0021a5f9a0aeb54dcbd1ffdceef9060200160405180910390a1505050565b6004546001600160a01b031633146102e25760405162461bcd60e51b81526004016100e7906110fc565b6001600160a01b038216600081815260016020908152604091829020805460ff19168515159081179091558251938452908301527f6dad0aed33f4b7f07095619b668698e17943fd9f4c83e7cfcc7f6dd880a11588910160405180910390a15050565b6000546001600160a01b03166103995760405162461bcd60e51b81526020600482015260196024820152781491950818dbdb1b1958dd1a5bdb881a5cc81b9bdd081cd95d603a1b60448201526064016100e7565b6000546001600160a01b038381169116146103ed5760405162461bcd60e51b81526020600482015260146024820152732bb937b7339029232a1031b7b63632b1ba34b7b760611b60448201526064016100e7565b600080546040516355bb7d6360e11b8152600481018490526001600160a01b039091169190829063ab76fac690602401602060405180830381865afa15801561043a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061045e9190611284565b6001600160a01b0380821660009081526003602090815260409182902082518084019093528054909316808352600190930154908201529192506104e45760405162461bcd60e51b815260206004820181905260248201527f4e6f20636f72726573706f6e64696e67204e465420746f6b656e20666f756e6460448201526064016100e7565b6000829050806001600160a01b03166318160ddd6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610527573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061054b91906112a1565b6040516370a0823160e01b81523360048201526001600160a01b038316906370a0823190602401602060405180830381865afa15801561058f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105b391906112a1565b1461060f5760405162461bcd60e51b815260206004820152602660248201527f4e6f7420616c6c2070696563657320617265206f776e6564206279207468652060448201526531b0b63632b960d11b60648201526084016100e7565b6040516323b872dd60e01b81526001600160a01b038516906323b872dd9061063f90339030908a906004016112ba565b600060405180830381600087803b15801561065957600080fd5b505af115801561066d573d6000803e3d6000fd5b5050835160208501516040516323b872dd60e01b81526001600160a01b0390921693506323b872dd92506106a791309133916004016112ba565b600060405180830381600087803b1580156106c157600080fd5b505af11580156106d5573d6000803e3d6000fd5b5050835160208501516040517fd2628c68a4330ea5ba2e16305b03a4e974e760e86f302d43e6bc00c28918cb2294506107129350879291906112ba565b60405180910390a1505050505050565b6000546001600160a01b03166107765760405162461bcd60e51b81526020600482015260196024820152781491950818dbdb1b1958dd1a5bdb881a5cc81b9bdd081cd95d603a1b60448201526064016100e7565b6000546040516331a9108f60e11b8152600481018490526001600160a01b0391821691339190861690636352211e90602401602060405180830381865afa1580156107c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107e99190611284565b6001600160a01b03161461080f5760405162461bcd60e51b81526004016100e7906112de565b6001600160a01b03841660009081526001602081905260409091205460ff161515146108a35760405162461bcd60e51b815260206004820152603c60248201527f4672616374696f6e616c697a6174696f6e206f66207468697320636f6c6c656360448201527f74696f6e206973206e6f7420616c6c6f7765642062792061646d696e0000000060648201526084016100e7565b6040516331a9108f60e11b81526004810184905233906001600160a01b03861690636352211e90602401602060405180830381865afa1580156108ea573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061090e9190611284565b6001600160a01b0316146109345760405162461bcd60e51b81526004016100e7906112de565b6040516323b872dd60e01b81526001600160a01b038516906323b872dd90610964903390309088906004016112ba565b600060405180830381600087803b15801561097e57600080fd5b505af1158015610992573d6000803e3d6000fd5b505050506001600160a01b0384166000908152600260209081526040808320868452909152812054819081908103610bde57836001600160a01b03166375794a3c6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a02573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a2691906112a1565b6040516340c10f1960e01b8152306004820152602481018290529093506001600160a01b038516906340c10f19906044016020604051808303816000875af1158015610a76573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a9a9190611267565b506040516355bb7d6360e11b8152600481018490526001600160a01b0385169063ab76fac690602401602060405180830381865afa158015610ae0573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b049190611284565b6001600160a01b0388811660008181526002602090815260408083208c84528252808320899055805180820182528481528083018d8152878716808652600390945293829020905181546001600160a01b0319169616959095178555915160019094019390935551630217888360e11b81526004810191909152602481018990529193508392509063042f1106906044016020604051808303816000875af1158015610bb4573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610bd89190611267565b50610c6f565b6001600160a01b0387811660009081526002602090815260408083208a8452909152908190205490516355bb7d6360e11b8152600481018290529094509085169063ab76fac690602401602060405180830381865afa158015610c45573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610c699190611284565b91508190505b60405163d2418ca760e01b81526001600160801b03861660048201526001600160a01b0382169063d2418ca7906024016020604051808303816000875af1158015610cbe573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610ce29190611267565b5060405163a9059cbb60e01b81523360048201526001600160801b03861660248201526001600160a01b0382169063a9059cbb906044016020604051808303816000875af1158015610d38573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d5c9190611267565b50604080516001600160a01b03808a168252602082018990528416918101919091526001600160801b03861660608201527f29f372538523984f33874da1b50e596ce0f180a6eb04d7ff22bb2ce80e1576b6906080015b60405180910390a150505050505050565b6004546001600160a01b03163314610dee5760405162461bcd60e51b81526004016100e7906110fc565b6000546001600160a01b031615610e475760405162461bcd60e51b815260206004820152601d60248201527f52465420636f6c6c656374696f6e20697320616c72656164792073657400000060448201526064016100e7565b6040516344a68ad560e01b8152736c4e9fe1ae37a41e93cee429e8e1881abdcbb54f9081906344a68ad590610e8a908a908a908a908a908a908a9060040161134e565b6020604051808303816000875af1158015610ea9573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610ecd9190611284565b600080546001600160a01b0319166001600160a01b039290921691821790556040519081527f7186a599bf2297b1f4c8957d30b0965291eee0021a5f9a0aeb54dcbd1ffdceef90602001610db3565b6001600160a01b0381168114610f3157600080fd5b50565b600060208284031215610f4657600080fd5b8135610f5181610f1c565b9392505050565b8015158114610f3157600080fd5b60008060408385031215610f7957600080fd5b8235610f8481610f1c565b91506020830135610f9481610f58565b809150509250929050565b60008060408385031215610fb257600080fd5b8235610fbd81610f1c565b946020939093013593505050565b600080600060608486031215610fe057600080fd5b8335610feb81610f1c565b92506020840135915060408401356001600160801b038116811461100e57600080fd5b809150509250925092565b60008083601f84011261102b57600080fd5b50813567ffffffffffffffff81111561104357600080fd5b60208301915083602082850101111561105b57600080fd5b9250929050565b6000806000806000806060878903121561107b57600080fd5b863567ffffffffffffffff8082111561109357600080fd5b61109f8a838b01611019565b909850965060208901359150808211156110b857600080fd5b6110c48a838b01611019565b909650945060408901359150808211156110dd57600080fd5b506110ea89828a01611019565b979a9699509497509295939492505050565b6020808252600e908201526d27b7363c9037bbb732b91031b0b760911b604082015260600190565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561114d57600080fd5b825167ffffffffffffffff8082111561116557600080fd5b818501915085601f83011261117957600080fd5b81518181111561118b5761118b611124565b604051601f8201601f19908116603f011681019083821181831017156111b3576111b3611124565b8160405282815288868487010111156111cb57600080fd5b600093505b828410156111ed57848401860151818501870152928501926111d0565b828411156111fe5760008684830101525b98975050505050505050565b6020808252603c908201527f4672616374696f6e616c697a657220636f6e74726163742073686f756c64206260408201527f6520616e2061646d696e206f662074686520636f6c6c656374696f6e00000000606082015260800190565b60006020828403121561127957600080fd5b8151610f5181610f58565b60006020828403121561129657600080fd5b8151610f5181610f1c565b6000602082840312156112b357600080fd5b5051919050565b6001600160a01b039384168152919092166020820152604081019190915260600190565b60208082526027908201527f4f6e6c7920746f6b656e206f776e657220636f756c64206672616374696f6e616040820152661b1a5e99481a5d60ca1b606082015260800190565b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b60608152600061136260608301888a611325565b8281036020840152611375818789611325565b9050828103604084015261138a818587611325565b999850505050505050505056fea264697066735822122090a1bec1bb2c2216359e5312618175ac644676d83b7b03f8f2c378700aedd5fe64736f6c634300080f0033
\ No newline at end of file
addedtests/src/eth/fractionalizer/Fractionalizer.soldiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/fractionalizer/Fractionalizer.sol
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier:  Apache License
+pragma solidity >=0.8.0;
+import {CollectionHelpers} from "../api/CollectionHelpers.sol";
+import {ContractHelpers} from "../api/ContractHelpers.sol";
+import {UniqueRefungibleToken} from "../api/UniqueRefungibleToken.sol";
+import {UniqueRefungible} from "../api/UniqueRefungible.sol";
+import {UniqueNFT} from "../api/UniqueNFT.sol";
+
+contract Fractionalizer {
+    struct Token {
+        address _collection;
+        uint256 _tokenId;
+    }
+    address rftCollection;
+    mapping(address => bool) nftCollectionAllowList;
+    mapping(address => mapping(uint256 => uint256)) nft2rftMapping;
+    mapping(address => Token) rft2nftMapping;
+    address owner;
+    bytes32 refungibleCollectionType = keccak256(bytes("ReFungible"));
+
+    constructor() {
+        owner = msg.sender;
+    }
+
+    modifier onlyOwner() {
+        require(msg.sender == owner, "Only owner can");
+        _;
+    }
+
+    event RFTCollectionSet(address _collection);
+    event AllowListSet(address _collection, bool _status);
+    event Fractionalized(address _collection, uint256 _tokenId, address _rftToken, uint128 _amount);
+    event DeFractionalized(address _rftToken, address _nftCollection, uint256 _nftTokenId);
+
+    function setRFTCollection(address _collection) public onlyOwner {
+        require(
+            rftCollection == address(0),
+            "RFT collection is already set"
+        );
+        UniqueRefungible refungibleContract = UniqueRefungible(_collection);
+        string memory collectionType = refungibleContract.uniqueCollectionType();
+        
+        require(
+            keccak256(bytes(collectionType)) == refungibleCollectionType,
+            "Fractionalizer contract should be an admin of the collection"
+        );
+        require(
+            refungibleContract.verifyOwnerOrAdmin(),
+            "Fractionalizer contract should be an admin of the collection"
+        );
+        rftCollection = _collection;
+        emit RFTCollectionSet(rftCollection);
+    }
+
+    function mintRFTCollection(string calldata _name, string calldata _description, string calldata _tokenPrefix) public onlyOwner {
+        require(
+            rftCollection == address(0),
+            "RFT collection is already set"
+        );
+        address collectionHelpers = 0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F;
+        rftCollection = CollectionHelpers(collectionHelpers).createRefungibleCollection(_name, _description, _tokenPrefix);
+        emit RFTCollectionSet(rftCollection);
+    }
+
+    function setAllowlist(address collection, bool status) public onlyOwner {
+        nftCollectionAllowList[collection] = status;
+        emit AllowListSet(collection, status);
+    }
+
+    function nft2rft(address _collection, uint256 _token, uint128 _pieces) public {
+        require(
+            rftCollection != address(0),
+            "RFT collection is not set"
+        );
+        UniqueRefungible rftCollectionContract = UniqueRefungible(rftCollection);
+        require(
+            UniqueNFT(_collection).ownerOf(_token) == msg.sender,
+            "Only token owner could fractionalize it"
+        );
+        require(
+            nftCollectionAllowList[_collection] == true,
+            "Fractionalization of this collection is not allowed by admin"
+        );
+        require(
+            UniqueNFT(_collection).ownerOf(_token) == msg.sender,
+            "Only token owner could fractionalize it"
+        );
+        UniqueNFT(_collection).transferFrom(
+            msg.sender,
+            address(this),
+            _token
+        );
+        uint256 rftTokenId;
+        address rftTokenAddress;
+        UniqueRefungibleToken rftTokenContract;
+        if (nft2rftMapping[_collection][_token] == 0) {
+            rftTokenId = rftCollectionContract.nextTokenId();
+            rftCollectionContract.mint(address(this), rftTokenId);
+            rftTokenAddress = rftCollectionContract.tokenContractAddress(rftTokenId);
+            nft2rftMapping[_collection][_token] = rftTokenId;
+            rft2nftMapping[rftTokenAddress] = Token(_collection, _token);
+
+            rftTokenContract = UniqueRefungibleToken(rftTokenAddress);
+            rftTokenContract.setParentNFT(_collection, _token);
+        } else {
+            rftTokenId = nft2rftMapping[_collection][_token];
+            rftTokenAddress = rftCollectionContract.tokenContractAddress(rftTokenId);
+            rftTokenContract = UniqueRefungibleToken(rftTokenAddress);
+        }
+        rftTokenContract.repartition(_pieces);
+        rftTokenContract.transfer(msg.sender, _pieces);
+        emit Fractionalized(_collection, _token, rftTokenAddress, _pieces);
+    }
+
+    function rft2nft(address _collection, uint256 _token) public {
+        require(
+            rftCollection != address(0),
+            "RFT collection is not set"
+        );
+        require(
+            rftCollection == _collection,
+            "Wrong RFT collection"
+        );
+        UniqueRefungible rftCollectionContract = UniqueRefungible(rftCollection);
+        address rftTokenAddress = rftCollectionContract.tokenContractAddress(_token);
+        Token memory nftToken = rft2nftMapping[rftTokenAddress];
+        require(
+            nftToken._collection != address(0),
+            "No corresponding NFT token found"
+        );
+        UniqueRefungibleToken rftTokenContract = UniqueRefungibleToken(rftTokenAddress);
+        require(
+            rftTokenContract.balanceOf(msg.sender) == rftTokenContract.totalSupply(),
+            "Not all pieces are owned by the caller"
+        );
+        rftCollectionContract.transferFrom(msg.sender, address(this), _token);
+        UniqueNFT(nftToken._collection).transferFrom(
+            address(this),
+            msg.sender,
+            nftToken._tokenId
+        );
+        emit DeFractionalized(rftTokenAddress, nftToken._collection, nftToken._tokenId);
+    }
+}
\ No newline at end of file
addedtests/src/eth/fractionalizer/FractionalizerAbi.jsondiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/fractionalizer/FractionalizerAbi.json
@@ -0,0 +1 @@
+[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_collection","type":"address"},{"indexed":false,"internalType":"bool","name":"_status","type":"bool"}],"name":"AllowListSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_rftToken","type":"address"},{"indexed":false,"internalType":"address","name":"_nftCollection","type":"address"},{"indexed":false,"internalType":"uint256","name":"_nftTokenId","type":"uint256"}],"name":"DeFractionalized","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_collection","type":"address"},{"indexed":false,"internalType":"uint256","name":"_tokenId","type":"uint256"},{"indexed":false,"internalType":"address","name":"_rftToken","type":"address"},{"indexed":false,"internalType":"uint128","name":"_amount","type":"uint128"}],"name":"Fractionalized","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_collection","type":"address"}],"name":"RFTCollectionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"msg","type":"string"}],"name":"Test","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes","name":"msg","type":"bytes"}],"name":"TestB","type":"event"},{"inputs":[{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_description","type":"string"},{"internalType":"string","name":"_tokenPrefix","type":"string"}],"name":"mintRFTCollection","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_collection","type":"address"},{"internalType":"uint256","name":"_token","type":"uint256"},{"internalType":"uint128","name":"_pieces","type":"uint128"}],"name":"nft2rft","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_collection","type":"address"},{"internalType":"uint256","name":"_token","type":"uint256"}],"name":"rft2nft","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"collection","type":"address"},{"internalType":"bool","name":"status","type":"bool"}],"name":"setAllowlist","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_collection","type":"address"}],"name":"setRFTCollection","outputs":[],"stateMutability":"nonpayable","type":"function"}]
\ No newline at end of file
addedtests/src/eth/fractionalizer/fractionalizer.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/fractionalizer/fractionalizer.test.ts
@@ -0,0 +1,176 @@
+// 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 Web3 from 'web3';
+import {ApiPromise} from '@polkadot/api';
+import {evmToAddress} from '@polkadot/util-crypto';
+import {readFile} from 'fs/promises';
+import fractionalizerAbi from './FractionalizerAbi.json';
+import {submitTransactionAsync} from '../../substrate/substrate-api';
+import {UNIQUE} from '../../util/helpers';
+import {collectionIdToAddress, createEthAccountWithBalance, createNonfungibleCollection, createRefungibleCollection, GAS_ARGS, itWeb3, tokenIdFromAddress, uniqueNFT, uniqueRefungible, uniqueRefungibleToken} from '../util/helpers';
+import {Contract} from 'web3-eth-contract';
+
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import chaiLike from 'chai-like';
+import {IKeyringPair} from '@polkadot/types/types';
+chai.use(chaiAsPromised);
+chai.use(chaiLike);
+const expect = chai.expect;
+
+async function deployFractionalizer(api: ApiPromise, web3: Web3, owner: string) {
+  const fractionalizerContract = new web3.eth.Contract(fractionalizerAbi as any, undefined, {
+    from: owner,
+    ...GAS_ARGS,
+  });
+  return await fractionalizerContract.deploy({data: (await readFile(`${__dirname}/Fractionalizer.bin`)).toString()}).send({from: owner});
+}
+
+async function initFractionalizer(api: ApiPromise, web3: Web3, privateKeyWrapper: (account: string) => IKeyringPair, owner: string) {
+  const fractionalizer = await deployFractionalizer(api, web3, owner);
+  const tx = api.tx.balances.transfer(evmToAddress(fractionalizer.options.address), 10n * UNIQUE);
+  const alice = privateKeyWrapper('//Alice');
+  await submitTransactionAsync(alice, tx);
+  const result = await fractionalizer.methods.mintRFTCollection('A', 'B', 'C').send();
+  const rftCollectionAddress = result.events.RFTCollectionSet.returnValues._collection;
+  return {fractionalizer, rftCollectionAddress};
+}
+
+async function createRFTToken(api: ApiPromise, web3: Web3, owner: string, fractionalizer: Contract, amount: bigint) {
+  const {collectionIdAddress: nftCollectionAddress} = await createNonfungibleCollection(api, web3, owner);
+  const nftContract = uniqueNFT(web3, nftCollectionAddress, owner);
+  const nftTokenId = await nftContract.methods.nextTokenId().call();
+  await nftContract.methods.mint(owner, nftTokenId).send();
+
+  await fractionalizer.methods.setAllowlist(nftCollectionAddress, true).send();
+  await nftContract.methods.approve(fractionalizer.options.address, nftTokenId).send();
+  const result = await fractionalizer.methods.nft2rft(nftCollectionAddress, nftTokenId, amount).send();
+  const {_collection, _tokenId, _rftToken} = result.events.Fractionalized.returnValues;
+  return {
+    nftCollectionAddress: _collection,
+    nftTokenId: _tokenId,
+    rftTokenAddress: _rftToken,
+  };
+}
+
+describe('Fractionalizer contract usage', () => {
+  itWeb3('Set RFT collection', async ({api, web3, privateKeyWrapper}) => {
+    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const fractionalizer = await deployFractionalizer(api, web3, owner);
+    const {collectionIdAddress} = await createRefungibleCollection(api, web3, owner);
+    const refungibleContract = uniqueRefungible(web3, collectionIdAddress, owner);
+    await refungibleContract.methods.addCollectionAdmin(fractionalizer.options.address).send();
+    const result = await fractionalizer.methods.setRFTCollection(collectionIdAddress).send();
+    expect(result.events).to.be.like({
+      RFTCollectionSet: {
+        returnValues: {
+          _collection: collectionIdAddress,
+        },
+      },
+    });
+  });
+
+  itWeb3('Mint RFT collection', async ({api, web3, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const fractionalizer = await deployFractionalizer(api, web3, owner);
+    const tx = api.tx.balances.transfer(evmToAddress(fractionalizer.options.address), 10n * UNIQUE);
+    await submitTransactionAsync(alice, tx);
+
+    const result = await fractionalizer.methods.mintRFTCollection('A', 'B', 'C').send({from: owner});
+    expect(result.events).to.be.like({
+      RFTCollectionSet: {},
+    });
+    expect(result.events.RFTCollectionSet.returnValues._collection).to.be.ok;
+  });
+
+  itWeb3('Set Allowlist', async ({api, web3, privateKeyWrapper}) => {
+    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const {fractionalizer} = await initFractionalizer(api, web3, privateKeyWrapper, owner);    
+
+    const {collectionIdAddress: nftCollectionAddress} = await createNonfungibleCollection(api, web3, owner);
+    const result1 = await fractionalizer.methods.setAllowlist(nftCollectionAddress, true).send({from: owner});
+    expect(result1.events).to.be.like({
+      AllowListSet: {
+        returnValues: {
+          _collection: nftCollectionAddress,
+          _status: true,
+        },
+      },
+    });
+    const result2 = await fractionalizer.methods.setAllowlist(nftCollectionAddress, false).send({from: owner});
+    expect(result2.events).to.be.like({
+      AllowListSet: {
+        returnValues: {
+          _collection: nftCollectionAddress,
+          _status: false,
+        },
+      },
+    });
+  });
+
+  itWeb3('NFT to RFT', async ({api, web3, privateKeyWrapper}) => {
+    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const {collectionIdAddress: nftCollectionAddress} = await createNonfungibleCollection(api, web3, owner);
+    const nftContract = uniqueNFT(web3, nftCollectionAddress, owner);
+    const nftTokenId = await nftContract.methods.nextTokenId().call();
+    await nftContract.methods.mint(owner, nftTokenId).send();
+
+    const {fractionalizer} = await initFractionalizer(api, web3, privateKeyWrapper, owner);
+
+    await fractionalizer.methods.setAllowlist(nftCollectionAddress, true).send();
+    await nftContract.methods.approve(fractionalizer.options.address, nftTokenId).send();
+    const result = await fractionalizer.methods.nft2rft(nftCollectionAddress, nftTokenId, 100).send();
+    expect(result.events).to.be.like({
+      Fractionalized: {
+        returnValues: {
+          _collection: nftCollectionAddress,
+          _tokenId: nftTokenId,
+          _amount: '100',
+        },
+      },
+    });
+    const rftTokenAddress = result.events.Fractionalized.returnValues._rftToken;
+    const rftTokenContract = uniqueRefungibleToken(web3, rftTokenAddress, owner);
+    expect(await rftTokenContract.methods.balanceOf(owner).call()).to.equal('100');
+  });
+
+  itWeb3('RFT to NFT', async ({api, web3, privateKeyWrapper}) => {
+    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const {fractionalizer, rftCollectionAddress} = await initFractionalizer(api, web3, privateKeyWrapper, owner);
+    const {rftTokenAddress, nftCollectionAddress, nftTokenId} = await createRFTToken(api, web3, owner, fractionalizer, 100n);
+
+    const {collectionId, tokenId} = tokenIdFromAddress(rftTokenAddress);
+    const refungibleAddress = collectionIdToAddress(collectionId);
+    expect(rftCollectionAddress).to.be.equal(refungibleAddress);
+    const refungibleTokenContract = uniqueRefungibleToken(web3, rftTokenAddress, owner);
+    await refungibleTokenContract.methods.approve(fractionalizer.options.address, 100).send();
+    const result = await fractionalizer.methods.rft2nft(refungibleAddress, tokenId).send();
+    expect(result.events).to.be.like({
+      DeFractionalized: {
+        returnValues: {
+          _rftToken: rftTokenAddress,
+          _nftCollection: nftCollectionAddress,
+          _nftTokenId: nftTokenId,
+        },
+      },
+    });
+  });
+});
\ No newline at end of file
modifiedtests/src/eth/util/helpers.tsdiffbeforeafterboth
before · tests/src/eth/util/helpers.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617// eslint-disable-next-line @typescript-eslint/triple-slash-reference18/// <reference path="helpers.d.ts" />1920import {ApiPromise} from '@polkadot/api';21import {IKeyringPair} from '@polkadot/types/types';22import {addressToEvm, evmToAddress} from '@polkadot/util-crypto';23import {expect} from 'chai';24import * as solc from 'solc';25import Web3 from 'web3';26import config from '../../config';27import getBalance from '../../substrate/get-balance';28import usingApi, {submitTransactionAsync} from '../../substrate/substrate-api';29import waitNewBlocks from '../../substrate/wait-new-blocks';30import {CollectionMode, CrossAccountId, getDetailedCollectionInfo, getGenericResult, UNIQUE} from '../../util/helpers';31import collectionHelpersAbi from '../collectionHelpersAbi.json';32import fungibleAbi from '../fungibleAbi.json';33import nonFungibleAbi from '../nonFungibleAbi.json';34import refungibleAbi from '../reFungibleAbi.json';35import contractHelpersAbi from './contractHelpersAbi.json';3637export const GAS_ARGS = {gas: 2500000};3839export enum SponsoringMode {40  Disabled = 0,41  Allowlisted = 1,42  Generous = 2,43}4445let web3Connected = false;46export async function usingWeb3<T>(cb: (web3: Web3) => Promise<T> | T): Promise<T> {47  if (web3Connected) throw new Error('do not nest usingWeb3 calls');48  web3Connected = true;4950  const provider = new Web3.providers.WebsocketProvider(config.substrateUrl);51  const web3 = new Web3(provider);5253  try {54    return await cb(web3);55  } finally {56    // provider.disconnect(3000, 'normal disconnect');57    provider.connection.close();58    web3Connected = false;59  }60}6162function encodeIntBE(v: number): number[] {63  if (v >= 0xffffffff || v < 0) throw new Error('id overflow');64  return [65    v >> 24,66    (v >> 16) & 0xff,67    (v >> 8) & 0xff,68    v & 0xff,69  ];70}7172export async function getCollectionAddressFromResult(api: ApiPromise, result: any) {73  const collectionIdAddress = normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);74  const collectionId = collectionIdFromAddress(collectionIdAddress);  75  const collection = (await getDetailedCollectionInfo(api, collectionId))!;76  return {collectionIdAddress, collectionId, collection};77}7879export function collectionIdToAddress(collection: number): string {80  const buf = Buffer.from([0x17, 0xc4, 0xe6, 0x45, 0x3c, 0xc4, 0x9a, 0xaa, 0xae, 0xac, 0xa8, 0x94, 0xe6, 0xd9, 0x68, 0x3e,81    ...encodeIntBE(collection),82  ]);83  return Web3.utils.toChecksumAddress('0x' + buf.toString('hex'));84}85export function collectionIdFromAddress(address: string): number {86  if (!address.startsWith('0x'))87    throw 'address not starts with "0x"';88  if (address.length > 42)89    throw 'address length is more than 20 bytes';90  return Number('0x' + address.substring(address.length - 8));91}92  93export function normalizeAddress(address: string): string {94  return '0x' + address.substring(address.length - 40);95}9697export function tokenIdToAddress(collection: number, token: number): string {98  const buf = Buffer.from([0xf8, 0x23, 0x8c, 0xcf, 0xff, 0x8e, 0xd8, 0x87, 0x46, 0x3f, 0xd5, 0xe0,99    ...encodeIntBE(collection),100    ...encodeIntBE(token),101  ]);102  return Web3.utils.toChecksumAddress('0x' + buf.toString('hex'));103}104export function tokenIdToCross(collection: number, token: number): CrossAccountId {105  return {106    Ethereum: tokenIdToAddress(collection, token),107  };108}109110export function createEthAccount(web3: Web3) {111  const account = web3.eth.accounts.create();112  web3.eth.accounts.wallet.add(account.privateKey);113  return account.address;114}115116export async function createEthAccountWithBalance(api: ApiPromise, web3: Web3, privateKeyWrapper: (account: string) => IKeyringPair) {117  const alice = privateKeyWrapper('//Alice');118  const account = createEthAccount(web3);119  await transferBalanceToEth(api, alice, account);120121  return account;122}123124export async function transferBalanceToEth(api: ApiPromise, source: IKeyringPair, target: string, amount = 1000n * UNIQUE) {125  const tx = api.tx.balances.transfer(evmToAddress(target), amount);126  const events = await submitTransactionAsync(source, tx);127  const result = getGenericResult(events);128  expect(result.success).to.be.true;129}130131export async function itWeb3(name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any, opts: { only?: boolean, skip?: boolean } = {}) {132  let i: any = it;133  if (opts.only) i = i.only;134  else if (opts.skip) i = i.skip;135  i(name, async () => {136    await usingApi(async (api, privateKeyWrapper) => {137      await usingWeb3(async web3 => {138        await cb({api, web3, privateKeyWrapper});139      });140    });141  });142}143itWeb3.only = (name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itWeb3(name, cb, {only: true});144itWeb3.skip = (name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itWeb3(name, cb, {skip: true});145146export async function generateSubstrateEthPair(web3: Web3) {147  const account = web3.eth.accounts.create();148  evmToAddress(account.address);149}150151type NormalizedEvent = {152    address: string,153    event: string,154    args: { [key: string]: string }155};156157export function normalizeEvents(events: any): NormalizedEvent[] {158  const output = [];159  for (const key of Object.keys(events)) {160    if (key.match(/^[0-9]+$/)) {161      output.push(events[key]);162    } else if (Array.isArray(events[key])) {163      output.push(...events[key]);164    } else {165      output.push(events[key]);166    }167  }168  output.sort((a, b) => a.logIndex - b.logIndex);169  return output.map(({address, event, returnValues}) => {170    const args: { [key: string]: string } = {};171    for (const key of Object.keys(returnValues)) {172      if (!key.match(/^[0-9]+$/)) {173        args[key] = returnValues[key];174      }175    }176    return {177      address,178      event,179      args,180    };181  });182}183184export async function recordEvents(contract: any, action: () => Promise<void>): Promise<NormalizedEvent[]> {185  const out: any = [];186  contract.events.allEvents((_: any, event: any) => {187    out.push(event);188  });189  await action();190  return normalizeEvents(out);191}192193export function subToEthLowercase(eth: string): string {194  const bytes = addressToEvm(eth);195  return '0x' + Buffer.from(bytes).toString('hex');196}197198export function subToEth(eth: string): string {199  return Web3.utils.toChecksumAddress(subToEthLowercase(eth));200}201202export function compileContract(name: string, src: string) {203  const out = JSON.parse(solc.compile(JSON.stringify({204    language: 'Solidity',205    sources: {206      [`${name}.sol`]: {207        content: `208          // SPDX-License-Identifier: UNLICENSED209          pragma solidity ^0.8.6;210211          ${src}212        `,213      },214    },215    settings: {216      outputSelection: {217        '*': {218          '*': ['*'],219        },220      },221    },222  }))).contracts[`${name}.sol`][name];223224  return {225    abi: out.abi,226    object: '0x' + out.evm.bytecode.object,227  };228}229230export async function deployFlipper(web3: Web3, deployer: string) {231  const compiled = compileContract('Flipper', `232    contract Flipper {233      bool value = false;234      function flip() public {235        value = !value;236      }237      function getValue() public view returns (bool) {238        return value;239      }240    }241  `);242  const flipperContract = new web3.eth.Contract(compiled.abi, undefined, {243    data: compiled.object,244    from: deployer,245    ...GAS_ARGS,246  });247  const flipper = await flipperContract.deploy({data: compiled.object}).send({from: deployer});248249  return flipper;250}251252export async function deployCollector(web3: Web3, deployer: string) {253  const compiled = compileContract('Collector', `254    contract Collector {255      uint256 collected;256      fallback() external payable {257        giveMoney();258      }259      function giveMoney() public payable {260        collected += msg.value;261      }262      function getCollected() public view returns (uint256) {263        return collected;264      }265      function getUnaccounted() public view returns (uint256) {266        return address(this).balance - collected;267      }268269      function withdraw(address payable target) public {270        target.transfer(collected);271        collected = 0;272      }273    }274  `);275  const collectorContract = new web3.eth.Contract(compiled.abi, undefined, {276    data: compiled.object,277    from: deployer,278    ...GAS_ARGS,279  });280  const collector = await collectorContract.deploy({data: compiled.object}).send({from: deployer});281282  return collector;283}284285/** 286 * pallet evm_contract_helpers287 * @param web3 288 * @param caller - eth address289 * @returns 290 */291export function contractHelpers(web3: Web3, caller: string) {292  return new web3.eth.Contract(contractHelpersAbi as any, '0x842899ECF380553E8a4de75bF534cdf6fBF64049', {from: caller, ...GAS_ARGS});293}294295/** 296 * evm collection helper297 * @param web3 298 * @param caller - eth address299 * @returns 300 */301export function evmCollectionHelpers(web3: Web3, caller: string) {302  return new web3.eth.Contract(collectionHelpersAbi as any, '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f', {from: caller, ...GAS_ARGS});303}304305/** 306 * evm collection307 * @param web3 308 * @param caller - eth address309 * @returns 310 */311export function evmCollection(web3: Web3, caller: string, collection: string, mode: CollectionMode = {type: 'NFT'}) {312  let abi;313  switch (mode.type) {314    case 'Fungible':315      abi = fungibleAbi;316      break;317    318    case 'NFT':319      abi = nonFungibleAbi;320      break;321    322    case 'ReFungible':323      abi = refungibleAbi;324      break;325326    default:327      throw 'Bad collection mode';328  }329  const contract = new web3.eth.Contract(abi as any, collection, {from: caller, ...GAS_ARGS});330  return contract;331}332333/**334 * Execute ethereum method call using substrate account335 * @param to target contract336 * @param mkTx - closure, receiving `contract.methods`, and returning method call,337 * to be used as following (assuming `to` = erc20 contract):338 * `m => m.transfer(to, amount)`339 *340 * # Example341 * ```ts342 * executeEthTxOnSub(api, alice, erc20Contract, m => m.transfer(target, amount));343 * ```344 */345export async function executeEthTxOnSub(web3: Web3, api: ApiPromise, from: IKeyringPair, to: any, mkTx: (methods: any) => any, {value = 0}: {value?: bigint | number} = { }) {346  const tx = api.tx.evm.call(347    subToEth(from.address),348    to.options.address,349    mkTx(to.methods).encodeABI(),350    value,351    GAS_ARGS.gas,352    await web3.eth.getGasPrice(),353    null,354    null,355    [],356  );357  const events = await submitTransactionAsync(from, tx);358  expect(events.some(({event: {section, method}}) => section == 'evm' && method == 'Executed')).to.be.true;359}360361export async function ethBalanceViaSub(api: ApiPromise, address: string): Promise<bigint> {362  return (await getBalance(api, [evmToAddress(address)]))[0];363}364365/**366 * Measure how much gas given closure consumes367 *368 * @param user which user balance will be checked369 */370export async function recordEthFee(api: ApiPromise, user: string, call: () => Promise<any>): Promise<bigint> {371  const before = await ethBalanceViaSub(api, user);372373  await call();374375  // In dev mode, the transaction might not finish processing in time376  await waitNewBlocks(api, 1);377  const after = await ethBalanceViaSub(api, user);378379  // Can't use .to.be.less, because chai doesn't supports bigint380  expect(after < before).to.be.true;381382  return before - after;383}384385type ElementOf<A> = A extends readonly (infer T)[] ? T : never;386// I want a fancier api, not a memory efficiency387export function* cartesian<T extends Array<Array<any>>, R extends Array<any>>(internalRest: [...R], ...args: [...T]): Generator<[...R, ...{[K in keyof T]: ElementOf<T[K]>}]> {388  if(args.length === 0) {389    yield internalRest as any;390    return;391  }392  for(const value of args[0]) {393    yield* cartesian([...internalRest, value], ...args.slice(1)) as any;394  }395}
after · tests/src/eth/util/helpers.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617// eslint-disable-next-line @typescript-eslint/triple-slash-reference18/// <reference path="helpers.d.ts" />1920import {ApiPromise} from '@polkadot/api';21import {IKeyringPair} from '@polkadot/types/types';22import {addressToEvm, evmToAddress} from '@polkadot/util-crypto';23import {expect} from 'chai';24import * as solc from 'solc';25import Web3 from 'web3';26import config from '../../config';27import getBalance from '../../substrate/get-balance';28import usingApi, {submitTransactionAsync} from '../../substrate/substrate-api';29import waitNewBlocks from '../../substrate/wait-new-blocks';30import {CollectionMode, CrossAccountId, getDetailedCollectionInfo, getGenericResult, UNIQUE} from '../../util/helpers';31import collectionHelpersAbi from '../collectionHelpersAbi.json';32import fungibleAbi from '../fungibleAbi.json';33import nonFungibleAbi from '../nonFungibleAbi.json';34import refungibleAbi from '../reFungibleAbi.json';35import refungibleTokenAbi from '../reFungibleTokenAbi.json';36import contractHelpersAbi from './contractHelpersAbi.json';3738export const GAS_ARGS = {gas: 2500000};3940export enum SponsoringMode {41  Disabled = 0,42  Allowlisted = 1,43  Generous = 2,44}4546let web3Connected = false;47export async function usingWeb3<T>(cb: (web3: Web3) => Promise<T> | T): Promise<T> {48  if (web3Connected) throw new Error('do not nest usingWeb3 calls');49  web3Connected = true;5051  const provider = new Web3.providers.WebsocketProvider(config.substrateUrl);52  const web3 = new Web3(provider);5354  try {55    return await cb(web3);56  } finally {57    // provider.disconnect(3000, 'normal disconnect');58    provider.connection.close();59    web3Connected = false;60  }61}6263function encodeIntBE(v: number): number[] {64  if (v >= 0xffffffff || v < 0) throw new Error('id overflow');65  return [66    v >> 24,67    (v >> 16) & 0xff,68    (v >> 8) & 0xff,69    v & 0xff,70  ];71}7273export async function getCollectionAddressFromResult(api: ApiPromise, result: any) {74  const collectionIdAddress = normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);75  const collectionId = collectionIdFromAddress(collectionIdAddress);  76  const collection = (await getDetailedCollectionInfo(api, collectionId))!;77  return {collectionIdAddress, collectionId, collection};78}7980export function collectionIdToAddress(collection: number): string {81  const buf = Buffer.from([0x17, 0xc4, 0xe6, 0x45, 0x3c, 0xc4, 0x9a, 0xaa, 0xae, 0xac, 0xa8, 0x94, 0xe6, 0xd9, 0x68, 0x3e,82    ...encodeIntBE(collection),83  ]);84  return Web3.utils.toChecksumAddress('0x' + buf.toString('hex'));85}86export function collectionIdFromAddress(address: string): number {87  if (!address.startsWith('0x'))88    throw 'address not starts with "0x"';89  if (address.length > 42)90    throw 'address length is more than 20 bytes';91  return Number('0x' + address.substring(address.length - 8));92}93  94export function normalizeAddress(address: string): string {95  return '0x' + address.substring(address.length - 40);96}9798export function tokenIdToAddress(collection: number, token: number): string {99  const buf = Buffer.from([0xf8, 0x23, 0x8c, 0xcf, 0xff, 0x8e, 0xd8, 0x87, 0x46, 0x3f, 0xd5, 0xe0,100    ...encodeIntBE(collection),101    ...encodeIntBE(token),102  ]);103  return Web3.utils.toChecksumAddress('0x' + buf.toString('hex'));104}105106export function tokenIdFromAddress(address: string) {107  if (!address.startsWith('0x'))108    throw 'address not starts with "0x"';109  if (address.length > 42)110    throw 'address length is more than 20 bytes';111  return {112    collectionId: Number('0x' + address.substring(address.length - 16, address.length - 8)),113    tokenId: Number('0x' + address.substring(address.length - 8)),114  };115}116117export function tokenIdToCross(collection: number, token: number): CrossAccountId {118  return {119    Ethereum: tokenIdToAddress(collection, token),120  };121}122123export function createEthAccount(web3: Web3) {124  const account = web3.eth.accounts.create();125  web3.eth.accounts.wallet.add(account.privateKey);126  return account.address;127}128129export async function createEthAccountWithBalance(api: ApiPromise, web3: Web3, privateKeyWrapper: (account: string) => IKeyringPair) {130  const alice = privateKeyWrapper('//Alice');131  const account = createEthAccount(web3);132  await transferBalanceToEth(api, alice, account);133134  return account;135}136137export async function transferBalanceToEth(api: ApiPromise, source: IKeyringPair, target: string, amount = 1000n * UNIQUE) {138  const tx = api.tx.balances.transfer(evmToAddress(target), amount);139  const events = await submitTransactionAsync(source, tx);140  const result = getGenericResult(events);141  expect(result.success).to.be.true;142}143144export async function createRefungibleCollection(api: ApiPromise, web3: Web3, owner: string) {145  const collectionHelper = evmCollectionHelpers(web3, owner);146  const result = await collectionHelper.methods147    .createRefungibleCollection('A', 'B', 'C')148    .send();149  return await getCollectionAddressFromResult(api, result);150}151152153export async function createNonfungibleCollection(api: ApiPromise, web3: Web3, owner: string) {154  const collectionHelper = evmCollectionHelpers(web3, owner);155  const result = await collectionHelper.methods156    .createNonfungibleCollection('A', 'B', 'C')157    .send();158  return await getCollectionAddressFromResult(api, result);159}160161export function uniqueNFT(web3: Web3, address: string, owner: string) {162  return new web3.eth.Contract(nonFungibleAbi as any, address, {163    from: owner,164    ...GAS_ARGS,165  });166}167168export function uniqueRefungible(web3: Web3, collectionAddress: string, owner: string) {169  return new web3.eth.Contract(refungibleAbi as any, collectionAddress, {170    from: owner,171    ...GAS_ARGS,172  });173}174175export function uniqueRefungibleToken(web3: Web3, tokenAddress: string, owner: string) {176  return new web3.eth.Contract(refungibleTokenAbi as any, tokenAddress, {177    from: owner,178    ...GAS_ARGS,179  });180}181182export async function itWeb3(name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any, opts: { only?: boolean, skip?: boolean } = {}) {183  let i: any = it;184  if (opts.only) i = i.only;185  else if (opts.skip) i = i.skip;186  i(name, async () => {187    await usingApi(async (api, privateKeyWrapper) => {188      await usingWeb3(async web3 => {189        await cb({api, web3, privateKeyWrapper});190      });191    });192  });193}194itWeb3.only = (name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itWeb3(name, cb, {only: true});195itWeb3.skip = (name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itWeb3(name, cb, {skip: true});196197export async function generateSubstrateEthPair(web3: Web3) {198  const account = web3.eth.accounts.create();199  evmToAddress(account.address);200}201202type NormalizedEvent = {203    address: string,204    event: string,205    args: { [key: string]: string }206};207208export function normalizeEvents(events: any): NormalizedEvent[] {209  const output = [];210  for (const key of Object.keys(events)) {211    if (key.match(/^[0-9]+$/)) {212      output.push(events[key]);213    } else if (Array.isArray(events[key])) {214      output.push(...events[key]);215    } else {216      output.push(events[key]);217    }218  }219  output.sort((a, b) => a.logIndex - b.logIndex);220  return output.map(({address, event, returnValues}) => {221    const args: { [key: string]: string } = {};222    for (const key of Object.keys(returnValues)) {223      if (!key.match(/^[0-9]+$/)) {224        args[key] = returnValues[key];225      }226    }227    return {228      address,229      event,230      args,231    };232  });233}234235export async function recordEvents(contract: any, action: () => Promise<void>): Promise<NormalizedEvent[]> {236  const out: any = [];237  contract.events.allEvents((_: any, event: any) => {238    out.push(event);239  });240  await action();241  return normalizeEvents(out);242}243244export function subToEthLowercase(eth: string): string {245  const bytes = addressToEvm(eth);246  return '0x' + Buffer.from(bytes).toString('hex');247}248249export function subToEth(eth: string): string {250  return Web3.utils.toChecksumAddress(subToEthLowercase(eth));251}252253export function compileContract(name: string, src: string) {254  const out = JSON.parse(solc.compile(JSON.stringify({255    language: 'Solidity',256    sources: {257      [`${name}.sol`]: {258        content: `259          // SPDX-License-Identifier: UNLICENSED260          pragma solidity ^0.8.6;261262          ${src}263        `,264      },265    },266    settings: {267      outputSelection: {268        '*': {269          '*': ['*'],270        },271      },272    },273  }))).contracts[`${name}.sol`][name];274275  return {276    abi: out.abi,277    object: '0x' + out.evm.bytecode.object,278  };279}280281export async function deployFlipper(web3: Web3, deployer: string) {282  const compiled = compileContract('Flipper', `283    contract Flipper {284      bool value = false;285      function flip() public {286        value = !value;287      }288      function getValue() public view returns (bool) {289        return value;290      }291    }292  `);293  const flipperContract = new web3.eth.Contract(compiled.abi, undefined, {294    data: compiled.object,295    from: deployer,296    ...GAS_ARGS,297  });298  const flipper = await flipperContract.deploy({data: compiled.object}).send({from: deployer});299300  return flipper;301}302303export async function deployCollector(web3: Web3, deployer: string) {304  const compiled = compileContract('Collector', `305    contract Collector {306      uint256 collected;307      fallback() external payable {308        giveMoney();309      }310      function giveMoney() public payable {311        collected += msg.value;312      }313      function getCollected() public view returns (uint256) {314        return collected;315      }316      function getUnaccounted() public view returns (uint256) {317        return address(this).balance - collected;318      }319320      function withdraw(address payable target) public {321        target.transfer(collected);322        collected = 0;323      }324    }325  `);326  const collectorContract = new web3.eth.Contract(compiled.abi, undefined, {327    data: compiled.object,328    from: deployer,329    ...GAS_ARGS,330  });331  const collector = await collectorContract.deploy({data: compiled.object}).send({from: deployer});332333  return collector;334}335336/** 337 * pallet evm_contract_helpers338 * @param web3 339 * @param caller - eth address340 * @returns 341 */342export function contractHelpers(web3: Web3, caller: string) {343  return new web3.eth.Contract(contractHelpersAbi as any, '0x842899ECF380553E8a4de75bF534cdf6fBF64049', {from: caller, ...GAS_ARGS});344}345346/** 347 * evm collection helper348 * @param web3 349 * @param caller - eth address350 * @returns 351 */352export function evmCollectionHelpers(web3: Web3, caller: string) {353  return new web3.eth.Contract(collectionHelpersAbi as any, '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f', {from: caller, ...GAS_ARGS});354}355356/** 357 * evm collection358 * @param web3 359 * @param caller - eth address360 * @returns 361 */362export function evmCollection(web3: Web3, caller: string, collection: string, mode: CollectionMode = {type: 'NFT'}) {363  let abi;364  switch (mode.type) {365    case 'Fungible':366      abi = fungibleAbi;367      break;368    369    case 'NFT':370      abi = nonFungibleAbi;371      break;372    373    case 'ReFungible':374      abi = refungibleAbi;375      break;376377    default:378      throw 'Bad collection mode';379  }380  const contract = new web3.eth.Contract(abi as any, collection, {from: caller, ...GAS_ARGS});381  return contract;382}383384/**385 * Execute ethereum method call using substrate account386 * @param to target contract387 * @param mkTx - closure, receiving `contract.methods`, and returning method call,388 * to be used as following (assuming `to` = erc20 contract):389 * `m => m.transfer(to, amount)`390 *391 * # Example392 * ```ts393 * executeEthTxOnSub(api, alice, erc20Contract, m => m.transfer(target, amount));394 * ```395 */396export async function executeEthTxOnSub(web3: Web3, api: ApiPromise, from: IKeyringPair, to: any, mkTx: (methods: any) => any, {value = 0}: {value?: bigint | number} = { }) {397  const tx = api.tx.evm.call(398    subToEth(from.address),399    to.options.address,400    mkTx(to.methods).encodeABI(),401    value,402    GAS_ARGS.gas,403    await web3.eth.getGasPrice(),404    null,405    null,406    [],407  );408  const events = await submitTransactionAsync(from, tx);409  expect(events.some(({event: {section, method}}) => section == 'evm' && method == 'Executed')).to.be.true;410}411412export async function ethBalanceViaSub(api: ApiPromise, address: string): Promise<bigint> {413  return (await getBalance(api, [evmToAddress(address)]))[0];414}415416/**417 * Measure how much gas given closure consumes418 *419 * @param user which user balance will be checked420 */421export async function recordEthFee(api: ApiPromise, user: string, call: () => Promise<any>): Promise<bigint> {422  const before = await ethBalanceViaSub(api, user);423424  await call();425426  // In dev mode, the transaction might not finish processing in time427  await waitNewBlocks(api, 1);428  const after = await ethBalanceViaSub(api, user);429430  // Can't use .to.be.less, because chai doesn't supports bigint431  expect(after < before).to.be.true;432433  return before - after;434}435436type ElementOf<A> = A extends readonly (infer T)[] ? T : never;437// I want a fancier api, not a memory efficiency438export function* cartesian<T extends Array<Array<any>>, R extends Array<any>>(internalRest: [...R], ...args: [...T]): Generator<[...R, ...{[K in keyof T]: ElementOf<T[K]>}]> {439  if(args.length === 0) {440    yield internalRest as any;441    return;442  }443  for(const value of args[0]) {444    yield* cartesian([...internalRest, value], ...args.slice(1)) as any;445  }446}
modifiedtests/yarn.lockdiffbeforeafterboth
--- a/tests/yarn.lock
+++ b/tests/yarn.lock
@@ -963,6 +963,20 @@
   dependencies:
     "@types/chai" "*"
 
+"@types/chai-like@^1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@types/chai-like/-/chai-like-1.1.1.tgz#c454039b0a2f92664fb5b7b7a2a66c3358783ae7"
+  integrity sha512-s46EZsupBuVhLn66DbRee5B0SELLmL4nFXVrBiV29BxLGm9Sh7Bful623j3AfiQRu2zAP4cnlZ3ETWB3eWc4bA==
+  dependencies:
+    "@types/chai" "*"
+
+"@types/chai-things@^0.0.35":
+  version "0.0.35"
+  resolved "https://registry.yarnpkg.com/@types/chai-things/-/chai-things-0.0.35.tgz#4b5d9ec032067faa62b3bf7bb40dc0bec941945f"
+  integrity sha512-BC8FwMf9FHj87XT4dgTwbdb8dNRilGqYWGmwLPdJ54YNk6K2PlcFTt68NGHjgPDnms8zIYcOtmPePd0mPNTo/Q==
+  dependencies:
+    "@types/chai" "*"
+
 "@types/chai@*", "@types/chai@^4.3.1":
   version "4.3.1"
   resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.1.tgz#e2c6e73e0bdeb2521d00756d099218e9f5d90a04"
@@ -1545,6 +1559,16 @@
   dependencies:
     check-error "^1.0.2"
 
+chai-like@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/chai-like/-/chai-like-1.1.1.tgz#8c558a414c34514e814d497c772547ceb7958f64"
+  integrity sha512-VKa9z/SnhXhkT1zIjtPACFWSoWsqVoaz1Vg+ecrKo5DCKVlgL30F/pEyEvXPBOVwCgLZcWUleCM/C1okaKdTTA==
+
+chai-things@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/chai-things/-/chai-things-0.2.0.tgz#c55128378f9bb399e994f00052151984ed6ebe70"
+  integrity sha512-6ns0SU21xdRCoEXVKH3HGbwnsgfVMXQ+sU5V8PI9rfxaITos8lss1vUxbF1FAcJKjfqmmmLVlr/z3sLes00w+A==
+
 chai@^4.3.6:
   version "4.3.6"
   resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.6.tgz#ffe4ba2d9fa9d6680cc0b370adae709ec9011e9c"