From 4dba5498649bc5dcdde642795984b567e80c3734 Mon Sep 17 00:00:00 2001 From: Jan Liphardt <jan@enya.ai> Date: Wed, 5 May 2021 21:31:51 -0700 Subject: [PATCH 01/10] wip --- contracts/NFT/ERC721.sol | 70 +++-- contracts/NFT/ERC721Mock.sol | 76 +++++ contracts/NFT/ERC721PausableMock.sol | 41 +++ contracts/NFT/ERC721ReceiverMock.sol | 39 +++ contracts/NFT/access/Ownable.sol | 68 +++++ contracts/NFT/extensions/ERC721URIStorage.sol | 66 ----- .../ERC721PresetMinterPauserAutoId.sol | 116 -------- deployment/local/addresses.json | 10 +- {test => optional}/setup_test.spec.ts | 74 +++-- test/nft.spec.ts | 272 ++++++++++++++++++ test/shared/env.ts | 6 +- 11 files changed, 602 insertions(+), 236 deletions(-) create mode 100644 contracts/NFT/ERC721Mock.sol create mode 100644 contracts/NFT/ERC721PausableMock.sol create mode 100644 contracts/NFT/ERC721ReceiverMock.sol create mode 100644 contracts/NFT/access/Ownable.sol delete mode 100644 contracts/NFT/extensions/ERC721URIStorage.sol delete mode 100644 contracts/NFT/presets/ERC721PresetMinterPauserAutoId.sol rename {test => optional}/setup_test.spec.ts (93%) create mode 100644 test/nft.spec.ts diff --git a/contracts/NFT/ERC721.sol b/contracts/NFT/ERC721.sol index 8c16911c3c8d..0fd40e79b397 100644 --- a/contracts/NFT/ERC721.sol +++ b/contracts/NFT/ERC721.sol @@ -16,6 +16,7 @@ import "./utils/introspection/ERC165.sol"; * {ERC721Enumerable}. */ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { + using Address for address; using Strings for uint256; @@ -25,6 +26,11 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { // Token symbol string private _symbol; + string private _baseURI; + + // Optional mapping for token URIs + mapping (uint256 => string) private _tokenURIs; + // Mapping from token ID to owner address mapping (uint256 => address) private _owners; @@ -71,23 +77,16 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { return owner; } - /** - * @dev See {IERC721Metadata-name}. - */ - function name() public view virtual override returns (string memory) { - return _name; - } - - /** - * @dev See {IERC721Metadata-symbol}. - */ - function symbol() public view virtual override returns (string memory) { - return _symbol; - } + function name() public view virtual override returns (string memory) { return _name; } + + function symbol() public view virtual override returns (string memory) { return _symbol; } + + function baseURI() public view virtual returns (string memory) { return _baseURI; } /** * @dev See {IERC721Metadata-tokenURI}. */ + /* function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token"); @@ -96,15 +95,46 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { ? string(abi.encodePacked(baseURI, tokenId.toString())) : ''; } - + */ + /** - * @dev Base URI for computing {tokenURI}. Empty by default, can be overriden - * in child contracts. + * @dev See {IERC721Metadata-tokenURI}. */ - function _baseURI() internal view virtual returns (string memory) { + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + + require(_exists(tokenId), "ERC721URIStorage: URI query for nonexistent token"); + + string memory _tokenURI = _tokenURIs[tokenId]; + string memory base = _baseURI; + + // If there is no base URI, return the token URI. + if (bytes(base).length == 0) { + return _tokenURI; + } + // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked). + if (bytes(_tokenURI).length > 0) { + return string(abi.encodePacked(base, _tokenURI)); + } + return ""; } + /** + * @dev Sets `_tokenURI` as the tokenURI of `tokenId`. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual { + require(_exists(tokenId), "ERC721URIStorage: URI set of nonexistent token"); + _tokenURIs[tokenId] = _tokenURI; + } + + function _setBaseURI(string calldata newBaseURI) internal virtual { + _baseURI = newBaseURI; + } + /** * @dev See {IERC721-approve}. */ @@ -276,6 +306,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * Emits a {Transfer} event. */ function _burn(uint256 tokenId) internal virtual { + address owner = ERC721.ownerOf(tokenId); _beforeTokenTransfer(owner, address(0), tokenId); @@ -286,7 +317,12 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { _balances[owner] -= 1; delete _owners[tokenId]; + if (bytes(_tokenURIs[tokenId]).length != 0) { + delete _tokenURIs[tokenId]; + } + emit Transfer(owner, address(0), tokenId); + } /** diff --git a/contracts/NFT/ERC721Mock.sol b/contracts/NFT/ERC721Mock.sol new file mode 100644 index 000000000000..4e3a2de29b01 --- /dev/null +++ b/contracts/NFT/ERC721Mock.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.7.6; + +import "./ERC721.sol"; +import "./utils/Counters.sol"; + +/** + * @title ERC721Mock + * This mock just provides a public safeMint, mint, and burn functions for testing purposes + */ +contract ERC721Mock is ERC721 { + + using Counters for Counters.Counter; + Counters.Counter private _tokenIds; + + constructor (string memory name, string memory symbol) ERC721(name, symbol) { } +/* + function mintNFT(address recipient, string memory tokenURI) public returns (uint256) + { + _tokenIds.increment(); + + uint256 newItemId = _tokenIds.current(); + + safeMint(recipient, newItemId); + + setTokenURI(newItemId, tokenURI); + + return newItemId; + } +*/ + function mintNFT(address recipient, uint256 tokenId, string memory tokenURI) public returns (uint256) + { + safeMint(recipient, tokenId); + + setTokenURI(tokenId, tokenURI); + + return tokenId; + } + + function baseURI() public view override returns (string memory) { + return baseURI(); + } + + function setBaseURI(string calldata newBaseURI) public { + _setBaseURI(newBaseURI); + } + + function setTokenURI(uint256 tokenId, string memory _tokenURI) public { + _setTokenURI(tokenId, _tokenURI); + } + + function getTokenURI(uint256 tokenId) public view returns (string memory) { + return tokenURI(tokenId); + } + + function exists(uint256 tokenId) public view returns (bool) { + return _exists(tokenId); + } + + function mint(address to, uint256 tokenId) public { + _mint(to, tokenId); + } + + function safeMint(address to, uint256 tokenId) public { + _safeMint(to, tokenId); + } + + function safeMint(address to, uint256 tokenId, bytes memory _data) public { + _safeMint(to, tokenId, _data); + } + + function burn(uint256 tokenId) public { + _burn(tokenId); + } +} diff --git a/contracts/NFT/ERC721PausableMock.sol b/contracts/NFT/ERC721PausableMock.sol new file mode 100644 index 000000000000..53d43cb8db5e --- /dev/null +++ b/contracts/NFT/ERC721PausableMock.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.7.6; + +import "./extensions/ERC721Pausable.sol"; + +/** + * @title ERC721PausableMock + * This mock just provides a public mint, burn and exists functions for testing purposes + */ +contract ERC721PausableMock is ERC721Pausable { + constructor (string memory name, string memory symbol) ERC721(name, symbol) { } + + function pause() external { + _pause(); + } + + function unpause() external { + _unpause(); + } + + function exists(uint256 tokenId) public view returns (bool) { + return _exists(tokenId); + } + + function mint(address to, uint256 tokenId) public { + _mint(to, tokenId); + } + + function safeMint(address to, uint256 tokenId) public { + _safeMint(to, tokenId); + } + + function safeMint(address to, uint256 tokenId, bytes memory _data) public { + _safeMint(to, tokenId, _data); + } + + function burn(uint256 tokenId) public { + _burn(tokenId); + } +} diff --git a/contracts/NFT/ERC721ReceiverMock.sol b/contracts/NFT/ERC721ReceiverMock.sol new file mode 100644 index 000000000000..334801196df5 --- /dev/null +++ b/contracts/NFT/ERC721ReceiverMock.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.7.6; + +import "./IERC721Receiver.sol"; + +contract ERC721ReceiverMock is IERC721Receiver { + enum Error { + None, + RevertWithMessage, + RevertWithoutMessage, + Panic + } + + bytes4 private immutable _retval; + Error private immutable _error; + + event Received(address operator, address from, uint256 tokenId, bytes data, uint256 gas); + + constructor (bytes4 retval, Error error) { + _retval = retval; + _error = error; + } + + function onERC721Received(address operator, address from, uint256 tokenId, bytes memory data) + public override returns (bytes4) + { + if (_error == Error.RevertWithMessage) { + revert("ERC721ReceiverMock: reverting"); + } else if (_error == Error.RevertWithoutMessage) { + revert(); + } else if (_error == Error.Panic) { + uint256 a = uint256(0) / uint256(0); + a; + } + emit Received(operator, from, tokenId, data, gasleft()); + return _retval; + } +} diff --git a/contracts/NFT/access/Ownable.sol b/contracts/NFT/access/Ownable.sol new file mode 100644 index 000000000000..f2a5b12f0fa3 --- /dev/null +++ b/contracts/NFT/access/Ownable.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >= 0.7.6; + +import "../utils/Context.sol"; +/** + * @dev Contract module which provides a basic access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * By default, the owner account will be the one that deploys the contract. This + * can later be changed with {transferOwnership}. + * + * This module is used through inheritance. It will make available the modifier + * `onlyOwner`, which can be applied to your functions to restrict their use to + * the owner. + */ +abstract contract Ownable is Context { + address private _owner; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + constructor () { + address msgSender = _msgSender(); + _owner = msgSender; + emit OwnershipTransferred(address(0), msgSender); + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view virtual returns (address) { + return _owner; + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(owner() == _msgSender(), "Ownable: caller is not the owner"); + _; + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions anymore. Can only be called by the current owner. + * + * NOTE: Renouncing ownership will leave the contract without an owner, + * thereby removing any functionality that is only available to the owner. + */ + function renounceOwnership() public virtual onlyOwner { + emit OwnershipTransferred(_owner, address(0)); + _owner = address(0); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + require(newOwner != address(0), "Ownable: new owner is the zero address"); + emit OwnershipTransferred(_owner, newOwner); + _owner = newOwner; + } +} diff --git a/contracts/NFT/extensions/ERC721URIStorage.sol b/contracts/NFT/extensions/ERC721URIStorage.sol deleted file mode 100644 index 6488e6771d9c..000000000000 --- a/contracts/NFT/extensions/ERC721URIStorage.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "../ERC721.sol"; - -/** - * @dev ERC721 token with storage based token URI management. - */ -abstract contract ERC721URIStorage is ERC721 { - using Strings for uint256; - - // Optional mapping for token URIs - mapping (uint256 => string) private _tokenURIs; - - /** - * @dev See {IERC721Metadata-tokenURI}. - */ - function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { - require(_exists(tokenId), "ERC721URIStorage: URI query for nonexistent token"); - - string memory _tokenURI = _tokenURIs[tokenId]; - string memory base = _baseURI(); - - // If there is no base URI, return the token URI. - if (bytes(base).length == 0) { - return _tokenURI; - } - // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked). - if (bytes(_tokenURI).length > 0) { - return string(abi.encodePacked(base, _tokenURI)); - } - - return super.tokenURI(tokenId); - } - - /** - * @dev Sets `_tokenURI` as the tokenURI of `tokenId`. - * - * Requirements: - * - * - `tokenId` must exist. - */ - function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual { - require(_exists(tokenId), "ERC721URIStorage: URI set of nonexistent token"); - _tokenURIs[tokenId] = _tokenURI; - } - - /** - * @dev Destroys `tokenId`. - * The approval is cleared when the token is burned. - * - * Requirements: - * - * - `tokenId` must exist. - * - * Emits a {Transfer} event. - */ - function _burn(uint256 tokenId) internal virtual override { - super._burn(tokenId); - - if (bytes(_tokenURIs[tokenId]).length != 0) { - delete _tokenURIs[tokenId]; - } - } -} diff --git a/contracts/NFT/presets/ERC721PresetMinterPauserAutoId.sol b/contracts/NFT/presets/ERC721PresetMinterPauserAutoId.sol deleted file mode 100644 index 1ff072bd3332..000000000000 --- a/contracts/NFT/presets/ERC721PresetMinterPauserAutoId.sol +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "../ERC721.sol"; -import "../extensions/ERC721Enumerable.sol"; -import "../extensions/ERC721Burnable.sol"; -import "../extensions/ERC721Pausable.sol"; -import "../access/AccessControlEnumerable.sol"; -import "../utils/Context.sol"; -import "../utils/Counters.sol"; - -/** - * @dev {ERC721} token, including: - * - * - ability for holders to burn (destroy) their tokens - * - a minter role that allows for token minting (creation) - * - a pauser role that allows to stop all token transfers - * - token ID and URI autogeneration - * - * This contract uses {AccessControl} to lock permissioned functions using the - * different roles - head to its documentation for details. - * - * The account that deploys the contract will be granted the minter and pauser - * roles, as well as the default admin role, which will let it grant both minter - * and pauser roles to other accounts. - */ -contract ERC721PresetMinterPauserAutoId is Context, AccessControlEnumerable, ERC721Enumerable, ERC721Burnable, ERC721Pausable { - using Counters for Counters.Counter; - - bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); - bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); - - Counters.Counter private _tokenIdTracker; - - string private _baseTokenURI; - - /** - * @dev Grants `DEFAULT_ADMIN_ROLE`, `MINTER_ROLE` and `PAUSER_ROLE` to the - * account that deploys the contract. - * - * Token URIs will be autogenerated based on `baseURI` and their token IDs. - * See {ERC721-tokenURI}. - */ - constructor(string memory name, string memory symbol, string memory baseTokenURI) ERC721(name, symbol) { - _baseTokenURI = baseTokenURI; - - _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); - - _setupRole(MINTER_ROLE, _msgSender()); - _setupRole(PAUSER_ROLE, _msgSender()); - } - - function _baseURI() internal view virtual override returns (string memory) { - return _baseTokenURI; - } - - /** - * @dev Creates a new token for `to`. Its token ID will be automatically - * assigned (and available on the emitted {IERC721-Transfer} event), and the token - * URI autogenerated based on the base URI passed at construction. - * - * See {ERC721-_mint}. - * - * Requirements: - * - * - the caller must have the `MINTER_ROLE`. - */ - function mint(address to) public virtual { - require(hasRole(MINTER_ROLE, _msgSender()), "ERC721PresetMinterPauserAutoId: must have minter role to mint"); - - // We cannot just use balanceOf to create the new tokenId because tokens - // can be burned (destroyed), so we need a separate counter. - _mint(to, _tokenIdTracker.current()); - _tokenIdTracker.increment(); - } - - /** - * @dev Pauses all token transfers. - * - * See {ERC721Pausable} and {Pausable-_pause}. - * - * Requirements: - * - * - the caller must have the `PAUSER_ROLE`. - */ - function pause() public virtual { - require(hasRole(PAUSER_ROLE, _msgSender()), "ERC721PresetMinterPauserAutoId: must have pauser role to pause"); - _pause(); - } - - /** - * @dev Unpauses all token transfers. - * - * See {ERC721Pausable} and {Pausable-_unpause}. - * - * Requirements: - * - * - the caller must have the `PAUSER_ROLE`. - */ - function unpause() public virtual { - require(hasRole(PAUSER_ROLE, _msgSender()), "ERC721PresetMinterPauserAutoId: must have pauser role to unpause"); - _unpause(); - } - - function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual override(ERC721, ERC721Enumerable, ERC721Pausable) { - super._beforeTokenTransfer(from, to, tokenId); - } - - /** - * @dev See {IERC165-supportsInterface}. - */ - function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControlEnumerable, ERC721, ERC721Enumerable) returns (bool) { - return super.supportsInterface(interfaceId); - } -} diff --git a/deployment/local/addresses.json b/deployment/local/addresses.json index 233b03ced44b..2018cf3cc710 100644 --- a/deployment/local/addresses.json +++ b/deployment/local/addresses.json @@ -1,10 +1,8 @@ { - "L1LiquidityPool": "0x2673a37B287b9896fbc9fB8E29Ed1d899BD4281E", - "L2LiquidityPool": "0x16Af4Db6548234c6463Ad6F0cf355260E96E741b", - "L1ERC20": "0x858B8623Ca51F089E7a51Dc81F7630901E6CCF55", - "L2DepositedERC20": "0x5736b4030Dc0A2aEC72c42C5f1b937E8CAFe46CE", - "L1ERC20Gateway": "0xE85308bB7378debbAC14ae15B41dDeaA2939bab9", + "L1ERC20": "0x3626D6562d560a13121B8AEb839F70BBFa973C8A", + "L2DepositedERC20": "0xC473e68E1621AA5913863906962Ca7BEb50e0175", + "L1ERC20Gateway": "0x62B5b86CFdD18245767F42716CEB236dE6484B42", "l1ETHGatewayAddress": "0x4F53A01556Dc6f120db9a1b4caE786D5f0b5792C", "l1MessengerAddress": "0xA6404B184Ad3f6F41b6472f02ba45f25C6A66d49", - "L2ERC721": "0xbCbF2cEBa7bFb6F25B3f9fC5A37e8b07Fa43700B" + "L2ERC721": "0x53f0d9881f8CD6D90479C3532e98714e37fb3Aa0" } \ No newline at end of file diff --git a/test/setup_test.spec.ts b/optional/setup_test.spec.ts similarity index 93% rename from test/setup_test.spec.ts rename to optional/setup_test.spec.ts index 35aa8a27c762..616cdb1438c8 100644 --- a/test/setup_test.spec.ts +++ b/optional/setup_test.spec.ts @@ -9,7 +9,7 @@ import L1ERC20GatewayJson from '../artifacts/contracts/L1ERC20Gateway.sol/L1ERC2 import L2LiquidityPoolJson from '../artifacts-ovm/contracts/L2LiquidityPool.sol/L2LiquidityPool.json' import L2DepositedERC20Json from '../artifacts-ovm/contracts/L2DepositedERC20.sol/L2DepositedERC20.json' -import L2ERC721Json from '../artifacts-ovm/contracts/NFT/ERC721.sol/ERC721.json' +import L2ERC721Json from '../artifacts-ovm/contracts/NFT/ERC721Mock.sol/ERC721Mock.json' import { OptimismEnv } from './shared/env' @@ -101,6 +101,12 @@ describe('Token, Bridge, and Swap Pool Setup and Test', async () => { env.bobl1Wallet ) + Factory__L2ERC721 = new ContractFactory( + L2ERC721Json.abi, + L2ERC721Json.bytecode, + env.bobl1Wallet + ) + Factory__L2DepositedERC20 = new ContractFactory( L2DepositedERC20Json.abi, L2DepositedERC20Json.bytecode, @@ -113,12 +119,6 @@ describe('Token, Bridge, and Swap Pool Setup and Test', async () => { env.bobl1Wallet ) - Factory__L2ERC721 = new ContractFactory( - L2ERC721Json.abi, - L2ERC721Json.bytecode, - env.bobl1Wallet - ) - }) before(async () => { @@ -187,7 +187,7 @@ describe('Token, Bridge, and Swap Pool Setup and Test', async () => { await L2LiquidityPool.registerTokenAddress(L1ERC20.address, L2DepositedERC20.address); // Mint a new NFT on L2 - // [initialSupply, name, decimals, symbol] + // [nftSymbol, nftName] // this is owned by bobl1Wallet L2ERC721 = await Factory__L2ERC721.deploy( nftSymbol, @@ -245,26 +245,7 @@ describe('Token, Bridge, and Swap Pool Setup and Test', async () => { preERC20Balances.add(transferAmount) ) }) -/* - it('should mint a new ERC721 and transfer it from Bob to Alice', async () => { - const transferAmount = utils.parseEther("500") - - //L1ERC20 is owned by Bob - const preERC20Balances = await L1ERC20.balanceOf(env.alicel1Wallet.address); - const transferERC20TX = await L1ERC20.transfer( - env.alicel1Wallet.address, - transferAmount, - ) - await transferERC20TX.wait() - - const postERC20Balance = await L1ERC20.balanceOf(env.alicel1Wallet.address); - - expect(postERC20Balance).to.deep.eq( - preERC20Balances.add(transferAmount) - ) - }) -*/ it('should add initial ETH and ERC20 to the L1 Liquidity Pool', async () => { // ************************************************** @@ -274,6 +255,7 @@ describe('Token, Bridge, and Swap Pool Setup and Test', async () => { const addERC20Amount = utils.parseEther("500") const l1ProviderLL = providers.getDefaultProvider(process.env.L1_NODE_WEB3_URL) + // Add ETH const preETHBalances = await getBalances("0x0000000000000000000000000000000000000000") const preL1LPETHBalance = await l1ProviderLL.getBalance(L1LiquidityPool.address) @@ -374,7 +356,8 @@ describe('Token, Bridge, and Swap Pool Setup and Test', async () => { ) }) - it('should add initial oWETH and ERC20 to the L2 Liquidity Pool', async () => { + it('should add initial oETH and ERC20 to the L2 Liquidity Pool', async () => { + const depositL2oWETHAmount = utils.parseEther("5.1") const addoWETHAmount = utils.parseEther("5") const depositL2ERC20Amount = utils.parseEther("510") @@ -440,7 +423,7 @@ describe('Token, Bridge, and Swap Pool Setup and Test', async () => { it('should move ETH from L1 LP to L2', async () => { - const swapAmount = utils.parseEther("0.05") + const swapAmount = utils.parseEther("0.50") const preBalances = await getBalances("0x0000000000000000000000000000000000000000") //this triggers the receive @@ -463,8 +446,9 @@ describe('Token, Bridge, and Swap Pool Setup and Test', async () => { ) }) - it('should swap wETH from L2 LP to ETH in L1 user wallet', async () => { + it('should swap oETH from L2 LP to ETH in L1 user wallet', async () => { + //basically, the swap-exit const swapAmount = utils.parseEther("0.05") const preBalances = await getBalances(env.L2ETHGateway.address) @@ -491,4 +475,34 @@ describe('Token, Bridge, and Swap Pool Setup and Test', async () => { preBalances.L2LPFeeBalance.add(swapAmount.mul(3).div(100)) ) }) + + it('should mint a new ERC721 and transfer it from Bob to Alice', async () => { + + const name = 'Non Fungible Token' + const symbol = 'NFT' + const firstTokenId = BigNumber.from('5042') + + const baseURI = 'https://api.com/v1/' + const sampleUri = 'mock://mytoken' + + const owner = env.bobl2Wallet.address; + const recipient = env.alicel2Wallet.address; + + const nft = await L2ERC721.mintNFT( + recipient, + 'https://www.atcc.org/products/all/CCL-2.aspx' + ) + + await nft.wait() + console.log("ERC721:",nft) + + //it('returns the amount of tokens owned by the given address', async function () { + expect(await L2ERC721.balanceOf(owner)).to.be.bignumber.equal('1'); + //}); + + //it('returns the owner of the given token ID', async function () { + expect(await L2ERC721.ownerOf(nft)).to.be.equal(recipient); + //}); + + }) }) \ No newline at end of file diff --git a/test/nft.spec.ts b/test/nft.spec.ts new file mode 100644 index 000000000000..ee4feeea6eb1 --- /dev/null +++ b/test/nft.spec.ts @@ -0,0 +1,272 @@ +import { expect } from 'chai' + +import { Contract, ContractFactory, BigNumber, Wallet, utils, providers } from 'ethers' +import { Direction } from './shared/watcher-utils' + +import L1ERC20Json from '../artifacts/contracts/ERC20.sol/ERC20.json' +import L1ERC20GatewayJson from '../artifacts/contracts/L1ERC20Gateway.sol/L1ERC20Gateway.json' +import L2DepositedERC20Json from '../artifacts-ovm/contracts/L2DepositedERC20.sol/L2DepositedERC20.json' +import L2ERC721Json from '../artifacts-ovm/contracts/NFT/ERC721Mock.sol/ERC721Mock.json' + +import { OptimismEnv } from './shared/env' + +import * as fs from 'fs' + +describe('NFT Test', async () => { + + let Factory__L1ERC20: ContractFactory + let Factory__L2DepositedERC20: ContractFactory + let Factory__L1ERC20Gateway: ContractFactory + let Factory__L2ERC721: ContractFactory + + let L1ERC20: Contract + let L2DepositedERC20: Contract + let L1ERC20Gateway: Contract + let L2ERC721: Contract + + let env: OptimismEnv + + //Test ERC20 + const initialAmount = utils.parseEther("10000000000") + const tokenName = 'JLKN Test' + const tokenDecimals = 18 + const tokenSymbol = 'JLKN' + + //Test Marc's BioBase NFT system + const nftName = 'BioBase' + const nftSymbol = 'BEE' //Bioeconomy Explodes + + const getBalances = async ( + _address: string, + _env=env + ) => { + + const aliceL1Balance = await _env.alicel1Wallet.getBalance() + const aliceL2Balance = await _env.alicel2Wallet.getBalance() + + const bobL1Balance = await _env.bobl1Wallet.getBalance() + const bobL2Balance = await _env.bobl2Wallet.getBalance() + + console.log("\nbobL1Balance:", bobL1Balance.toString()) + console.log("bobL2Balance:", bobL2Balance.toString()) + console.log("aliceL1Balance:", aliceL1Balance.toString()) + console.log("aliceL2Balance:", aliceL2Balance.toString()) + + return { + aliceL1Balance, + aliceL2Balance, + bobL1Balance, + bobL2Balance, + } + } + + /************* BOB owns all the pools, and ALICE Mints a new token ***********/ + before(async () => { + + env = await OptimismEnv.new() + + Factory__L1ERC20 = new ContractFactory( + L1ERC20Json.abi, + L1ERC20Json.bytecode, + env.bobl1Wallet + ) + + Factory__L2ERC721 = new ContractFactory( + L2ERC721Json.abi, + L2ERC721Json.bytecode, + env.bobl2Wallet + ) + + Factory__L2DepositedERC20 = new ContractFactory( + L2DepositedERC20Json.abi, + L2DepositedERC20Json.bytecode, + env.bobl2Wallet + ) + + Factory__L1ERC20Gateway = new ContractFactory( + L1ERC20GatewayJson.abi, + L1ERC20GatewayJson.bytecode, + env.bobl1Wallet + ) + + }) + + before(async () => { + + //Who? mints a new token and sets up the L1 and L2 infrastructure + // Mint a new token on L1 + // [initialSupply, name, decimals, symbol] + // this is owned by bobl1Wallet + L1ERC20 = await Factory__L1ERC20.deploy( + initialAmount, + tokenName, + tokenDecimals, + tokenSymbol + ) + await L1ERC20.deployTransaction.wait() + console.log("L1ERC20 deployed to:", L1ERC20.address) + + // Who? sets up things on L2 for this new token + // [l2MessengerAddress, name, symbol] + L2DepositedERC20 = await Factory__L2DepositedERC20.deploy( + env.watcher.l2.messengerAddress, + tokenName, + tokenSymbol + ) + await L2DepositedERC20.deployTransaction.wait() + console.log("L2DepositedERC20 deployed to:", L2DepositedERC20.address) + + // Who? deploys a gateway for this new token + // [L1_ERC20.address, OVM_L2DepositedERC20.address, l1MessengerAddress] + L1ERC20Gateway = await Factory__L1ERC20Gateway.deploy( + L1ERC20.address, + L2DepositedERC20.address, + env.watcher.l1.messengerAddress, + ) + await L1ERC20Gateway.deployTransaction.wait() + console.log("L1ERC20Gateway deployed to:", L1ERC20Gateway.address) + + // Who initializes the contracts for the new token + const initL2 = await L2DepositedERC20.init(L1ERC20Gateway.address); + await initL2.wait(); + console.log('L2 ERC20 initialized:',initL2.hash); + + // Mint a new NFT on L2 + // [nftSymbol, nftName] + // this is owned by bobl1Wallet + L2ERC721 = await Factory__L2ERC721.deploy( + nftSymbol, + nftName + ) + await L2ERC721.deployTransaction.wait() + console.log("Marc's BioBase NFT L2ERC721 deployed to:", L2ERC721) + console.log("Marc's BioBase NFT L2ERC721 deployed to:", L2ERC721.address) + + }) + + before(async () => { + //keep track of where things are for future use by the front end + console.log("\n\n********************************\nSaving all key addresses") + + const addresses = { + L1ERC20: L1ERC20.address, + L2DepositedERC20: L2DepositedERC20.address, + L1ERC20Gateway: L1ERC20Gateway.address, + l1ETHGatewayAddress: env.L1ETHGateway.address, + l1MessengerAddress: env.l1MessengerAddress, + L2ERC721: L2ERC721.address + } + + console.log(JSON.stringify(addresses, null, 2)) + + fs.writeFile('./deployment/local/addresses.json', JSON.stringify(addresses, null, 2), err => { + if (err) { + console.log('Error writing addresses to file:', err) + } else { + console.log('Successfully wrote addresses to file') + } + }) + + console.log('********************************\n\n') + + }) + + it('should transfer ERC20 from Bob to Alice', async () => { + + const depositAmount = utils.parseEther("50") + const preBalances = await getBalances("0x0000000000000000000000000000000000000000") + + console.log("\n Depositing...") + + const { tx, receipt } = await env.waitForXDomainTransaction( + env.L1ETHGateway.deposit({ value: depositAmount }), + Direction.L1ToL2 + ) + + const l1FeePaid = receipt.gasUsed.mul(tx.gasPrice) + const postBalances = await getBalances("0x0000000000000000000000000000000000000000") + + expect(postBalances.bobL2Balance).to.deep.eq( + preBalances.bobL2Balance.add(depositAmount) + ) + expect(postBalances.bobL1Balance).to.deep.eq( + preBalances.bobL1Balance.sub(l1FeePaid.add(depositAmount)) + ) + + }) + + it('should mint a new ERC721 and transfer it from Bob to Alice', async () => { + + //const name = 'Non Fungible Token' + //const symbol = 'NFT' + //const firstTokenId = BigNumber.from('5042') + + //const baseURI = 'https://api.com/v1/' + //const sampleUri = 'mock://mytoken' + + const owner = env.bobl2Wallet.address; + const recipient = env.alicel2Wallet.address; + + let nft = await L2ERC721.mintNFT( + recipient, + BigNumber.from(1), + 'https://www.atcc.org/products/all/CCL-2.aspx' + ) + await nft.wait() + console.log("ERC721:",nft) + + nft = await L2ERC721.mintNFT( + recipient, + BigNumber.from(2), + 'https://www.atcc.org/products/all/CCL-2.aspx#characteristics' + ) + await nft.wait() + console.log("ERC7212:",nft) + + nft = await L2ERC721.mintNFT( + recipient, + BigNumber.from(42), + 'https://www.atcc.org/products/all/CCL-2.aspx#characte' + ) + await nft.wait() + console.log("ERC7212:",nft) + + const filter = { + address: L2ERC721.address, + topics: [ + // the name of the event, parnetheses containing the data type of each event, no spaces + utils.id("Transfer(address,address,uint256)") + ] + } + + env.l2Provider.on(filter, (res) => { + // do whatever you want here + // I'm pretty sure this returns a promise, so don't forget to resolve it + console.log("res:",res) + }) + + await L2ERC721.on("Transfer", (to, amount, from) => { + console.log("Transfer:",to, amount, from); + }); + + const balanceOwner = await L2ERC721.balanceOf(owner); + console.log("balanceOwner:",balanceOwner.toString()) + + const balanceRecipient = await L2ERC721.balanceOf(recipient); + console.log("balanceRecipient:",balanceRecipient.toString()) + + let nftURL = await L2ERC721.getTokenURI(BigNumber.from(2)); + console.log("nftURL:",nftURL) + nftURL = await L2ERC721.getTokenURI(BigNumber.from(42)); + console.log("nftURL:",nftURL) + + //it('returns the amount of tokens owned by the given address', async function () { + //expect(await L2ERC721.balanceOf(owner)).to.deep.eq('1'); + //}); + + //it('returns the owner of the given token ID', async function () { + //expect(await L2ERC721.ownerOf(nft)).to.deep.eq(recipient); + //}); + + }) +}) \ No newline at end of file diff --git a/test/shared/env.ts b/test/shared/env.ts index 0cc3013eb615..f7cbeeda5b74 100644 --- a/test/shared/env.ts +++ b/test/shared/env.ts @@ -34,6 +34,8 @@ export class OptimismEnv { l1MessengerAddress: String ctc: Contract + l2Provider + // L2 Contracts L2ETHGateway: Contract l2Messenger: Contract @@ -60,6 +62,7 @@ export class OptimismEnv { this.bobl2Wallet = args.bobl2Wallet this.alicel1Wallet = args.alicel1Wallet this.alicel2Wallet = args.alicel2Wallet + this.l2Provider = args.l2Provider this.ctc = args.ctc } @@ -100,7 +103,8 @@ export class OptimismEnv { bobl1Wallet, bobl2Wallet, alicel1Wallet, - alicel2Wallet + alicel2Wallet, + l2Provider }) } From cd738d5db58491c8b23f4c346379b8f13684e676 Mon Sep 17 00:00:00 2001 From: Jan Liphardt <jan@enya.ai> Date: Wed, 5 May 2021 23:44:55 -0700 Subject: [PATCH 02/10] transition to openzeppilin libraries --- contracts/Address.sol | 189 -------- contracts/{NFT => }/ERC721Mock.sol | 21 +- contracts/Initializable.sol | 55 --- contracts/NFT/ERC721.sol | 411 ------------------ contracts/NFT/ERC721PausableMock.sol | 41 -- contracts/NFT/ERC721ReceiverMock.sol | 39 -- contracts/NFT/IERC721.sol | 129 ------ contracts/NFT/IERC721Receiver.sol | 21 - contracts/NFT/access/AccessControl.sol | 243 ----------- .../NFT/access/AccessControlEnumerable.sol | 87 ---- contracts/NFT/access/Ownable.sol | 68 --- contracts/NFT/extensions/ERC721Burnable.sol | 25 -- contracts/NFT/extensions/ERC721Enumerable.sol | 159 ------- contracts/NFT/extensions/ERC721Pausable.sol | 28 -- .../NFT/extensions/IERC721Enumerable.sol | 29 -- contracts/NFT/extensions/IERC721Metadata.sol | 27 -- contracts/NFT/security/Pausable.sol | 90 ---- contracts/NFT/utils/Address.sol | 189 -------- contracts/NFT/utils/Context.sol | 24 - contracts/NFT/utils/Counters.sol | 38 -- contracts/NFT/utils/ERC721Holder.sol | 23 - contracts/NFT/utils/Strings.sol | 67 --- contracts/NFT/utils/introspection/ERC165.sol | 28 -- contracts/NFT/utils/introspection/IERC165.sol | 24 - contracts/NFT/utils/structs/EnumerableMap.sol | 224 ---------- contracts/NFT/utils/structs/EnumerableSet.sol | 295 ------------- contracts/Proxy.sol | 83 ---- contracts/UpgradeableProxy.sol | 78 ---- deployment/local/addresses.json | 8 +- package.json | 6 +- patches/@openzeppelin+contracts+3.4.1.patch | 22 + test/nft.spec.ts | 2 +- 32 files changed, 32 insertions(+), 2741 deletions(-) delete mode 100644 contracts/Address.sol rename contracts/{NFT => }/ERC721Mock.sol (76%) delete mode 100644 contracts/Initializable.sol delete mode 100644 contracts/NFT/ERC721.sol delete mode 100644 contracts/NFT/ERC721PausableMock.sol delete mode 100644 contracts/NFT/ERC721ReceiverMock.sol delete mode 100644 contracts/NFT/IERC721.sol delete mode 100644 contracts/NFT/IERC721Receiver.sol delete mode 100644 contracts/NFT/access/AccessControl.sol delete mode 100644 contracts/NFT/access/AccessControlEnumerable.sol delete mode 100644 contracts/NFT/access/Ownable.sol delete mode 100644 contracts/NFT/extensions/ERC721Burnable.sol delete mode 100644 contracts/NFT/extensions/ERC721Enumerable.sol delete mode 100644 contracts/NFT/extensions/ERC721Pausable.sol delete mode 100644 contracts/NFT/extensions/IERC721Enumerable.sol delete mode 100644 contracts/NFT/extensions/IERC721Metadata.sol delete mode 100644 contracts/NFT/security/Pausable.sol delete mode 100644 contracts/NFT/utils/Address.sol delete mode 100644 contracts/NFT/utils/Context.sol delete mode 100644 contracts/NFT/utils/Counters.sol delete mode 100644 contracts/NFT/utils/ERC721Holder.sol delete mode 100644 contracts/NFT/utils/Strings.sol delete mode 100644 contracts/NFT/utils/introspection/ERC165.sol delete mode 100644 contracts/NFT/utils/introspection/IERC165.sol delete mode 100644 contracts/NFT/utils/structs/EnumerableMap.sol delete mode 100644 contracts/NFT/utils/structs/EnumerableSet.sol delete mode 100644 contracts/Proxy.sol delete mode 100644 contracts/UpgradeableProxy.sol create mode 100644 patches/@openzeppelin+contracts+3.4.1.patch diff --git a/contracts/Address.sol b/contracts/Address.sol deleted file mode 100644 index 42a9dc1ebe9a..000000000000 --- a/contracts/Address.sol +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.6.2 <0.8.0; - -/** - * @dev Collection of functions related to the address type - */ -library Address { - /** - * @dev Returns true if `account` is a contract. - * - * [IMPORTANT] - * ==== - * It is unsafe to assume that an address for which this function returns - * false is an externally-owned account (EOA) and not a contract. - * - * Among others, `isContract` will return false for the following - * types of addresses: - * - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * ==== - */ - function isContract(address account) internal view returns (bool) { - // This method relies on extcodesize, which returns 0 for contracts in - // construction, since the code is only stored at the end of the - // constructor execution. - - uint256 size; - // solhint-disable-next-line no-inline-assembly - assembly { size := extcodesize(account) } - return size > 0; - } - - /** - * @dev Replacement for Solidity's `transfer`: sends `amount` wei to - * `recipient`, forwarding all available gas and reverting on errors. - * - * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost - * of certain opcodes, possibly making contracts go over the 2300 gas limit - * imposed by `transfer`, making them unable to receive funds via - * `transfer`. {sendValue} removes this limitation. - * - * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. - * - * IMPORTANT: because control is transferred to `recipient`, care must be - * taken to not create reentrancy vulnerabilities. Consider using - * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. - */ - function sendValue(address payable recipient, uint256 amount) internal { - require(address(this).balance >= amount, "Address: insufficient balance"); - - // solhint-disable-next-line avoid-low-level-calls, avoid-call-value - (bool success, ) = recipient.call{ value: amount }(""); - require(success, "Address: unable to send value, recipient may have reverted"); - } - - /** - * @dev Performs a Solidity function call using a low level `call`. A - * plain`call` is an unsafe replacement for a function call: use this - * function instead. - * - * If `target` reverts with a revert reason, it is bubbled up by this - * function (like regular Solidity function calls). - * - * Returns the raw returned data. To convert to the expected return value, - * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. - * - * Requirements: - * - * - `target` must be a contract. - * - calling `target` with `data` must not revert. - * - * _Available since v3.1._ - */ - function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCall(target, data, "Address: low-level call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with - * `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but also transferring `value` wei to `target`. - * - * Requirements: - * - * - the calling contract must have an ETH balance of at least `value`. - * - the called Solidity function must be `payable`. - * - * _Available since v3.1._ - */ - function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { - return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); - } - - /** - * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but - * with `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) { - require(address(this).balance >= value, "Address: insufficient balance for call"); - require(isContract(target), "Address: call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.call{ value: value }(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { - return functionStaticCall(target, data, "Address: low-level static call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall(address target, bytes memory data, string memory errorMessage) internal view returns (bytes memory) { - require(isContract(target), "Address: static call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.staticcall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { - return functionDelegateCall(target, data, "Address: low-level delegate call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { - require(isContract(target), "Address: delegate call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.delegatecall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - function _verifyCallResult(bool success, bytes memory returndata, string memory errorMessage) private pure returns(bytes memory) { - if (success) { - return returndata; - } else { - // Look for revert reason and bubble it up if present - if (returndata.length > 0) { - // The easiest way to bubble the revert reason is using memory via assembly - - // solhint-disable-next-line no-inline-assembly - assembly { - let returndata_size := mload(returndata) - revert(add(32, returndata), returndata_size) - } - } else { - revert(errorMessage); - } - } - } -} diff --git a/contracts/NFT/ERC721Mock.sol b/contracts/ERC721Mock.sol similarity index 76% rename from contracts/NFT/ERC721Mock.sol rename to contracts/ERC721Mock.sol index 4e3a2de29b01..f3df03ab33d7 100644 --- a/contracts/NFT/ERC721Mock.sol +++ b/contracts/ERC721Mock.sol @@ -1,34 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.7.6; - -import "./ERC721.sol"; -import "./utils/Counters.sol"; - +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; /** * @title ERC721Mock * This mock just provides a public safeMint, mint, and burn functions for testing purposes */ contract ERC721Mock is ERC721 { - using Counters for Counters.Counter; - Counters.Counter private _tokenIds; - constructor (string memory name, string memory symbol) ERC721(name, symbol) { } -/* - function mintNFT(address recipient, string memory tokenURI) public returns (uint256) - { - _tokenIds.increment(); - uint256 newItemId = _tokenIds.current(); - - safeMint(recipient, newItemId); - - setTokenURI(newItemId, tokenURI); - - return newItemId; - } -*/ function mintNFT(address recipient, uint256 tokenId, string memory tokenURI) public returns (uint256) { safeMint(recipient, tokenId); diff --git a/contracts/Initializable.sol b/contracts/Initializable.sol deleted file mode 100644 index d8d620126b4d..000000000000 --- a/contracts/Initializable.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: MIT - -// solhint-disable-next-line compiler-version -pragma solidity >=0.4.24 <0.8.0; - -import "./Address.sol"; - -/** - * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed - * behind a proxy. Since a proxied contract can't have a constructor, it's common to move constructor logic to an - * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer - * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. - * - * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as - * possible by providing the encoded function call as the `_data` argument to {UpgradeableProxy-constructor}. - * - * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure - * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. - */ -abstract contract Initializable { - - /** - * @dev Indicates that the contract has been initialized. - */ - bool private _initialized; - - /** - * @dev Indicates that the contract is in the process of being initialized. - */ - bool private _initializing; - - /** - * @dev Modifier to protect an initializer function from being invoked twice. - */ - modifier initializer() { - require(_initializing || _isConstructor() || !_initialized, "Initializable: contract is already initialized"); - - bool isTopLevelCall = !_initializing; - if (isTopLevelCall) { - _initializing = true; - _initialized = true; - } - - _; - - if (isTopLevelCall) { - _initializing = false; - } - } - - /// @dev Returns true if and only if the function is running in the constructor - function _isConstructor() private view returns (bool) { - return !Address.isContract(address(this)); - } -} diff --git a/contracts/NFT/ERC721.sol b/contracts/NFT/ERC721.sol deleted file mode 100644 index 0fd40e79b397..000000000000 --- a/contracts/NFT/ERC721.sol +++ /dev/null @@ -1,411 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "./IERC721.sol"; -import "./IERC721Receiver.sol"; -import "./extensions/IERC721Metadata.sol"; -import "./utils/Address.sol"; -import "./utils/Context.sol"; -import "./utils/Strings.sol"; -import "./utils/introspection/ERC165.sol"; - -/** - * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including - * the Metadata extension, but not including the Enumerable extension, which is available separately as - * {ERC721Enumerable}. - */ -contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { - - using Address for address; - using Strings for uint256; - - // Token name - string private _name; - - // Token symbol - string private _symbol; - - string private _baseURI; - - // Optional mapping for token URIs - mapping (uint256 => string) private _tokenURIs; - - // Mapping from token ID to owner address - mapping (uint256 => address) private _owners; - - // Mapping owner address to token count - mapping (address => uint256) private _balances; - - // Mapping from token ID to approved address - mapping (uint256 => address) private _tokenApprovals; - - // Mapping from owner to operator approvals - mapping (address => mapping (address => bool)) private _operatorApprovals; - - /** - * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection. - */ - constructor (string memory name_, string memory symbol_) { - _name = name_; - _symbol = symbol_; - } - - /** - * @dev See {IERC165-supportsInterface}. - */ - function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { - return interfaceId == type(IERC721).interfaceId - || interfaceId == type(IERC721Metadata).interfaceId - || super.supportsInterface(interfaceId); - } - - /** - * @dev See {IERC721-balanceOf}. - */ - function balanceOf(address owner) public view virtual override returns (uint256) { - require(owner != address(0), "ERC721: balance query for the zero address"); - return _balances[owner]; - } - - /** - * @dev See {IERC721-ownerOf}. - */ - function ownerOf(uint256 tokenId) public view virtual override returns (address) { - address owner = _owners[tokenId]; - require(owner != address(0), "ERC721: owner query for nonexistent token"); - return owner; - } - - function name() public view virtual override returns (string memory) { return _name; } - - function symbol() public view virtual override returns (string memory) { return _symbol; } - - function baseURI() public view virtual returns (string memory) { return _baseURI; } - - /** - * @dev See {IERC721Metadata-tokenURI}. - */ - /* - function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { - require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token"); - - string memory baseURI = _baseURI(); - return bytes(baseURI).length > 0 - ? string(abi.encodePacked(baseURI, tokenId.toString())) - : ''; - } - */ - - /** - * @dev See {IERC721Metadata-tokenURI}. - */ - function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { - - require(_exists(tokenId), "ERC721URIStorage: URI query for nonexistent token"); - - string memory _tokenURI = _tokenURIs[tokenId]; - string memory base = _baseURI; - - // If there is no base URI, return the token URI. - if (bytes(base).length == 0) { - return _tokenURI; - } - // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked). - if (bytes(_tokenURI).length > 0) { - return string(abi.encodePacked(base, _tokenURI)); - } - - return ""; - } - - /** - * @dev Sets `_tokenURI` as the tokenURI of `tokenId`. - * - * Requirements: - * - * - `tokenId` must exist. - */ - function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual { - require(_exists(tokenId), "ERC721URIStorage: URI set of nonexistent token"); - _tokenURIs[tokenId] = _tokenURI; - } - - function _setBaseURI(string calldata newBaseURI) internal virtual { - _baseURI = newBaseURI; - } - - /** - * @dev See {IERC721-approve}. - */ - function approve(address to, uint256 tokenId) public virtual override { - address owner = ERC721.ownerOf(tokenId); - require(to != owner, "ERC721: approval to current owner"); - - require(_msgSender() == owner || isApprovedForAll(owner, _msgSender()), - "ERC721: approve caller is not owner nor approved for all" - ); - - _approve(to, tokenId); - } - - /** - * @dev See {IERC721-getApproved}. - */ - function getApproved(uint256 tokenId) public view virtual override returns (address) { - require(_exists(tokenId), "ERC721: approved query for nonexistent token"); - - return _tokenApprovals[tokenId]; - } - - /** - * @dev See {IERC721-setApprovalForAll}. - */ - function setApprovalForAll(address operator, bool approved) public virtual override { - require(operator != _msgSender(), "ERC721: approve to caller"); - - _operatorApprovals[_msgSender()][operator] = approved; - emit ApprovalForAll(_msgSender(), operator, approved); - } - - /** - * @dev See {IERC721-isApprovedForAll}. - */ - function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { - return _operatorApprovals[owner][operator]; - } - - /** - * @dev See {IERC721-transferFrom}. - */ - function transferFrom(address from, address to, uint256 tokenId) public virtual override { - //solhint-disable-next-line max-line-length - require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved"); - - _transfer(from, to, tokenId); - } - - /** - * @dev See {IERC721-safeTransferFrom}. - */ - function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override { - safeTransferFrom(from, to, tokenId, ""); - } - - /** - * @dev See {IERC721-safeTransferFrom}. - */ - function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public virtual override { - require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved"); - _safeTransfer(from, to, tokenId, _data); - } - - /** - * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients - * are aware of the ERC721 protocol to prevent tokens from being forever locked. - * - * `_data` is additional data, it has no specified format and it is sent in call to `to`. - * - * This internal function is equivalent to {safeTransferFrom}, and can be used to e.g. - * implement alternative mechanisms to perform token transfer, such as signature-based. - * - * Requirements: - * - * - `from` cannot be the zero address. - * - `to` cannot be the zero address. - * - `tokenId` token must exist and be owned by `from`. - * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. - * - * Emits a {Transfer} event. - */ - function _safeTransfer(address from, address to, uint256 tokenId, bytes memory _data) internal virtual { - _transfer(from, to, tokenId); - require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer"); - } - - /** - * @dev Returns whether `tokenId` exists. - * - * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. - * - * Tokens start existing when they are minted (`_mint`), - * and stop existing when they are burned (`_burn`). - */ - function _exists(uint256 tokenId) internal view virtual returns (bool) { - return _owners[tokenId] != address(0); - } - - /** - * @dev Returns whether `spender` is allowed to manage `tokenId`. - * - * Requirements: - * - * - `tokenId` must exist. - */ - function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) { - require(_exists(tokenId), "ERC721: operator query for nonexistent token"); - address owner = ERC721.ownerOf(tokenId); - return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender)); - } - - /** - * @dev Safely mints `tokenId` and transfers it to `to`. - * - * Requirements: - * - * - `tokenId` must not exist. - * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. - * - * Emits a {Transfer} event. - */ - function _safeMint(address to, uint256 tokenId) internal virtual { - _safeMint(to, tokenId, ""); - } - - /** - * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is - * forwarded in {IERC721Receiver-onERC721Received} to contract recipients. - */ - function _safeMint(address to, uint256 tokenId, bytes memory _data) internal virtual { - _mint(to, tokenId); - require(_checkOnERC721Received(address(0), to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer"); - } - - /** - * @dev Mints `tokenId` and transfers it to `to`. - * - * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible - * - * Requirements: - * - * - `tokenId` must not exist. - * - `to` cannot be the zero address. - * - * Emits a {Transfer} event. - */ - function _mint(address to, uint256 tokenId) internal virtual { - require(to != address(0), "ERC721: mint to the zero address"); - require(!_exists(tokenId), "ERC721: token already minted"); - - _beforeTokenTransfer(address(0), to, tokenId); - - _balances[to] += 1; - _owners[tokenId] = to; - - emit Transfer(address(0), to, tokenId); - } - - /** - * @dev Destroys `tokenId`. - * The approval is cleared when the token is burned. - * - * Requirements: - * - * - `tokenId` must exist. - * - * Emits a {Transfer} event. - */ - function _burn(uint256 tokenId) internal virtual { - - address owner = ERC721.ownerOf(tokenId); - - _beforeTokenTransfer(owner, address(0), tokenId); - - // Clear approvals - _approve(address(0), tokenId); - - _balances[owner] -= 1; - delete _owners[tokenId]; - - if (bytes(_tokenURIs[tokenId]).length != 0) { - delete _tokenURIs[tokenId]; - } - - emit Transfer(owner, address(0), tokenId); - - } - - /** - * @dev Transfers `tokenId` from `from` to `to`. - * As opposed to {transferFrom}, this imposes no restrictions on msg.sender. - * - * Requirements: - * - * - `to` cannot be the zero address. - * - `tokenId` token must be owned by `from`. - * - * Emits a {Transfer} event. - */ - function _transfer(address from, address to, uint256 tokenId) internal virtual { - require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer of token that is not own"); - require(to != address(0), "ERC721: transfer to the zero address"); - - _beforeTokenTransfer(from, to, tokenId); - - // Clear approvals from the previous owner - _approve(address(0), tokenId); - - _balances[from] -= 1; - _balances[to] += 1; - _owners[tokenId] = to; - - emit Transfer(from, to, tokenId); - } - - /** - * @dev Approve `to` to operate on `tokenId` - * - * Emits a {Approval} event. - */ - function _approve(address to, uint256 tokenId) internal virtual { - _tokenApprovals[tokenId] = to; - emit Approval(ERC721.ownerOf(tokenId), to, tokenId); - } - - /** - * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address. - * The call is not executed if the target address is not a contract. - * - * @param from address representing the previous owner of the given token ID - * @param to target address that will receive the tokens - * @param tokenId uint256 ID of the token to be transferred - * @param _data bytes optional data to send along with the call - * @return bool whether the call correctly returned the expected magic value - */ - function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data) - private returns (bool) - { - if (to.isContract()) { - try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) { - return retval == IERC721Receiver(to).onERC721Received.selector; - } catch (bytes memory reason) { - if (reason.length == 0) { - revert("ERC721: transfer to non ERC721Receiver implementer"); - } else { - // solhint-disable-next-line no-inline-assembly - assembly { - revert(add(32, reason), mload(reason)) - } - } - } - } else { - return true; - } - } - - /** - * @dev Hook that is called before any token transfer. This includes minting - * and burning. - * - * Calling conditions: - * - * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be - * transferred to `to`. - * - When `from` is zero, `tokenId` will be minted for `to`. - * - When `to` is zero, ``from``'s `tokenId` will be burned. - * - `from` and `to` are never both zero. - * - * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. - */ - function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual { } -} diff --git a/contracts/NFT/ERC721PausableMock.sol b/contracts/NFT/ERC721PausableMock.sol deleted file mode 100644 index 53d43cb8db5e..000000000000 --- a/contracts/NFT/ERC721PausableMock.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "./extensions/ERC721Pausable.sol"; - -/** - * @title ERC721PausableMock - * This mock just provides a public mint, burn and exists functions for testing purposes - */ -contract ERC721PausableMock is ERC721Pausable { - constructor (string memory name, string memory symbol) ERC721(name, symbol) { } - - function pause() external { - _pause(); - } - - function unpause() external { - _unpause(); - } - - function exists(uint256 tokenId) public view returns (bool) { - return _exists(tokenId); - } - - function mint(address to, uint256 tokenId) public { - _mint(to, tokenId); - } - - function safeMint(address to, uint256 tokenId) public { - _safeMint(to, tokenId); - } - - function safeMint(address to, uint256 tokenId, bytes memory _data) public { - _safeMint(to, tokenId, _data); - } - - function burn(uint256 tokenId) public { - _burn(tokenId); - } -} diff --git a/contracts/NFT/ERC721ReceiverMock.sol b/contracts/NFT/ERC721ReceiverMock.sol deleted file mode 100644 index 334801196df5..000000000000 --- a/contracts/NFT/ERC721ReceiverMock.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "./IERC721Receiver.sol"; - -contract ERC721ReceiverMock is IERC721Receiver { - enum Error { - None, - RevertWithMessage, - RevertWithoutMessage, - Panic - } - - bytes4 private immutable _retval; - Error private immutable _error; - - event Received(address operator, address from, uint256 tokenId, bytes data, uint256 gas); - - constructor (bytes4 retval, Error error) { - _retval = retval; - _error = error; - } - - function onERC721Received(address operator, address from, uint256 tokenId, bytes memory data) - public override returns (bytes4) - { - if (_error == Error.RevertWithMessage) { - revert("ERC721ReceiverMock: reverting"); - } else if (_error == Error.RevertWithoutMessage) { - revert(); - } else if (_error == Error.Panic) { - uint256 a = uint256(0) / uint256(0); - a; - } - emit Received(operator, from, tokenId, data, gasleft()); - return _retval; - } -} diff --git a/contracts/NFT/IERC721.sol b/contracts/NFT/IERC721.sol deleted file mode 100644 index a3a127f805f2..000000000000 --- a/contracts/NFT/IERC721.sol +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "./utils/introspection/IERC165.sol"; - -/** - * @dev Required interface of an ERC721 compliant contract. - */ -interface IERC721 is IERC165 { - /** - * @dev Emitted when `tokenId` token is transferred from `from` to `to`. - */ - event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); - - /** - * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. - */ - event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); - - /** - * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets. - */ - event ApprovalForAll(address indexed owner, address indexed operator, bool approved); - - /** - * @dev Returns the number of tokens in ``owner``'s account. - */ - function balanceOf(address owner) external view returns (uint256 balance); - - /** - * @dev Returns the owner of the `tokenId` token. - * - * Requirements: - * - * - `tokenId` must exist. - */ - function ownerOf(uint256 tokenId) external view returns (address owner); - - /** - * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients - * are aware of the ERC721 protocol to prevent tokens from being forever locked. - * - * Requirements: - * - * - `from` cannot be the zero address. - * - `to` cannot be the zero address. - * - `tokenId` token must exist and be owned by `from`. - * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}. - * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. - * - * Emits a {Transfer} event. - */ - function safeTransferFrom(address from, address to, uint256 tokenId) external; - - /** - * @dev Transfers `tokenId` token from `from` to `to`. - * - * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. - * - * Requirements: - * - * - `from` cannot be the zero address. - * - `to` cannot be the zero address. - * - `tokenId` token must be owned by `from`. - * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. - * - * Emits a {Transfer} event. - */ - function transferFrom(address from, address to, uint256 tokenId) external; - - /** - * @dev Gives permission to `to` to transfer `tokenId` token to another account. - * The approval is cleared when the token is transferred. - * - * Only a single account can be approved at a time, so approving the zero address clears previous approvals. - * - * Requirements: - * - * - The caller must own the token or be an approved operator. - * - `tokenId` must exist. - * - * Emits an {Approval} event. - */ - function approve(address to, uint256 tokenId) external; - - /** - * @dev Returns the account approved for `tokenId` token. - * - * Requirements: - * - * - `tokenId` must exist. - */ - function getApproved(uint256 tokenId) external view returns (address operator); - - /** - * @dev Approve or remove `operator` as an operator for the caller. - * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. - * - * Requirements: - * - * - The `operator` cannot be the caller. - * - * Emits an {ApprovalForAll} event. - */ - function setApprovalForAll(address operator, bool _approved) external; - - /** - * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. - * - * See {setApprovalForAll} - */ - function isApprovedForAll(address owner, address operator) external view returns (bool); - - /** - * @dev Safely transfers `tokenId` token from `from` to `to`. - * - * Requirements: - * - * - `from` cannot be the zero address. - * - `to` cannot be the zero address. - * - `tokenId` token must exist and be owned by `from`. - * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. - * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. - * - * Emits a {Transfer} event. - */ - function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; -} diff --git a/contracts/NFT/IERC721Receiver.sol b/contracts/NFT/IERC721Receiver.sol deleted file mode 100644 index 563b569c5afd..000000000000 --- a/contracts/NFT/IERC721Receiver.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -/** - * @title ERC721 token receiver interface - * @dev Interface for any contract that wants to support safeTransfers - * from ERC721 asset contracts. - */ -interface IERC721Receiver { - /** - * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} - * by `operator` from `from`, this function is called. - * - * It must return its Solidity selector to confirm the token transfer. - * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. - * - * The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`. - */ - function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4); -} diff --git a/contracts/NFT/access/AccessControl.sol b/contracts/NFT/access/AccessControl.sol deleted file mode 100644 index 8ee168353182..000000000000 --- a/contracts/NFT/access/AccessControl.sol +++ /dev/null @@ -1,243 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "../utils/Context.sol"; -import "../utils/Strings.sol"; -import "../utils/introspection/ERC165.sol"; - -/** - * @dev External interface of AccessControl declared to support ERC165 detection. - */ -interface IAccessControl { - function hasRole(bytes32 role, address account) external view returns (bool); - function getRoleAdmin(bytes32 role) external view returns (bytes32); - function grantRole(bytes32 role, address account) external; - function revokeRole(bytes32 role, address account) external; - function renounceRole(bytes32 role, address account) external; -} - -/** - * @dev Contract module that allows children to implement role-based access - * control mechanisms. This is a lightweight version that doesn't allow enumerating role - * members except through off-chain means by accessing the contract event logs. Some - * applications may benefit from on-chain enumerability, for those cases see - * {AccessControlEnumerable}. - * - * Roles are referred to by their `bytes32` identifier. These should be exposed - * in the external API and be unique. The best way to achieve this is by - * using `public constant` hash digests: - * - * ``` - * bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); - * ``` - * - * Roles can be used to represent a set of permissions. To restrict access to a - * function call, use {hasRole}: - * - * ``` - * function foo() public { - * require(hasRole(MY_ROLE, msg.sender)); - * ... - * } - * ``` - * - * Roles can be granted and revoked dynamically via the {grantRole} and - * {revokeRole} functions. Each role has an associated admin role, and only - * accounts that have a role's admin role can call {grantRole} and {revokeRole}. - * - * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means - * that only accounts with this role will be able to grant or revoke other - * roles. More complex role relationships can be created by using - * {_setRoleAdmin}. - * - * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to - * grant and revoke this role. Extra precautions should be taken to secure - * accounts that have been granted it. - */ -abstract contract AccessControl is Context, IAccessControl, ERC165 { - struct RoleData { - mapping (address => bool) members; - bytes32 adminRole; - } - - mapping (bytes32 => RoleData) private _roles; - - bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; - - /** - * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` - * - * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite - * {RoleAdminChanged} not being emitted signaling this. - * - * _Available since v3.1._ - */ - event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); - - /** - * @dev Emitted when `account` is granted `role`. - * - * `sender` is the account that originated the contract call, an admin role - * bearer except when using {_setupRole}. - */ - event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); - - /** - * @dev Emitted when `account` is revoked `role`. - * - * `sender` is the account that originated the contract call: - * - if using `revokeRole`, it is the admin role bearer - * - if using `renounceRole`, it is the role bearer (i.e. `account`) - */ - event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); - - /** - * @dev Modifier that checks that an account has a specific role. Reverts - * with a standardized message including the required role. - * - * The format of the revert reason is given by the following regular expression: - * - * /^AccessControl: account (0x[0-9a-f]{20}) is missing role (0x[0-9a-f]{32})$/ - * - * _Available since v4.1._ - */ - modifier onlyRole(bytes32 role) { - _checkRole(role, _msgSender()); - _; - } - - /** - * @dev See {IERC165-supportsInterface}. - */ - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - return interfaceId == type(IAccessControl).interfaceId - || super.supportsInterface(interfaceId); - } - - /** - * @dev Returns `true` if `account` has been granted `role`. - */ - function hasRole(bytes32 role, address account) public view override returns (bool) { - return _roles[role].members[account]; - } - - /** - * @dev Revert with a standard message if `account` is missing `role`. - * - * The format of the revert reason is given by the following regular expression: - * - * /^AccessControl: account (0x[0-9a-f]{20}) is missing role (0x[0-9a-f]{32})$/ - */ - function _checkRole(bytes32 role, address account) internal view { - if(!hasRole(role, account)) { - revert(string(abi.encodePacked( - "AccessControl: account ", - Strings.toHexString(uint160(account), 20), - " is missing role ", - Strings.toHexString(uint256(role), 32) - ))); - } - } - - /** - * @dev Returns the admin role that controls `role`. See {grantRole} and - * {revokeRole}. - * - * To change a role's admin, use {_setRoleAdmin}. - */ - function getRoleAdmin(bytes32 role) public view override returns (bytes32) { - return _roles[role].adminRole; - } - - /** - * @dev Grants `role` to `account`. - * - * If `account` had not been already granted `role`, emits a {RoleGranted} - * event. - * - * Requirements: - * - * - the caller must have ``role``'s admin role. - */ - function grantRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) { - _grantRole(role, account); - } - - /** - * @dev Revokes `role` from `account`. - * - * If `account` had been granted `role`, emits a {RoleRevoked} event. - * - * Requirements: - * - * - the caller must have ``role``'s admin role. - */ - function revokeRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) { - _revokeRole(role, account); - } - - /** - * @dev Revokes `role` from the calling account. - * - * Roles are often managed via {grantRole} and {revokeRole}: this function's - * purpose is to provide a mechanism for accounts to lose their privileges - * if they are compromised (such as when a trusted device is misplaced). - * - * If the calling account had been granted `role`, emits a {RoleRevoked} - * event. - * - * Requirements: - * - * - the caller must be `account`. - */ - function renounceRole(bytes32 role, address account) public virtual override { - require(account == _msgSender(), "AccessControl: can only renounce roles for self"); - - _revokeRole(role, account); - } - - /** - * @dev Grants `role` to `account`. - * - * If `account` had not been already granted `role`, emits a {RoleGranted} - * event. Note that unlike {grantRole}, this function doesn't perform any - * checks on the calling account. - * - * [WARNING] - * ==== - * This function should only be called from the constructor when setting - * up the initial roles for the system. - * - * Using this function in any other way is effectively circumventing the admin - * system imposed by {AccessControl}. - * ==== - */ - function _setupRole(bytes32 role, address account) internal virtual { - _grantRole(role, account); - } - - /** - * @dev Sets `adminRole` as ``role``'s admin role. - * - * Emits a {RoleAdminChanged} event. - */ - function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual { - emit RoleAdminChanged(role, getRoleAdmin(role), adminRole); - _roles[role].adminRole = adminRole; - } - - function _grantRole(bytes32 role, address account) private { - if (!hasRole(role, account)) { - _roles[role].members[account] = true; - emit RoleGranted(role, account, _msgSender()); - } - } - - function _revokeRole(bytes32 role, address account) private { - if (hasRole(role, account)) { - _roles[role].members[account] = false; - emit RoleRevoked(role, account, _msgSender()); - } - } -} diff --git a/contracts/NFT/access/AccessControlEnumerable.sol b/contracts/NFT/access/AccessControlEnumerable.sol deleted file mode 100644 index edf374045e56..000000000000 --- a/contracts/NFT/access/AccessControlEnumerable.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "./AccessControl.sol"; -import "../utils/structs/EnumerableSet.sol"; - -/** - * @dev External interface of AccessControlEnumerable declared to support ERC165 detection. - */ -interface IAccessControlEnumerable { - function getRoleMember(bytes32 role, uint256 index) external view returns (address); - function getRoleMemberCount(bytes32 role) external view returns (uint256); -} - -/** - * @dev Extension of {AccessControl} that allows enumerating the members of each role. - */ -abstract contract AccessControlEnumerable is IAccessControlEnumerable, AccessControl { - using EnumerableSet for EnumerableSet.AddressSet; - - mapping (bytes32 => EnumerableSet.AddressSet) private _roleMembers; - - /** - * @dev See {IERC165-supportsInterface}. - */ - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - return interfaceId == type(IAccessControlEnumerable).interfaceId - || super.supportsInterface(interfaceId); - } - - /** - * @dev Returns one of the accounts that have `role`. `index` must be a - * value between 0 and {getRoleMemberCount}, non-inclusive. - * - * Role bearers are not sorted in any particular way, and their ordering may - * change at any point. - * - * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure - * you perform all queries on the same block. See the following - * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] - * for more information. - */ - function getRoleMember(bytes32 role, uint256 index) public view override returns (address) { - return _roleMembers[role].at(index); - } - - /** - * @dev Returns the number of accounts that have `role`. Can be used - * together with {getRoleMember} to enumerate all bearers of a role. - */ - function getRoleMemberCount(bytes32 role) public view override returns (uint256) { - return _roleMembers[role].length(); - } - - /** - * @dev Overload {grantRole} to track enumerable memberships - */ - function grantRole(bytes32 role, address account) public virtual override { - super.grantRole(role, account); - _roleMembers[role].add(account); - } - - /** - * @dev Overload {revokeRole} to track enumerable memberships - */ - function revokeRole(bytes32 role, address account) public virtual override { - super.revokeRole(role, account); - _roleMembers[role].remove(account); - } - - /** - * @dev Overload {renounceRole} to track enumerable memberships - */ - function renounceRole(bytes32 role, address account) public virtual override { - super.renounceRole(role, account); - _roleMembers[role].remove(account); - } - - /** - * @dev Overload {_setupRole} to track enumerable memberships - */ - function _setupRole(bytes32 role, address account) internal virtual override { - super._setupRole(role, account); - _roleMembers[role].add(account); - } -} diff --git a/contracts/NFT/access/Ownable.sol b/contracts/NFT/access/Ownable.sol deleted file mode 100644 index f2a5b12f0fa3..000000000000 --- a/contracts/NFT/access/Ownable.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >= 0.7.6; - -import "../utils/Context.sol"; -/** - * @dev Contract module which provides a basic access control mechanism, where - * there is an account (an owner) that can be granted exclusive access to - * specific functions. - * - * By default, the owner account will be the one that deploys the contract. This - * can later be changed with {transferOwnership}. - * - * This module is used through inheritance. It will make available the modifier - * `onlyOwner`, which can be applied to your functions to restrict their use to - * the owner. - */ -abstract contract Ownable is Context { - address private _owner; - - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - - /** - * @dev Initializes the contract setting the deployer as the initial owner. - */ - constructor () { - address msgSender = _msgSender(); - _owner = msgSender; - emit OwnershipTransferred(address(0), msgSender); - } - - /** - * @dev Returns the address of the current owner. - */ - function owner() public view virtual returns (address) { - return _owner; - } - - /** - * @dev Throws if called by any account other than the owner. - */ - modifier onlyOwner() { - require(owner() == _msgSender(), "Ownable: caller is not the owner"); - _; - } - - /** - * @dev Leaves the contract without owner. It will not be possible to call - * `onlyOwner` functions anymore. Can only be called by the current owner. - * - * NOTE: Renouncing ownership will leave the contract without an owner, - * thereby removing any functionality that is only available to the owner. - */ - function renounceOwnership() public virtual onlyOwner { - emit OwnershipTransferred(_owner, address(0)); - _owner = address(0); - } - - /** - * @dev Transfers ownership of the contract to a new account (`newOwner`). - * Can only be called by the current owner. - */ - function transferOwnership(address newOwner) public virtual onlyOwner { - require(newOwner != address(0), "Ownable: new owner is the zero address"); - emit OwnershipTransferred(_owner, newOwner); - _owner = newOwner; - } -} diff --git a/contracts/NFT/extensions/ERC721Burnable.sol b/contracts/NFT/extensions/ERC721Burnable.sol deleted file mode 100644 index 2dba7500d842..000000000000 --- a/contracts/NFT/extensions/ERC721Burnable.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "../ERC721.sol"; -import "../utils/Context.sol"; - -/** - * @title ERC721 Burnable Token - * @dev ERC721 Token that can be irreversibly burned (destroyed). - */ -abstract contract ERC721Burnable is Context, ERC721 { - /** - * @dev Burns `tokenId`. See {ERC721-_burn}. - * - * Requirements: - * - * - The caller must own `tokenId` or be an approved operator. - */ - function burn(uint256 tokenId) public virtual { - //solhint-disable-next-line max-line-length - require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721Burnable: caller is not owner nor approved"); - _burn(tokenId); - } -} diff --git a/contracts/NFT/extensions/ERC721Enumerable.sol b/contracts/NFT/extensions/ERC721Enumerable.sol deleted file mode 100644 index feb5568ef9bb..000000000000 --- a/contracts/NFT/extensions/ERC721Enumerable.sol +++ /dev/null @@ -1,159 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "../ERC721.sol"; -import "./IERC721Enumerable.sol"; - -/** - * @dev This implements an optional extension of {ERC721} defined in the EIP that adds - * enumerability of all the token ids in the contract as well as all token ids owned by each - * account. - */ -abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { - // Mapping from owner to list of owned token IDs - mapping(address => mapping(uint256 => uint256)) private _ownedTokens; - - // Mapping from token ID to index of the owner tokens list - mapping(uint256 => uint256) private _ownedTokensIndex; - - // Array with all token ids, used for enumeration - uint256[] private _allTokens; - - // Mapping from token id to position in the allTokens array - mapping(uint256 => uint256) private _allTokensIndex; - - /** - * @dev See {IERC165-supportsInterface}. - */ - function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) { - return interfaceId == type(IERC721Enumerable).interfaceId - || super.supportsInterface(interfaceId); - } - - /** - * @dev See {IERC721Enumerable-tokenOfOwnerByIndex}. - */ - function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual override returns (uint256) { - require(index < ERC721.balanceOf(owner), "ERC721Enumerable: owner index out of bounds"); - return _ownedTokens[owner][index]; - } - - /** - * @dev See {IERC721Enumerable-totalSupply}. - */ - function totalSupply() public view virtual override returns (uint256) { - return _allTokens.length; - } - - /** - * @dev See {IERC721Enumerable-tokenByIndex}. - */ - function tokenByIndex(uint256 index) public view virtual override returns (uint256) { - require(index < ERC721Enumerable.totalSupply(), "ERC721Enumerable: global index out of bounds"); - return _allTokens[index]; - } - - /** - * @dev Hook that is called before any token transfer. This includes minting - * and burning. - * - * Calling conditions: - * - * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be - * transferred to `to`. - * - When `from` is zero, `tokenId` will be minted for `to`. - * - When `to` is zero, ``from``'s `tokenId` will be burned. - * - `from` cannot be the zero address. - * - `to` cannot be the zero address. - * - * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. - */ - function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual override { - super._beforeTokenTransfer(from, to, tokenId); - - if (from == address(0)) { - _addTokenToAllTokensEnumeration(tokenId); - } else if (from != to) { - _removeTokenFromOwnerEnumeration(from, tokenId); - } - if (to == address(0)) { - _removeTokenFromAllTokensEnumeration(tokenId); - } else if (to != from) { - _addTokenToOwnerEnumeration(to, tokenId); - } - } - - /** - * @dev Private function to add a token to this extension's ownership-tracking data structures. - * @param to address representing the new owner of the given token ID - * @param tokenId uint256 ID of the token to be added to the tokens list of the given address - */ - function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private { - uint256 length = ERC721.balanceOf(to); - _ownedTokens[to][length] = tokenId; - _ownedTokensIndex[tokenId] = length; - } - - /** - * @dev Private function to add a token to this extension's token tracking data structures. - * @param tokenId uint256 ID of the token to be added to the tokens list - */ - function _addTokenToAllTokensEnumeration(uint256 tokenId) private { - _allTokensIndex[tokenId] = _allTokens.length; - _allTokens.push(tokenId); - } - - /** - * @dev Private function to remove a token from this extension's ownership-tracking data structures. Note that - * while the token is not assigned a new owner, the `_ownedTokensIndex` mapping is _not_ updated: this allows for - * gas optimizations e.g. when performing a transfer operation (avoiding double writes). - * This has O(1) time complexity, but alters the order of the _ownedTokens array. - * @param from address representing the previous owner of the given token ID - * @param tokenId uint256 ID of the token to be removed from the tokens list of the given address - */ - function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private { - // To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and - // then delete the last slot (swap and pop). - - uint256 lastTokenIndex = ERC721.balanceOf(from) - 1; - uint256 tokenIndex = _ownedTokensIndex[tokenId]; - - // When the token to delete is the last token, the swap operation is unnecessary - if (tokenIndex != lastTokenIndex) { - uint256 lastTokenId = _ownedTokens[from][lastTokenIndex]; - - _ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token - _ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index - } - - // This also deletes the contents at the last position of the array - delete _ownedTokensIndex[tokenId]; - delete _ownedTokens[from][lastTokenIndex]; - } - - /** - * @dev Private function to remove a token from this extension's token tracking data structures. - * This has O(1) time complexity, but alters the order of the _allTokens array. - * @param tokenId uint256 ID of the token to be removed from the tokens list - */ - function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private { - // To prevent a gap in the tokens array, we store the last token in the index of the token to delete, and - // then delete the last slot (swap and pop). - - uint256 lastTokenIndex = _allTokens.length - 1; - uint256 tokenIndex = _allTokensIndex[tokenId]; - - // When the token to delete is the last token, the swap operation is unnecessary. However, since this occurs so - // rarely (when the last minted token is burnt) that we still do the swap here to avoid the gas cost of adding - // an 'if' statement (like in _removeTokenFromOwnerEnumeration) - uint256 lastTokenId = _allTokens[lastTokenIndex]; - - _allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token - _allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index - - // This also deletes the contents at the last position of the array - delete _allTokensIndex[tokenId]; - _allTokens.pop(); - } -} diff --git a/contracts/NFT/extensions/ERC721Pausable.sol b/contracts/NFT/extensions/ERC721Pausable.sol deleted file mode 100644 index 0c813d729039..000000000000 --- a/contracts/NFT/extensions/ERC721Pausable.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "../ERC721.sol"; -import "../security/Pausable.sol"; - -/** - * @dev ERC721 token with pausable token transfers, minting and burning. - * - * Useful for scenarios such as preventing trades until the end of an evaluation - * period, or having an emergency switch for freezing all token transfers in the - * event of a large bug. - */ -abstract contract ERC721Pausable is ERC721, Pausable { - /** - * @dev See {ERC721-_beforeTokenTransfer}. - * - * Requirements: - * - * - the contract must not be paused. - */ - function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal virtual override { - super._beforeTokenTransfer(from, to, tokenId); - - require(!paused(), "ERC721Pausable: token transfer while paused"); - } -} diff --git a/contracts/NFT/extensions/IERC721Enumerable.sol b/contracts/NFT/extensions/IERC721Enumerable.sol deleted file mode 100644 index 2066b7790e50..000000000000 --- a/contracts/NFT/extensions/IERC721Enumerable.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "../IERC721.sol"; - -/** - * @title ERC-721 Non-Fungible Token Standard, optional enumeration extension - * @dev See https://eips.ethereum.org/EIPS/eip-721 - */ -interface IERC721Enumerable is IERC721 { - - /** - * @dev Returns the total amount of tokens stored by the contract. - */ - function totalSupply() external view returns (uint256); - - /** - * @dev Returns a token ID owned by `owner` at a given `index` of its token list. - * Use along with {balanceOf} to enumerate all of ``owner``'s tokens. - */ - function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256 tokenId); - - /** - * @dev Returns a token ID at a given `index` of all the tokens stored by the contract. - * Use along with {totalSupply} to enumerate all tokens. - */ - function tokenByIndex(uint256 index) external view returns (uint256); -} diff --git a/contracts/NFT/extensions/IERC721Metadata.sol b/contracts/NFT/extensions/IERC721Metadata.sol deleted file mode 100644 index 0fd7a4e7cec6..000000000000 --- a/contracts/NFT/extensions/IERC721Metadata.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "../IERC721.sol"; - -/** - * @title ERC-721 Non-Fungible Token Standard, optional metadata extension - * @dev See https://eips.ethereum.org/EIPS/eip-721 - */ -interface IERC721Metadata is IERC721 { - - /** - * @dev Returns the token collection name. - */ - function name() external view returns (string memory); - - /** - * @dev Returns the token collection symbol. - */ - function symbol() external view returns (string memory); - - /** - * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. - */ - function tokenURI(uint256 tokenId) external view returns (string memory); -} diff --git a/contracts/NFT/security/Pausable.sol b/contracts/NFT/security/Pausable.sol deleted file mode 100644 index b78acafbfce9..000000000000 --- a/contracts/NFT/security/Pausable.sol +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "../utils/Context.sol"; - -/** - * @dev Contract module which allows children to implement an emergency stop - * mechanism that can be triggered by an authorized account. - * - * This module is used through inheritance. It will make available the - * modifiers `whenNotPaused` and `whenPaused`, which can be applied to - * the functions of your contract. Note that they will not be pausable by - * simply including this module, only once the modifiers are put in place. - */ -abstract contract Pausable is Context { - /** - * @dev Emitted when the pause is triggered by `account`. - */ - event Paused(address account); - - /** - * @dev Emitted when the pause is lifted by `account`. - */ - event Unpaused(address account); - - bool private _paused; - - /** - * @dev Initializes the contract in unpaused state. - */ - constructor () { - _paused = false; - } - - /** - * @dev Returns true if the contract is paused, and false otherwise. - */ - function paused() public view virtual returns (bool) { - return _paused; - } - - /** - * @dev Modifier to make a function callable only when the contract is not paused. - * - * Requirements: - * - * - The contract must not be paused. - */ - modifier whenNotPaused() { - require(!paused(), "Pausable: paused"); - _; - } - - /** - * @dev Modifier to make a function callable only when the contract is paused. - * - * Requirements: - * - * - The contract must be paused. - */ - modifier whenPaused() { - require(paused(), "Pausable: not paused"); - _; - } - - /** - * @dev Triggers stopped state. - * - * Requirements: - * - * - The contract must not be paused. - */ - function _pause() internal virtual whenNotPaused { - _paused = true; - emit Paused(_msgSender()); - } - - /** - * @dev Returns to normal state. - * - * Requirements: - * - * - The contract must be paused. - */ - function _unpause() internal virtual whenPaused { - _paused = false; - emit Unpaused(_msgSender()); - } -} diff --git a/contracts/NFT/utils/Address.sol b/contracts/NFT/utils/Address.sol deleted file mode 100644 index 39c0e0b67d8f..000000000000 --- a/contracts/NFT/utils/Address.sol +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -/** - * @dev Collection of functions related to the address type - */ -library Address { - /** - * @dev Returns true if `account` is a contract. - * - * [IMPORTANT] - * ==== - * It is unsafe to assume that an address for which this function returns - * false is an externally-owned account (EOA) and not a contract. - * - * Among others, `isContract` will return false for the following - * types of addresses: - * - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * ==== - */ - function isContract(address account) internal view returns (bool) { - // This method relies on extcodesize, which returns 0 for contracts in - // construction, since the code is only stored at the end of the - // constructor execution. - - uint256 size; - // solhint-disable-next-line no-inline-assembly - assembly { size := extcodesize(account) } - return size > 0; - } - - /** - * @dev Replacement for Solidity's `transfer`: sends `amount` wei to - * `recipient`, forwarding all available gas and reverting on errors. - * - * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost - * of certain opcodes, possibly making contracts go over the 2300 gas limit - * imposed by `transfer`, making them unable to receive funds via - * `transfer`. {sendValue} removes this limitation. - * - * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. - * - * IMPORTANT: because control is transferred to `recipient`, care must be - * taken to not create reentrancy vulnerabilities. Consider using - * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. - */ - function sendValue(address payable recipient, uint256 amount) internal { - require(address(this).balance >= amount, "Address: insufficient balance"); - - // solhint-disable-next-line avoid-low-level-calls, avoid-call-value - (bool success, ) = recipient.call{ value: amount }(""); - require(success, "Address: unable to send value, recipient may have reverted"); - } - - /** - * @dev Performs a Solidity function call using a low level `call`. A - * plain`call` is an unsafe replacement for a function call: use this - * function instead. - * - * If `target` reverts with a revert reason, it is bubbled up by this - * function (like regular Solidity function calls). - * - * Returns the raw returned data. To convert to the expected return value, - * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. - * - * Requirements: - * - * - `target` must be a contract. - * - calling `target` with `data` must not revert. - * - * _Available since v3.1._ - */ - function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCall(target, data, "Address: low-level call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with - * `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but also transferring `value` wei to `target`. - * - * Requirements: - * - * - the calling contract must have an ETH balance of at least `value`. - * - the called Solidity function must be `payable`. - * - * _Available since v3.1._ - */ - function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { - return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); - } - - /** - * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but - * with `errorMessage` as a fallback revert reason when `target` reverts. - * - * _Available since v3.1._ - */ - function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) { - require(address(this).balance >= value, "Address: insufficient balance for call"); - require(isContract(target), "Address: call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.call{ value: value }(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { - return functionStaticCall(target, data, "Address: low-level static call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a static call. - * - * _Available since v3.3._ - */ - function functionStaticCall(address target, bytes memory data, string memory errorMessage) internal view returns (bytes memory) { - require(isContract(target), "Address: static call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.staticcall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { - return functionDelegateCall(target, data, "Address: low-level delegate call failed"); - } - - /** - * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], - * but performing a delegate call. - * - * _Available since v3.4._ - */ - function functionDelegateCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { - require(isContract(target), "Address: delegate call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = target.delegatecall(data); - return _verifyCallResult(success, returndata, errorMessage); - } - - function _verifyCallResult(bool success, bytes memory returndata, string memory errorMessage) private pure returns(bytes memory) { - if (success) { - return returndata; - } else { - // Look for revert reason and bubble it up if present - if (returndata.length > 0) { - // The easiest way to bubble the revert reason is using memory via assembly - - // solhint-disable-next-line no-inline-assembly - assembly { - let returndata_size := mload(returndata) - revert(add(32, returndata), returndata_size) - } - } else { - revert(errorMessage); - } - } - } -} diff --git a/contracts/NFT/utils/Context.sol b/contracts/NFT/utils/Context.sol deleted file mode 100644 index 50c02fb88788..000000000000 --- a/contracts/NFT/utils/Context.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -/* - * @dev Provides information about the current execution context, including the - * sender of the transaction and its data. While these are generally available - * via msg.sender and msg.data, they should not be accessed in such a direct - * manner, since when dealing with meta-transactions the account sending and - * paying for execution may not be the actual sender (as far as an application - * is concerned). - * - * This contract is only required for intermediate, library-like contracts. - */ -abstract contract Context { - function _msgSender() internal view virtual returns (address) { - return msg.sender; - } - - function _msgData() internal view virtual returns (bytes calldata) { - this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 - return msg.data; - } -} diff --git a/contracts/NFT/utils/Counters.sol b/contracts/NFT/utils/Counters.sol deleted file mode 100644 index ccb3775e84a8..000000000000 --- a/contracts/NFT/utils/Counters.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -/** - * @title Counters - * @author Matt Condon (@shrugs) - * @dev Provides counters that can only be incremented or decremented by one. This can be used e.g. to track the number - * of elements in a mapping, issuing ERC721 ids, or counting request ids. - * - * Include with `using Counters for Counters.Counter;` - */ -library Counters { - struct Counter { - // This variable should never be directly accessed by users of the library: interactions must be restricted to - // the library's function. As of Solidity v0.5.2, this cannot be enforced, though there is a proposal to add - // this feature: see https://github.com/ethereum/solidity/issues/4637 - uint256 _value; // default: 0 - } - - function current(Counter storage counter) internal view returns (uint256) { - return counter._value; - } - - function increment(Counter storage counter) internal { - //unchecked { - counter._value += 1; - //} - } - - function decrement(Counter storage counter) internal { - uint256 value = counter._value; - require(value > 0, "Counter: decrement overflow"); - //unchecked { - counter._value = value - 1; - //} - } -} diff --git a/contracts/NFT/utils/ERC721Holder.sol b/contracts/NFT/utils/ERC721Holder.sol deleted file mode 100644 index 40808c15d6b8..000000000000 --- a/contracts/NFT/utils/ERC721Holder.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "../IERC721Receiver.sol"; - - /** - * @dev Implementation of the {IERC721Receiver} interface. - * - * Accepts all token transfers. - * Make sure the contract is able to use its token with {IERC721-safeTransferFrom}, {IERC721-approve} or {IERC721-setApprovalForAll}. - */ -contract ERC721Holder is IERC721Receiver { - - /** - * @dev See {IERC721Receiver-onERC721Received}. - * - * Always returns `IERC721Receiver.onERC721Received.selector`. - */ - function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { - return this.onERC721Received.selector; - } -} diff --git a/contracts/NFT/utils/Strings.sol b/contracts/NFT/utils/Strings.sol deleted file mode 100644 index 099307009708..000000000000 --- a/contracts/NFT/utils/Strings.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -/** - * @dev String operations. - */ -library Strings { - bytes16 private constant alphabet = "0123456789abcdef"; - - /** - * @dev Converts a `uint256` to its ASCII `string` decimal representation. - */ - function toString(uint256 value) internal pure returns (string memory) { - // Inspired by OraclizeAPI's implementation - MIT licence - // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol - - if (value == 0) { - return "0"; - } - uint256 temp = value; - uint256 digits; - while (temp != 0) { - digits++; - temp /= 10; - } - bytes memory buffer = new bytes(digits); - while (value != 0) { - digits -= 1; - buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); - value /= 10; - } - return string(buffer); - } - - /** - * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. - */ - function toHexString(uint256 value) internal pure returns (string memory) { - if (value == 0) { - return "0x00"; - } - uint256 temp = value; - uint256 length = 0; - while (temp != 0) { - length++; - temp >>= 8; - } - return toHexString(value, length); - } - - /** - * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. - */ - function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { - bytes memory buffer = new bytes(2 * length + 2); - buffer[0] = "0"; - buffer[1] = "x"; - for (uint256 i = 2 * length + 1; i > 1; --i) { - buffer[i] = alphabet[value & 0xf]; - value >>= 4; - } - require(value == 0, "Strings: hex length insufficient"); - return string(buffer); - } - -} diff --git a/contracts/NFT/utils/introspection/ERC165.sol b/contracts/NFT/utils/introspection/ERC165.sol deleted file mode 100644 index 96d6b925e04e..000000000000 --- a/contracts/NFT/utils/introspection/ERC165.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "./IERC165.sol"; - -/** - * @dev Implementation of the {IERC165} interface. - * - * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check - * for the additional interface id that will be supported. For example: - * - * ```solidity - * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - * return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId); - * } - * ``` - * - * Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation. - */ -abstract contract ERC165 is IERC165 { - /** - * @dev See {IERC165-supportsInterface}. - */ - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - return interfaceId == type(IERC165).interfaceId; - } -} diff --git a/contracts/NFT/utils/introspection/IERC165.sol b/contracts/NFT/utils/introspection/IERC165.sol deleted file mode 100644 index d68c78895c62..000000000000 --- a/contracts/NFT/utils/introspection/IERC165.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -/** - * @dev Interface of the ERC165 standard, as defined in the - * https://eips.ethereum.org/EIPS/eip-165[EIP]. - * - * Implementers can declare support of contract interfaces, which can then be - * queried by others ({ERC165Checker}). - * - * For an implementation, see {ERC165}. - */ -interface IERC165 { - /** - * @dev Returns true if this contract implements the interface defined by - * `interfaceId`. See the corresponding - * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] - * to learn more about how these ids are created. - * - * This function call must use less than 30 000 gas. - */ - function supportsInterface(bytes4 interfaceId) external view returns (bool); -} diff --git a/contracts/NFT/utils/structs/EnumerableMap.sol b/contracts/NFT/utils/structs/EnumerableMap.sol deleted file mode 100644 index 056fb49218b7..000000000000 --- a/contracts/NFT/utils/structs/EnumerableMap.sol +++ /dev/null @@ -1,224 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -import "./EnumerableSet.sol"; - -/** - * @dev Library for managing an enumerable variant of Solidity's - * https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`] - * type. - * - * Maps have the following properties: - * - * - Entries are added, removed, and checked for existence in constant time - * (O(1)). - * - Entries are enumerated in O(n). No guarantees are made on the ordering. - * - * ``` - * contract Example { - * // Add the library methods - * using EnumerableMap for EnumerableMap.UintToAddressMap; - * - * // Declare a set state variable - * EnumerableMap.UintToAddressMap private myMap; - * } - * ``` - * - * As of v3.0.0, only maps of type `uint256 -> address` (`UintToAddressMap`) are - * supported. - */ -library EnumerableMap { - using EnumerableSet for EnumerableSet.Bytes32Set; - - // To implement this library for multiple types with as little code - // repetition as possible, we write it in terms of a generic Map type with - // bytes32 keys and values. - // The Map implementation uses private functions, and user-facing - // implementations (such as Uint256ToAddressMap) are just wrappers around - // the underlying Map. - // This means that we can only create new EnumerableMaps for types that fit - // in bytes32. - - struct Map { - // Storage of keys - EnumerableSet.Bytes32Set _keys; - - mapping (bytes32 => bytes32) _values; - } - - /** - * @dev Adds a key-value pair to a map, or updates the value for an existing - * key. O(1). - * - * Returns true if the key was added to the map, that is if it was not - * already present. - */ - function _set(Map storage map, bytes32 key, bytes32 value) private returns (bool) { - map._values[key] = value; - return map._keys.add(key); - } - - /** - * @dev Removes a key-value pair from a map. O(1). - * - * Returns true if the key was removed from the map, that is if it was present. - */ - function _remove(Map storage map, bytes32 key) private returns (bool) { - delete map._values[key]; - return map._keys.remove(key); - } - - /** - * @dev Returns true if the key is in the map. O(1). - */ - function _contains(Map storage map, bytes32 key) private view returns (bool) { - return map._keys.contains(key); - } - - /** - * @dev Returns the number of key-value pairs in the map. O(1). - */ - function _length(Map storage map) private view returns (uint256) { - return map._keys.length(); - } - - /** - * @dev Returns the key-value pair stored at position `index` in the map. O(1). - * - * Note that there are no guarantees on the ordering of entries inside the - * array, and it may change when more entries are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - */ - function _at(Map storage map, uint256 index) private view returns (bytes32, bytes32) { - bytes32 key = map._keys.at(index); - return (key, map._values[key]); - } - - /** - * @dev Tries to returns the value associated with `key`. O(1). - * Does not revert if `key` is not in the map. - */ - function _tryGet(Map storage map, bytes32 key) private view returns (bool, bytes32) { - bytes32 value = map._values[key]; - if (value == bytes32(0)) { - return (_contains(map, key), bytes32(0)); - } else { - return (true, value); - } - } - - /** - * @dev Returns the value associated with `key`. O(1). - * - * Requirements: - * - * - `key` must be in the map. - */ - function _get(Map storage map, bytes32 key) private view returns (bytes32) { - bytes32 value = map._values[key]; - require(value != 0 || _contains(map, key), "EnumerableMap: nonexistent key"); - return value; - } - - /** - * @dev Same as {_get}, with a custom error message when `key` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {_tryGet}. - */ - function _get(Map storage map, bytes32 key, string memory errorMessage) private view returns (bytes32) { - bytes32 value = map._values[key]; - require(value != 0 || _contains(map, key), errorMessage); - return value; - } - - // UintToAddressMap - - struct UintToAddressMap { - Map _inner; - } - - /** - * @dev Adds a key-value pair to a map, or updates the value for an existing - * key. O(1). - * - * Returns true if the key was added to the map, that is if it was not - * already present. - */ - function set(UintToAddressMap storage map, uint256 key, address value) internal returns (bool) { - return _set(map._inner, bytes32(key), bytes32(uint256(uint160(value)))); - } - - /** - * @dev Removes a value from a set. O(1). - * - * Returns true if the key was removed from the map, that is if it was present. - */ - function remove(UintToAddressMap storage map, uint256 key) internal returns (bool) { - return _remove(map._inner, bytes32(key)); - } - - /** - * @dev Returns true if the key is in the map. O(1). - */ - function contains(UintToAddressMap storage map, uint256 key) internal view returns (bool) { - return _contains(map._inner, bytes32(key)); - } - - /** - * @dev Returns the number of elements in the map. O(1). - */ - function length(UintToAddressMap storage map) internal view returns (uint256) { - return _length(map._inner); - } - - /** - * @dev Returns the element stored at position `index` in the set. O(1). - * Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - */ - function at(UintToAddressMap storage map, uint256 index) internal view returns (uint256, address) { - (bytes32 key, bytes32 value) = _at(map._inner, index); - return (uint256(key), address(uint160(uint256(value)))); - } - - /** - * @dev Tries to returns the value associated with `key`. O(1). - * Does not revert if `key` is not in the map. - * - * _Available since v3.4._ - */ - function tryGet(UintToAddressMap storage map, uint256 key) internal view returns (bool, address) { - (bool success, bytes32 value) = _tryGet(map._inner, bytes32(key)); - return (success, address(uint160(uint256(value)))); - } - - /** - * @dev Returns the value associated with `key`. O(1). - * - * Requirements: - * - * - `key` must be in the map. - */ - function get(UintToAddressMap storage map, uint256 key) internal view returns (address) { - return address(uint160(uint256(_get(map._inner, bytes32(key))))); - } - - /** - * @dev Same as {get}, with a custom error message when `key` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryGet}. - */ - function get(UintToAddressMap storage map, uint256 key, string memory errorMessage) internal view returns (address) { - return address(uint160(uint256(_get(map._inner, bytes32(key), errorMessage)))); - } -} diff --git a/contracts/NFT/utils/structs/EnumerableSet.sol b/contracts/NFT/utils/structs/EnumerableSet.sol deleted file mode 100644 index 579bb85b9145..000000000000 --- a/contracts/NFT/utils/structs/EnumerableSet.sol +++ /dev/null @@ -1,295 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.7.6; - -/** - * @dev Library for managing - * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive - * types. - * - * Sets have the following properties: - * - * - Elements are added, removed, and checked for existence in constant time - * (O(1)). - * - Elements are enumerated in O(n). No guarantees are made on the ordering. - * - * ``` - * contract Example { - * // Add the library methods - * using EnumerableSet for EnumerableSet.AddressSet; - * - * // Declare a set state variable - * EnumerableSet.AddressSet private mySet; - * } - * ``` - * - * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) - * and `uint256` (`UintSet`) are supported. - */ -library EnumerableSet { - // To implement this library for multiple types with as little code - // repetition as possible, we write it in terms of a generic Set type with - // bytes32 values. - // The Set implementation uses private functions, and user-facing - // implementations (such as AddressSet) are just wrappers around the - // underlying Set. - // This means that we can only create new EnumerableSets for types that fit - // in bytes32. - - struct Set { - // Storage of set values - bytes32[] _values; - - // Position of the value in the `values` array, plus 1 because index 0 - // means a value is not in the set. - mapping (bytes32 => uint256) _indexes; - } - - /** - * @dev Add a value to a set. O(1). - * - * Returns true if the value was added to the set, that is if it was not - * already present. - */ - function _add(Set storage set, bytes32 value) private returns (bool) { - if (!_contains(set, value)) { - set._values.push(value); - // The value is stored at length-1, but we add 1 to all indexes - // and use 0 as a sentinel value - set._indexes[value] = set._values.length; - return true; - } else { - return false; - } - } - - /** - * @dev Removes a value from a set. O(1). - * - * Returns true if the value was removed from the set, that is if it was - * present. - */ - function _remove(Set storage set, bytes32 value) private returns (bool) { - // We read and store the value's index to prevent multiple reads from the same storage slot - uint256 valueIndex = set._indexes[value]; - - if (valueIndex != 0) { // Equivalent to contains(set, value) - // To delete an element from the _values array in O(1), we swap the element to delete with the last one in - // the array, and then remove the last element (sometimes called as 'swap and pop'). - // This modifies the order of the array, as noted in {at}. - - uint256 toDeleteIndex = valueIndex - 1; - uint256 lastIndex = set._values.length - 1; - - if (lastIndex != toDeleteIndex) { - bytes32 lastvalue = set._values[lastIndex]; - - // Move the last value to the index where the value to delete is - set._values[toDeleteIndex] = lastvalue; - // Update the index for the moved value - set._indexes[lastvalue] = valueIndex; // Replace lastvalue's index to valueIndex - } - - // Delete the slot where the moved value was stored - set._values.pop(); - - // Delete the index for the deleted slot - delete set._indexes[value]; - - return true; - } else { - return false; - } - } - - /** - * @dev Returns true if the value is in the set. O(1). - */ - function _contains(Set storage set, bytes32 value) private view returns (bool) { - return set._indexes[value] != 0; - } - - /** - * @dev Returns the number of values on the set. O(1). - */ - function _length(Set storage set) private view returns (uint256) { - return set._values.length; - } - - /** - * @dev Returns the value stored at position `index` in the set. O(1). - * - * Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - */ - function _at(Set storage set, uint256 index) private view returns (bytes32) { - return set._values[index]; - } - - // Bytes32Set - - struct Bytes32Set { - Set _inner; - } - - /** - * @dev Add a value to a set. O(1). - * - * Returns true if the value was added to the set, that is if it was not - * already present. - */ - function add(Bytes32Set storage set, bytes32 value) internal returns (bool) { - return _add(set._inner, value); - } - - /** - * @dev Removes a value from a set. O(1). - * - * Returns true if the value was removed from the set, that is if it was - * present. - */ - function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) { - return _remove(set._inner, value); - } - - /** - * @dev Returns true if the value is in the set. O(1). - */ - function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) { - return _contains(set._inner, value); - } - - /** - * @dev Returns the number of values in the set. O(1). - */ - function length(Bytes32Set storage set) internal view returns (uint256) { - return _length(set._inner); - } - - /** - * @dev Returns the value stored at position `index` in the set. O(1). - * - * Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - */ - function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) { - return _at(set._inner, index); - } - - // AddressSet - - struct AddressSet { - Set _inner; - } - - /** - * @dev Add a value to a set. O(1). - * - * Returns true if the value was added to the set, that is if it was not - * already present. - */ - function add(AddressSet storage set, address value) internal returns (bool) { - return _add(set._inner, bytes32(uint256(uint160(value)))); - } - - /** - * @dev Removes a value from a set. O(1). - * - * Returns true if the value was removed from the set, that is if it was - * present. - */ - function remove(AddressSet storage set, address value) internal returns (bool) { - return _remove(set._inner, bytes32(uint256(uint160(value)))); - } - - /** - * @dev Returns true if the value is in the set. O(1). - */ - function contains(AddressSet storage set, address value) internal view returns (bool) { - return _contains(set._inner, bytes32(uint256(uint160(value)))); - } - - /** - * @dev Returns the number of values in the set. O(1). - */ - function length(AddressSet storage set) internal view returns (uint256) { - return _length(set._inner); - } - - /** - * @dev Returns the value stored at position `index` in the set. O(1). - * - * Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - */ - function at(AddressSet storage set, uint256 index) internal view returns (address) { - return address(uint160(uint256(_at(set._inner, index)))); - } - - - // UintSet - - struct UintSet { - Set _inner; - } - - /** - * @dev Add a value to a set. O(1). - * - * Returns true if the value was added to the set, that is if it was not - * already present. - */ - function add(UintSet storage set, uint256 value) internal returns (bool) { - return _add(set._inner, bytes32(value)); - } - - /** - * @dev Removes a value from a set. O(1). - * - * Returns true if the value was removed from the set, that is if it was - * present. - */ - function remove(UintSet storage set, uint256 value) internal returns (bool) { - return _remove(set._inner, bytes32(value)); - } - - /** - * @dev Returns true if the value is in the set. O(1). - */ - function contains(UintSet storage set, uint256 value) internal view returns (bool) { - return _contains(set._inner, bytes32(value)); - } - - /** - * @dev Returns the number of values on the set. O(1). - */ - function length(UintSet storage set) internal view returns (uint256) { - return _length(set._inner); - } - - /** - * @dev Returns the value stored at position `index` in the set. O(1). - * - * Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - */ - function at(UintSet storage set, uint256 index) internal view returns (uint256) { - return uint256(_at(set._inner, index)); - } -} diff --git a/contracts/Proxy.sol b/contracts/Proxy.sol deleted file mode 100644 index 7f51c61d3e46..000000000000 --- a/contracts/Proxy.sol +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.6.0 <0.8.0; - -/** - * @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM - * instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to - * be specified by overriding the virtual {_implementation} function. - * - * Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a - * different contract through the {_delegate} function. - * - * The success and return data of the delegated call will be returned back to the caller of the proxy. - */ -abstract contract Proxy { - /** - * @dev Delegates the current call to `implementation`. - * - * This function does not return to its internall call site, it will return directly to the external caller. - */ - function _delegate(address implementation) internal virtual { - // solhint-disable-next-line no-inline-assembly - assembly { - // Copy msg.data. We take full control of memory in this inline assembly - // block because it will not return to Solidity code. We overwrite the - // Solidity scratch pad at memory position 0. - calldatacopy(0, 0, calldatasize()) - - // Call the implementation. - // out and outsize are 0 because we don't know the size yet. - let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) - - // Copy the returned data. - returndatacopy(0, 0, returndatasize()) - - switch result - // delegatecall returns 0 on error. - case 0 { revert(0, returndatasize()) } - default { return(0, returndatasize()) } - } - } - - /** - * @dev This is a virtual function that should be overriden so it returns the address to which the fallback function - * and {_fallback} should delegate. - */ - function _implementation() internal view virtual returns (address); - - /** - * @dev Delegates the current call to the address returned by `_implementation()`. - * - * This function does not return to its internall call site, it will return directly to the external caller. - */ - function _fallback() internal virtual { - _beforeFallback(); - _delegate(_implementation()); - } - - /** - * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other - * function in the contract matches the call data. - */ - fallback () external payable virtual { - _fallback(); - } - - /** - * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if call data - * is empty. - */ - receive () external payable virtual { - _fallback(); - } - - /** - * @dev Hook that is called before falling back to the implementation. Can happen as part of a manual `_fallback` - * call, or as part of the Solidity `fallback` or `receive` functions. - * - * If overriden should call `super._beforeFallback()`. - */ - function _beforeFallback() internal virtual { - } -} diff --git a/contracts/UpgradeableProxy.sol b/contracts/UpgradeableProxy.sol deleted file mode 100644 index 40f24c879645..000000000000 --- a/contracts/UpgradeableProxy.sol +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.6.0 <0.8.0; - -import "./Proxy.sol"; -import "./Address.sol"; - -/** - * @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an - * implementation address that can be changed. This address is stored in storage in the location specified by - * https://eips.ethereum.org/EIPS/eip-1967[EIP1967], so that it doesn't conflict with the storage layout of the - * implementation behind the proxy. - * - * Upgradeability is only provided internally through {_upgradeTo}. For an externally upgradeable proxy see - * {TransparentUpgradeableProxy}. - */ -contract UpgradeableProxy is Proxy { - /** - * @dev Initializes the upgradeable proxy with an initial implementation specified by `_logic`. - * - * If `_data` is nonempty, it's used as data in a delegate call to `_logic`. This will typically be an encoded - * function call, and allows initializating the storage of the proxy like a Solidity constructor. - */ - constructor(address _logic, bytes memory _data) public payable { - assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); - _setImplementation(_logic); - if(_data.length > 0) { - Address.functionDelegateCall(_logic, _data); - } - } - - /** - * @dev Emitted when the implementation is upgraded. - */ - event Upgraded(address indexed implementation); - - /** - * @dev Storage slot with the address of the current implementation. - * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is - * validated in the constructor. - */ - bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - - /** - * @dev Returns the current implementation address. - */ - function _implementation() internal view virtual override returns (address impl) { - bytes32 slot = _IMPLEMENTATION_SLOT; - // solhint-disable-next-line no-inline-assembly - assembly { - impl := sload(slot) - } - } - - /** - * @dev Upgrades the proxy to a new implementation. - * - * Emits an {Upgraded} event. - */ - function _upgradeTo(address newImplementation) internal virtual { - _setImplementation(newImplementation); - emit Upgraded(newImplementation); - } - - /** - * @dev Stores a new address in the EIP1967 implementation slot. - */ - function _setImplementation(address newImplementation) private { - require(Address.isContract(newImplementation), "UpgradeableProxy: new implementation is not a contract"); - - bytes32 slot = _IMPLEMENTATION_SLOT; - - // solhint-disable-next-line no-inline-assembly - assembly { - sstore(slot, newImplementation) - } - } -} diff --git a/deployment/local/addresses.json b/deployment/local/addresses.json index 2018cf3cc710..771e3caf5a8a 100644 --- a/deployment/local/addresses.json +++ b/deployment/local/addresses.json @@ -1,8 +1,8 @@ { - "L1ERC20": "0x3626D6562d560a13121B8AEb839F70BBFa973C8A", - "L2DepositedERC20": "0xC473e68E1621AA5913863906962Ca7BEb50e0175", - "L1ERC20Gateway": "0x62B5b86CFdD18245767F42716CEB236dE6484B42", + "L1ERC20": "0xA80CFF28362A9a6267d99C06aA81c165386D9Cf2", + "L2DepositedERC20": "0x195d3769BDA68410F5FB3aa79f2DC4D7Cf67befe", + "L1ERC20Gateway": "0xBbD4C168b54A13a6574440F4967dc4Ced33F745d", "l1ETHGatewayAddress": "0x4F53A01556Dc6f120db9a1b4caE786D5f0b5792C", "l1MessengerAddress": "0xA6404B184Ad3f6F41b6472f02ba45f25C6A66d49", - "L2ERC721": "0x53f0d9881f8CD6D90479C3532e98714e37fb3Aa0" + "L2ERC721": "0xDB1b9ffB4c6630cAEB032e1F4dAe928B167CeceF" } \ No newline at end of file diff --git a/package.json b/package.json index 7af17a8e45f9..b4fd32f25e04 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "build": "./scripts/build.sh", "build:contracts": "hardhat compile", "build:contracts:ovm": "hardhat compile --network omgx", - "deploy": "./scripts/build.sh && hardhat --network omgx test && ./scripts/copy.sh" + "deploy": "./scripts/build.sh && hardhat --network omgx test && ./scripts/copy.sh", + "postinstall": "patch-package" }, "devDependencies": { "@eth-optimism/hardhat-ovm": "^0.0.2", @@ -37,6 +38,7 @@ "@openzeppelin/contracts": "^3.3.0", "dotenv": "^8.2.0", "enyalabs_contracts": "git+ssh://git@github.com:enyalabs/contracts.git#rinkeby", - "glob": "^7.1.6" + "glob": "^7.1.6", + "patch-package": "^6.4.7" } } diff --git a/patches/@openzeppelin+contracts+3.4.1.patch b/patches/@openzeppelin+contracts+3.4.1.patch new file mode 100644 index 000000000000..e8d8e2d92626 --- /dev/null +++ b/patches/@openzeppelin+contracts+3.4.1.patch @@ -0,0 +1,22 @@ +diff --git a/node_modules/@openzeppelin/contracts/utils/Address.sol b/node_modules/@openzeppelin/contracts/utils/Address.sol +index 42a9dc1..bb610c2 100644 +--- a/node_modules/@openzeppelin/contracts/utils/Address.sol ++++ b/node_modules/@openzeppelin/contracts/utils/Address.sol +@@ -51,7 +51,7 @@ library Address { + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { +- require(address(this).balance >= amount, "Address: insufficient balance"); ++ //require(address(this).balance >= amount, "Address: insufficient balance"); + + // solhint-disable-next-line avoid-low-level-calls, avoid-call-value + (bool success, ) = recipient.call{ value: amount }(""); +@@ -112,7 +112,7 @@ library Address { + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) { +- require(address(this).balance >= value, "Address: insufficient balance for call"); ++ //require(address(this).balance >= value, "Address: insufficient balance for call"); + require(isContract(target), "Address: call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls diff --git a/test/nft.spec.ts b/test/nft.spec.ts index ee4feeea6eb1..5143ad448fbc 100644 --- a/test/nft.spec.ts +++ b/test/nft.spec.ts @@ -34,7 +34,7 @@ describe('NFT Test', async () => { //Test Marc's BioBase NFT system const nftName = 'BioBase' - const nftSymbol = 'BEE' //Bioeconomy Explodes + const nftSymbol = 'BEE' //BioEconomy Explodes const getBalances = async ( _address: string, From 23dec2f8aedd3155b123b5ed015311f746ff1651 Mon Sep 17 00:00:00 2001 From: Jan Liphardt <jan@enya.ai> Date: Thu, 6 May 2021 13:16:43 -0700 Subject: [PATCH 03/10] wip --- README.md | 92 ++++--- contracts/ERC721Mock.sol | 2 - deployment/local/addresses.json | 10 +- optional/setup_test.spec.ts | 2 +- test/a_setup.spec.ts | 448 ++++++++++++++++++++++++++++++++ test/b_nft.spec.ts | 128 +++++++++ test/nft.spec.ts | 272 ------------------- wallet/package.json | 2 - 8 files changed, 634 insertions(+), 322 deletions(-) create mode 100644 test/a_setup.spec.ts create mode 100644 test/b_nft.spec.ts delete mode 100644 test/nft.spec.ts diff --git a/README.md b/README.md index 008c83e833f6..58c29aee74c1 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,10 @@ Create a `.env` file in the root directory of this project. Add environment-spec ### 1.1 Using Rinkeby Testnet -If you just want to work on the wallet, you can use the stable testnet on Rinkeby and AWS. To test on Rinkeby (ChainID 4), you will need an Infura key and two accounts with Rinkeby ETH in them. The test wallets must contain enough ETH to cover the tests. The `.env` parameters are +If you just want to work on the wallet, you can use the stable testnet on Rinkeby (ChainID 4) and AWS. To test on Rinkeby, you will need an Infura key and two accounts with Rinkeby ETH in them. The test wallets must contain enough ETH to cover the tests. The `.env` parameters are ```bash + NODE_ENV=local L1_NODE_WEB3_URL=https://rinkeby.infura.io/v3/KEY L2_NODE_WEB3_URL=http://54.161.5.63:8545 @@ -35,36 +36,40 @@ ETH1_ADDRESS_RESOLVER_ADDRESS=0xa32cf2433ba24595d3aCE5cc9A7079d3f1CC5E0c TEST_PRIVATE_KEY_1=0xPRIVATE KEY OF THE FIRST TEST WALLET TEST_PRIVATE_KEY_2=0xPRIVATE KEY OF THE SECOND TEST WALLET TARGET_GAS_LIMIT=9000000000 -CHAIN_ID=420 +CHAIN_ID=28 + ``` -### 1.2 Using Local Net +If you do not know what these values are, you can get them from the values written to the terminal as your local OMGX spins up. + +### 1.2 Using a Local Testnet If would like to change the wallet-associated smart contracts and/or work on other aspects of the system, you should run a local development system. You can do this by ```bash -# Git clone with submodules + $ git clone git@github.com:enyalabs/optimism-integration.git $ cd optimism-integration $ docker-compose pull $ yarn install $ ./up_local.sh -``` - -If you do not know what these values are, you can get them from the values written to the terminal as your local OMGX spins up. +``` As the system boots, you'll see several things zip by in the terminal that you will need to correctly configure your `.env`, namely the values for the ```bash + ETH1_ADDRESS_RESOLVER_ADDRESS=0x______ TEST_PRIVATE_KEY_1=0x_____ TEST_PRIVATE_KEY_2=0x______ + ``` -For the test private keys, we normally use the ones generated by hardhat as it spins up the local L1. Each of those is funded with 1000 ETH. You can get the test private keys from the hardhat (`l1_chain`) Docker terminal or from your main terminal. You will also need the `ADDRESS_RESOLVER_ADDRESS`, which will zip by as your local system deploys (see the `deployer` Docker terminal output or your main terminal window). Fill these values into your `.env`. Your final `.env` should look something like this +For the test private keys, we normally use the ones generated by hardhat as it sets up the local L1. Each of those is funded with 1000 ETH. You can get the test private keys from the hardhat (`l1_chain`) Docker terminal or from your main terminal. You will also need the `ADDRESS_RESOLVER_ADDRESS`, which will zip by as your local system deploys (see the `deployer` Docker terminal output or your main terminal window). Fill these values into your `.env`. Your final `.env` should look something like this ```bash + NODE_ENV=local L1_NODE_WEB3_URL=http://localhost:9545 L2_NODE_WEB3_URL=http://localhost:8545 @@ -72,15 +77,18 @@ ETH1_ADDRESS_RESOLVER_ADDRESS=0x3e4CFaa8730092552d9425575E49bB542e329981 TEST_PRIVATE_KEY_1=0x754fde3f5e60ef2c7649061e06957c29017fe21032a8017132c0078e37f6193a TEST_PRIVATE_KEY_2=0x23d9aeeaa08ab710a57972eb56fc711d9ab13afdecc92c89586e0150bfa380a6 TARGET_GAS_LIMIT=9000000000 -CHAIN_ID=420 +CHAIN_ID=28 + ``` ## 2. Running the Integration Tests ```bash + $ yarn install $ yarn build #builds the contracts $ yarn deploy #if needed - this will test and deploy the contracts, and write their addresses to /deployments/addresses.json + ``` The information generated during the deploy (e.g the `/deployments/addresses.json`) is used by the web wallet front end to set things up correctly. **The full test suite includes some very slow transactions such as withdrawals, which can take 300 seconds each. Please be patient.** @@ -140,22 +148,22 @@ If you are working on a local testnet, please deploy **L2LiquidityPool.sol** fir ```javascript - L2LiquidityPool = await Factory__L2LiquidityPool.deploy( - env.watcher.l2.messengerAddress, - ) - await L2LiquidityPool.deployTransaction.wait() - - L1LiquidityPool = await Factory__L1LiquidityPool.deploy( - L2LiquidityPool.address, - env.watcher.l1.messengerAddress, - env.L2ETHGateway.address, - 3 - ) - await L1LiquidityPool.deployTransaction.wait() - - const L2LiquidityPoolTX = await L2LiquidityPool.init(L1LiquidityPool.address, "3") - await L2LiquidityPoolTX.wait() - console.log(' L2 LP initialized:',L2LiquidityPoolTX.hash); + L2LiquidityPool = await Factory__L2LiquidityPool.deploy( + env.watcher.l2.messengerAddress, + ) + await L2LiquidityPool.deployTransaction.wait() + + L1LiquidityPool = await Factory__L1LiquidityPool.deploy( + L2LiquidityPool.address, + env.watcher.l1.messengerAddress, + env.L2ETHGateway.address, + 3 + ) + await L1LiquidityPool.deployTransaction.wait() + + const L2LiquidityPoolTX = await L2LiquidityPool.init(L1LiquidityPool.address, "3") + await L2LiquidityPoolTX.wait() + console.log(' L2 LP initialized:',L2LiquidityPoolTX.hash); ``` @@ -170,21 +178,27 @@ Used to swap ERC20 tokens. * `expire` sets the status of the swap to be **EXPIRED**. * `check` returns the **Swap** construct -## 4. Running the web wallet +## 4. Running the Web Wallet ```bash + $ cd /wallet $ yarn install $ yarn start + ``` You will need to set up MetaMask to know about the two accounts you are using for local testing. You will need to point MetaMask at your local chains (at :9545 and :8545) and add the account used for testing, which is typically whatever you used a `TEST_PRIVATE_KEY_2` aka `Alice`: ```bash + TEST_PRIVATE_KEY_2=0x23d9aeeaa08ab710a57972eb56fc711d9ab13afdecc92c89586e0150bfa380a6 + ``` -You also need to add networks to your MetaMask. +### Metamask Settings + +You also need to add networks to your MetaMask: * Local L1 @@ -197,31 +211,27 @@ You also need to add networks to your MetaMask. ``` URL = http://localhost:8545 - ChainID = 420 + ChainID = 28 ``` -> You have to reset the MetaMask when you re-run the local networks! The reset button is in **Settings > Advanced > Reset Account**. +> You might have to reset MetaMask when you re-start the local network. The reset button is in **Settings > Advanced > Reset Account**. -* Rinkeby +* Rinkeby L1 - Select Rinkeby Test Network + Select the dewfault Rinkeby Test Network -* AWS L2 +* OMGX L2 ``` URL = http://18.208.138.49:8545 - ChainID = 420 + ChainID = 28 ``` -Here's a step-by-step process on how to do that while in your browser: - -1. Open MetaMask the browser extension and click on your currently connected network at the top pane, next to your jazzicon. - -2. You'll see a modal open up with list of networks. Click on the "Custom RPC" button at the very bottom. (See example image below.) +### Wallet Use and Supported Functions -  +1. Open the MetaMask browser extension and click on your currently connected network at the top pane, next to your jazzicon. -3. Next, you'll enter in the network parameters and save it: +2. You'll see a modal open up with list of networks. If you are working on a local system, click on the "Custom RPC" button at the very bottom. Enter/select your L1 and L2 network parameters. -  +3. Account Page. here you can see your balances, and move tokens from L1 to L2, and back. diff --git a/contracts/ERC721Mock.sol b/contracts/ERC721Mock.sol index f3df03ab33d7..bade43b9fef3 100644 --- a/contracts/ERC721Mock.sol +++ b/contracts/ERC721Mock.sol @@ -13,9 +13,7 @@ contract ERC721Mock is ERC721 { function mintNFT(address recipient, uint256 tokenId, string memory tokenURI) public returns (uint256) { safeMint(recipient, tokenId); - setTokenURI(tokenId, tokenURI); - return tokenId; } diff --git a/deployment/local/addresses.json b/deployment/local/addresses.json index 771e3caf5a8a..ecee8d0e7fd2 100644 --- a/deployment/local/addresses.json +++ b/deployment/local/addresses.json @@ -1,8 +1,10 @@ { - "L1ERC20": "0xA80CFF28362A9a6267d99C06aA81c165386D9Cf2", - "L2DepositedERC20": "0x195d3769BDA68410F5FB3aa79f2DC4D7Cf67befe", - "L1ERC20Gateway": "0xBbD4C168b54A13a6574440F4967dc4Ced33F745d", + "L1LiquidityPool": "0x9C5E699dF0dBEAb06afa715a4104D6C2F9a19A78", + "L2LiquidityPool": "0x64D8e4ebFB202c459d831F0FdCD49e4e7872D97d", + "L1ERC20": "0x998b1F40925f4F7bB55784EFC9569E236AB3ca23", + "L2DepositedERC20": "0x6271d5bB96218ca26EFFd94b47eb6517f095d168", + "L1ERC20Gateway": "0x2C717D2ACd2FC736dFC2CA9b95ccb2dCFAcC574c", "l1ETHGatewayAddress": "0x4F53A01556Dc6f120db9a1b4caE786D5f0b5792C", "l1MessengerAddress": "0xA6404B184Ad3f6F41b6472f02ba45f25C6A66d49", - "L2ERC721": "0xDB1b9ffB4c6630cAEB032e1F4dAe928B167CeceF" + "L2ERC721": "0x8c0D00a0C3E7F2F6f34CaaA251C19Cd4EacD43D4" } \ No newline at end of file diff --git a/optional/setup_test.spec.ts b/optional/setup_test.spec.ts index 616cdb1438c8..29493e682b29 100644 --- a/optional/setup_test.spec.ts +++ b/optional/setup_test.spec.ts @@ -15,7 +15,7 @@ import { OptimismEnv } from './shared/env' import * as fs from 'fs' -describe('Token, Bridge, and Swap Pool Setup and Test', async () => { +describe('System Setup', async () => { let Factory__L1LiquidityPool: ContractFactory let Factory__L2LiquidityPool: ContractFactory diff --git a/test/a_setup.spec.ts b/test/a_setup.spec.ts new file mode 100644 index 000000000000..7ca24ba8dbee --- /dev/null +++ b/test/a_setup.spec.ts @@ -0,0 +1,448 @@ +import { expect } from 'chai' +import { Contract, ContractFactory, BigNumber, Wallet, utils, providers } from 'ethers' +import { Direction } from './shared/watcher-utils' + +import L1ERC20Json from '../artifacts/contracts/ERC20.sol/ERC20.json' +import L1ERC20GatewayJson from '../artifacts/contracts/L1ERC20Gateway.sol/L1ERC20Gateway.json' + +import L2DepositedERC20Json from '../artifacts-ovm/contracts/L2DepositedERC20.sol/L2DepositedERC20.json' + +import L1LiquidityPoolJson from '../artifacts/contracts/L1LiquidityPool.sol/L1LiquidityPool.json' +import L2LiquidityPoolJson from '../artifacts-ovm/contracts/L2LiquidityPool.sol/L2LiquidityPool.json' + +import { OptimismEnv } from './shared/env' + +import * as fs from 'fs' + +describe('System setup', async () => { + + let Factory__L1LiquidityPool: ContractFactory + let Factory__L2LiquidityPool: ContractFactory + let Factory__L1ERC20: ContractFactory + let Factory__L2DepositedERC20: ContractFactory + let Factory__L1ERC20Gateway: ContractFactory + + let L1LiquidityPool: Contract + let L2LiquidityPool: Contract + let L1ERC20: Contract + let L2DepositedERC20: Contract + let L1ERC20Gateway: Contract + + let env: OptimismEnv + + //Test ERC20 + const initialAmount = utils.parseEther("10000000000") + const tokenName = 'OMGX Test' + const tokenDecimals = 18 + const tokenSymbol = 'OMGX' + + /************* BOB owns all the pools, and ALICE mints a new token ***********/ + before(async () => { + + env = await OptimismEnv.new() + + Factory__L1LiquidityPool = new ContractFactory( + L1LiquidityPoolJson.abi, + L1LiquidityPoolJson.bytecode, + env.bobl1Wallet + ) + + Factory__L2LiquidityPool = new ContractFactory( + L2LiquidityPoolJson.abi, + L2LiquidityPoolJson.bytecode, + env.bobl2Wallet + ) + + Factory__L1ERC20 = new ContractFactory( + L1ERC20Json.abi, + L1ERC20Json.bytecode, + env.bobl1Wallet + ) + + Factory__L2DepositedERC20 = new ContractFactory( + L2DepositedERC20Json.abi, + L2DepositedERC20Json.bytecode, + env.bobl2Wallet + ) + + Factory__L1ERC20Gateway = new ContractFactory( + L1ERC20GatewayJson.abi, + L1ERC20GatewayJson.bytecode, + env.bobl1Wallet + ) + + }) + + before(async () => { + + //Set up the L2LP + L2LiquidityPool = await Factory__L2LiquidityPool.deploy( + env.watcher.l2.messengerAddress, + ) + await L2LiquidityPool.deployTransaction.wait() + console.log("L2LiquidityPool deployed to:", L2LiquidityPool.address) + + L1LiquidityPool = await Factory__L1LiquidityPool.deploy( + L2LiquidityPool.address, + env.watcher.l1.messengerAddress, + env.L2ETHGateway.address, + 3 + ) + await L1LiquidityPool.deployTransaction.wait() + console.log("L1LiquidityPool deployed to:", L1LiquidityPool.address) + + const L2LiquidityPoolTX = await L2LiquidityPool.init(L1LiquidityPool.address, /*this is the fee = */ "3", env.L2ETHGateway.address) + //The '3' here denotes the fee to charge, i.e. fee = 3% + await L2LiquidityPoolTX.wait() + console.log('L2 LP initialized with the L1LiquidityPool.address:',L2LiquidityPoolTX.hash); + + //Mint a new token on L1 and set up the L1 and L2 infrastructure + // [initialSupply, name, decimals, symbol] + // this is owned by bobl1Wallet + L1ERC20 = await Factory__L1ERC20.deploy( + initialAmount, + tokenName, + tokenDecimals, + tokenSymbol + ) + await L1ERC20.deployTransaction.wait() + console.log("L1ERC20 deployed to:", L1ERC20.address) + + //Set up things on L2 for this new token + // [l2MessengerAddress, name, symbol] + L2DepositedERC20 = await Factory__L2DepositedERC20.deploy( + env.watcher.l2.messengerAddress, + tokenName, + tokenSymbol + ) + await L2DepositedERC20.deployTransaction.wait() + console.log("L2DepositedERC20 deployed to:", L2DepositedERC20.address) + + //Deploy a gateway for the new token + // [L1_ERC20.address, OVM_L2DepositedERC20.address, l1MessengerAddress] + L1ERC20Gateway = await Factory__L1ERC20Gateway.deploy( + L1ERC20.address, + L2DepositedERC20.address, + env.watcher.l1.messengerAddress, + ) + await L1ERC20Gateway.deployTransaction.wait() + console.log("L1ERC20Gateway deployed to:", L1ERC20Gateway.address) + + //Initialize the contracts for the new token + const initL2 = await L2DepositedERC20.init(L1ERC20Gateway.address); + await initL2.wait(); + console.log('L2 ERC20 initialized:',initL2.hash); + + //Register Erc20 token addresses in both Liquidity pools + await L1LiquidityPool.registerTokenAddress(L1ERC20.address, L2DepositedERC20.address); + await L2LiquidityPool.registerTokenAddress(L1ERC20.address, L2DepositedERC20.address); + + }) + + before(async () => { + //keep track of where things are for future use by the front end + console.log("\n\n********************************\nSaving all key addresses") + + const addresses = { + L1LiquidityPool: L1LiquidityPool.address, + L2LiquidityPool: L2LiquidityPool.address, + L1ERC20: L1ERC20.address, + L2DepositedERC20: L2DepositedERC20.address, + L1ERC20Gateway: L1ERC20Gateway.address, + l1ETHGatewayAddress: env.L1ETHGateway.address, + l1MessengerAddress: env.l1MessengerAddress + } + + console.log(JSON.stringify(addresses, null, 2)) + + fs.writeFile('./deployment/local/addresses.json', JSON.stringify(addresses, null, 2), err => { + if (err) { + console.log('Error writing addresses to file:', err) + } else { + console.log('Successfully wrote addresses to file') + } + }) + + console.log('********************************\n\n') + + }) + + it('should transfer ERC20 from Bob to Alice', async () => { + const transferAmount = utils.parseEther("500") + + //L1ERC20 is owned by Bob + const preERC20Balances = await L1ERC20.balanceOf(env.alicel1Wallet.address); + + const transferERC20TX = await L1ERC20.transfer( + env.alicel1Wallet.address, + transferAmount, + ) + await transferERC20TX.wait() + + const postERC20Balance = await L1ERC20.balanceOf(env.alicel1Wallet.address); + + expect(postERC20Balance).to.deep.eq( + preERC20Balances.add(transferAmount) + ) + }) + + // it('should add initial ETH and ERC20 to the L1 Liquidity Pool', async () => { + + // // ************************************************** + // // Only the contract owner (Bob) can deposit ETH into L1 LP + // // ************************************************** + // const addETHAmount = utils.parseEther("5") + // const addERC20Amount = utils.parseEther("500") + + // const l1ProviderLL = providers.getDefaultProvider(process.env.L1_NODE_WEB3_URL) + + // // Add ETH + // const preETHBalances = await getBalances("0x0000000000000000000000000000000000000000") + // const preL1LPETHBalance = await l1ProviderLL.getBalance(L1LiquidityPool.address) + + // //const fee = BigNumber.from(189176000000000) + // const chainID = await env.bobl1Wallet.getChainId() + // const gasPrice = await env.bobl1Wallet.getGasPrice() + + // const gasEstimate = await env.bobl1Wallet.estimateGas({ + // from: env.bobl1Wallet.address, + // to: L1LiquidityPool.address, + // value: addETHAmount + // }) + + // //Bob, the owner of the L1LiquidityPool, sends funds into the L1LP + // const depositETHTX = await env.bobl1Wallet.sendTransaction({ + // from: env.bobl1Wallet.address, + // to: L1LiquidityPool.address, + // value: addETHAmount + // }) + // await depositETHTX.wait() + + // const postETHBalances = await getBalances("0x0000000000000000000000000000000000000000") + // const postL1LPETHBalance = await l1ProviderLL.getBalance(L1LiquidityPool.address) + + // const receipt = await l1ProviderLL.getTransactionReceipt(depositETHTX.hash); + // //console.log("transaction receipt:",receipt) + + // //add fee calculation + // const feeSimple = depositETHTX.gasLimit.mul(depositETHTX.gasPrice) + + // console.log('\nFEE DEBUG INFORMATION') + // console.log("ChainID:",chainID) + // console.log("GasPrice (gWei):",utils.formatUnits(gasPrice, "gwei")) + // console.log("Fee actually paid:",utils.formatUnits(preETHBalances.bobL1Balance.sub(addETHAmount).sub(postETHBalances.bobL1Balance), "gwei")) + // console.log("Fee gasLimit*gasPrice:",utils.formatUnits(feeSimple, "gwei")) + // console.log("GasEstimate (gWei):",utils.formatUnits(gasEstimate, "gwei")) + // console.log("GasUsed (gWei):",utils.formatUnits(receipt.gasUsed, "gwei")) + // console.log('\n') + + // /* + // console.log("Fee actually paid:",postETHBalances.bobL1Balance.sub(addAmount).sub(postETHBalances.bobL1Balance).toString()) + // console.log("Fee gasLimit*gasPrice:",feeSimple.toString()) + // console.log("GasEstimate:",gasEstimate.toString()) + // console.log("GasUsed:",gasUsed.toString()) + // */ + + // /* + // IF YOU SEND ZERO AMOUNT, these numbers will be off. For example, + + // Fee actually paid: 189176.0 + // Fee gasLimit*gasPrice: 202464.0 + // GasEstimate (gWei): 0.000025308 + // GasUsed (gWei): 0.000023647 + + // IF YOU SEND NONZERO AMOUNT + + // Fee actually paid: 342776.0 + // Fee gasLimit*gasPrice: 342776.0 + // GasEstimate (gWei): 0.000042847 + // GasUsed (gWei): 0.000042847 + // */ + + // //Bob should have less money now - this breaks for zero value transfer + // expect(postETHBalances.bobL1Balance).to.deep.eq( + // preETHBalances.bobL1Balance.sub(addETHAmount).sub(feeSimple) + // ) + + // //He paid into the L1LP + // expect(postL1LPETHBalance).to.deep.eq( + // preL1LPETHBalance.add(addETHAmount) + // ) + + // //Alice did not pay, so no change + // expect(postETHBalances.aliceL1Balance).to.deep.eq( + // preETHBalances.aliceL1Balance + // ) + + // // Add ERC20 Token + // const preL1LPERC20Balance = await L1ERC20.balanceOf(L1LiquidityPool.address) + + // const approveERC20TX = await L1ERC20.approve( + // L1LiquidityPool.address, + // addERC20Amount, + // ) + // await approveERC20TX.wait() + + // const depositERC20TX = await L1LiquidityPool.ownerAddERC20Liquidity( + // addERC20Amount, + // L1ERC20.address, + // ); + // await depositERC20TX.wait(); + + // const postL1LPERC20Balance = await L1ERC20.balanceOf(L1LiquidityPool.address) + + // expect(postL1LPERC20Balance).to.deep.eq( + // preL1LPERC20Balance.add(addERC20Amount) + // ) + // }) + + // it('should add initial oETH and ERC20 to the L2 Liquidity Pool', async () => { + + // const depositL2oWETHAmount = utils.parseEther("5.1") + // const addoWETHAmount = utils.parseEther("5") + // const depositL2ERC20Amount = utils.parseEther("510") + // const addERC20Amount = utils.parseEther("500") + + // // Add ETH + // const preL2LPEthBalance = await env.L2ETHGateway.balanceOf(L2LiquidityPool.address) + + // await env.waitForXDomainTransaction( + // env.L1ETHGateway.deposit({ value: depositL2oWETHAmount }), + // Direction.L1ToL2 + // ) + + // const approveETHTX = await env.L2ETHGateway.approve( + // L2LiquidityPool.address, + // addoWETHAmount, + // ); + // await approveETHTX.wait() + + // const depositETHTX = await L2LiquidityPool.ownerAddERC20Liquidity( + // addoWETHAmount, + // env.L2ETHGateway.address, + // ); + // await depositETHTX.wait() + + // const postL2LPEthBalance = await env.L2ETHGateway.balanceOf(L2LiquidityPool.address) + + // expect(postL2LPEthBalance).to.deep.eq( + // preL2LPEthBalance.add(addoWETHAmount) + // ) + // // Add ERC20 + // const preL2LPERC20Balance = await L2DepositedERC20.balanceOf(L2LiquidityPool.address) + + // const approveL1ERC20TX = await L1ERC20.approve( + // L1ERC20Gateway.address, + // depositL2ERC20Amount, + // ) + // await approveL1ERC20TX.wait() + + // await env.waitForXDomainTransaction( + // L1ERC20Gateway.deposit(depositL2ERC20Amount), + // Direction.L1ToL2 + // ) + + // const approveL2ERC20TX = await L2DepositedERC20.approve( + // L2LiquidityPool.address, + // addERC20Amount, + // ) + // await approveL2ERC20TX.wait() + + // const depositERC20TX = await L2LiquidityPool.ownerAddERC20Liquidity( + // addERC20Amount, + // L2DepositedERC20.address, + // ); + // await depositERC20TX.wait() + + // const postL2LPERC20Balance = await L2DepositedERC20.balanceOf(L2LiquidityPool.address) + + // expect(postL2LPERC20Balance).to.deep.eq( + // preL2LPERC20Balance.add(addERC20Amount) + // ) + // }) + + // it('should move ETH from L1 LP to L2', async () => { + + // const swapAmount = utils.parseEther("0.50") + // const preBalances = await getBalances("0x0000000000000000000000000000000000000000") + + // //this triggers the receive + // await env.waitForXDomainTransaction( + // env.alicel1Wallet.sendTransaction({ + // from: env.alicel1Wallet.address, + // to: L1LiquidityPool.address, + // value: swapAmount + // }), + // Direction.L1ToL2 + // ) + + // const postBalance = await getBalances("0x0000000000000000000000000000000000000000") + + // expect(postBalance.aliceL2Balance).to.deep.eq( + // preBalances.aliceL2Balance.add(swapAmount.mul(97).div(100)) + // ) + // expect(postBalance.L1LPFeeBalance).to.deep.eq( + // preBalances.L1LPFeeBalance.add(swapAmount.mul(3).div(100)) + // ) + // }) + + // it('should swap oETH from L2 LP to ETH in L1 user wallet', async () => { + + // //basically, the swap-exit + // const swapAmount = utils.parseEther("0.05") + // const preBalances = await getBalances(env.L2ETHGateway.address) + + // const approveTX = await env.L2ETHGateway.approve( + // L2LiquidityPool.address, + // swapAmount + // ) + // await approveTX.wait() + + // await env.waitForXDomainTransaction( + // L2LiquidityPool.clientDepositL2( + // swapAmount, + // env.L2ETHGateway.address + // ), + // Direction.L2ToL1 + // ) + + // const postBalance = await getBalances(env.L2ETHGateway.address) + + // expect(postBalance.bobL1Balance).to.deep.eq( + // preBalances.bobL1Balance.add(swapAmount.mul(97).div(100)) + // ) + // expect(postBalance.L2LPFeeBalance).to.deep.eq( + // preBalances.L2LPFeeBalance.add(swapAmount.mul(3).div(100)) + // ) + // }) + + // it('should mint a new ERC721 and transfer it from Bob to Alice', async () => { + + // const name = 'Non Fungible Token' + // const symbol = 'NFT' + // const firstTokenId = BigNumber.from('5042') + + // const baseURI = 'https://api.com/v1/' + // const sampleUri = 'mock://mytoken' + + // const owner = env.bobl2Wallet.address; + // const recipient = env.alicel2Wallet.address; + + // const nft = await L2ERC721.mintNFT( + // recipient, + // 'https://www.atcc.org/products/all/CCL-2.aspx' + // ) + + // await nft.wait() + // console.log("ERC721:",nft) + + // //it('returns the amount of tokens owned by the given address', async function () { + // expect(await L2ERC721.balanceOf(owner)).to.be.bignumber.equal('1'); + // //}); + + // //it('returns the owner of the given token ID', async function () { + // expect(await L2ERC721.ownerOf(nft)).to.be.equal(recipient); + // //}); + + // }) +}) \ No newline at end of file diff --git a/test/b_nft.spec.ts b/test/b_nft.spec.ts new file mode 100644 index 000000000000..3263c5ec5aa4 --- /dev/null +++ b/test/b_nft.spec.ts @@ -0,0 +1,128 @@ +import { expect } from 'chai' +import { Contract, ContractFactory, BigNumber, Wallet, utils, providers } from 'ethers' +import { Direction } from './shared/watcher-utils' +import L2ERC721Json from '../artifacts-ovm/contracts/ERC721Mock.sol/ERC721Mock.json' +import { OptimismEnv } from './shared/env' + +import * as fs from 'fs' + +describe('NFT Test', async () => { + + let Factory__L2ERC721: ContractFactory + let L2ERC721: Contract + + let env: OptimismEnv + + //Test Marc's BioBase NFT system + const nftName = 'BioBase' + const nftSymbol = 'BEE' //BioEconomy Explodes + + const getBalances = async ( + _address: string, + _env=env + ) => { + + const aliceL1Balance = await _env.alicel1Wallet.getBalance() + const aliceL2Balance = await _env.alicel2Wallet.getBalance() + + const bobL1Balance = await _env.bobl1Wallet.getBalance() + const bobL2Balance = await _env.bobl2Wallet.getBalance() + + console.log("\nbobL1Balance:", bobL1Balance.toString()) + console.log("bobL2Balance:", bobL2Balance.toString()) + console.log("aliceL1Balance:", aliceL1Balance.toString()) + console.log("aliceL2Balance:", aliceL2Balance.toString()) + + return { + aliceL1Balance, + aliceL2Balance, + bobL1Balance, + bobL2Balance, + } + } + + /************* BOB owns all the pools, and ALICE Mints a new token ***********/ + before(async () => { + + env = await OptimismEnv.new() + + Factory__L2ERC721 = new ContractFactory( + L2ERC721Json.abi, + L2ERC721Json.bytecode, + env.bobl2Wallet + ) + + }) + + before(async () => { + + // Mint a new NFT on L2 + // [nftSymbol, nftName] + // this is owned by bobl1Wallet + L2ERC721 = await Factory__L2ERC721.deploy( + nftSymbol, + nftName + ) + await L2ERC721.deployTransaction.wait() + console.log("Marc's BioBase NFT L2ERC721 deployed to:", L2ERC721) + console.log("Marc's BioBase NFT L2ERC721 deployed to:", L2ERC721.address) + + }) + + before(async () => { + + fs.readFile('./deployment/local/addresses.json', 'utf8' , (err, data) => { + + if (err) { + console.error(err) + return + } + + const addressArray = JSON.parse(data); + + //this will either update or overwrite, depending, but either is fine + addressArray['L2ERC721'] = L2ERC721.address; + + fs.writeFile('./deployment/local/addresses.json', JSON.stringify(addressArray, null, 2), err => { + if (err) { + console.log('Error adding NFT address to file:', err) + } else { + console.log('Successfully added NFT address to file') + } + }) + }) + + }) + + it('should mint a new ERC721 and transfer it from Bob to Alice', async () => { + + const owner = env.bobl2Wallet.address; + const recipient = env.alicel2Wallet.address; + + let nft = await L2ERC721.mintNFT( + recipient, + BigNumber.from(42), + 'https://www.atcc.org/products/all/CCL-2.aspx' + ) + await nft.wait() + console.log("ERC721:",nft) + + const balanceOwner = await L2ERC721.balanceOf(owner); + console.log("balanceOwner:",balanceOwner.toString()) + + const balanceRecipient = await L2ERC721.balanceOf(recipient); + console.log("balanceRecipient:",balanceRecipient.toString()) + + let nftURL = await L2ERC721.getTokenURI(BigNumber.from(42)); + console.log("nftURL:",nftURL) + + //it('returns the amount of tokens owned by the given address', async function () { + expect(await L2ERC721.balanceOf(owner)).to.deep.eq('0'); + //}); + + //it('returns the owner of the given token ID', async function () { + //expect(await L2ERC721.ownerOf(nft)).to.deep.eq(recipient); + //}); + + }) +}) \ No newline at end of file diff --git a/test/nft.spec.ts b/test/nft.spec.ts deleted file mode 100644 index 5143ad448fbc..000000000000 --- a/test/nft.spec.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { expect } from 'chai' - -import { Contract, ContractFactory, BigNumber, Wallet, utils, providers } from 'ethers' -import { Direction } from './shared/watcher-utils' - -import L1ERC20Json from '../artifacts/contracts/ERC20.sol/ERC20.json' -import L1ERC20GatewayJson from '../artifacts/contracts/L1ERC20Gateway.sol/L1ERC20Gateway.json' -import L2DepositedERC20Json from '../artifacts-ovm/contracts/L2DepositedERC20.sol/L2DepositedERC20.json' -import L2ERC721Json from '../artifacts-ovm/contracts/NFT/ERC721Mock.sol/ERC721Mock.json' - -import { OptimismEnv } from './shared/env' - -import * as fs from 'fs' - -describe('NFT Test', async () => { - - let Factory__L1ERC20: ContractFactory - let Factory__L2DepositedERC20: ContractFactory - let Factory__L1ERC20Gateway: ContractFactory - let Factory__L2ERC721: ContractFactory - - let L1ERC20: Contract - let L2DepositedERC20: Contract - let L1ERC20Gateway: Contract - let L2ERC721: Contract - - let env: OptimismEnv - - //Test ERC20 - const initialAmount = utils.parseEther("10000000000") - const tokenName = 'JLKN Test' - const tokenDecimals = 18 - const tokenSymbol = 'JLKN' - - //Test Marc's BioBase NFT system - const nftName = 'BioBase' - const nftSymbol = 'BEE' //BioEconomy Explodes - - const getBalances = async ( - _address: string, - _env=env - ) => { - - const aliceL1Balance = await _env.alicel1Wallet.getBalance() - const aliceL2Balance = await _env.alicel2Wallet.getBalance() - - const bobL1Balance = await _env.bobl1Wallet.getBalance() - const bobL2Balance = await _env.bobl2Wallet.getBalance() - - console.log("\nbobL1Balance:", bobL1Balance.toString()) - console.log("bobL2Balance:", bobL2Balance.toString()) - console.log("aliceL1Balance:", aliceL1Balance.toString()) - console.log("aliceL2Balance:", aliceL2Balance.toString()) - - return { - aliceL1Balance, - aliceL2Balance, - bobL1Balance, - bobL2Balance, - } - } - - /************* BOB owns all the pools, and ALICE Mints a new token ***********/ - before(async () => { - - env = await OptimismEnv.new() - - Factory__L1ERC20 = new ContractFactory( - L1ERC20Json.abi, - L1ERC20Json.bytecode, - env.bobl1Wallet - ) - - Factory__L2ERC721 = new ContractFactory( - L2ERC721Json.abi, - L2ERC721Json.bytecode, - env.bobl2Wallet - ) - - Factory__L2DepositedERC20 = new ContractFactory( - L2DepositedERC20Json.abi, - L2DepositedERC20Json.bytecode, - env.bobl2Wallet - ) - - Factory__L1ERC20Gateway = new ContractFactory( - L1ERC20GatewayJson.abi, - L1ERC20GatewayJson.bytecode, - env.bobl1Wallet - ) - - }) - - before(async () => { - - //Who? mints a new token and sets up the L1 and L2 infrastructure - // Mint a new token on L1 - // [initialSupply, name, decimals, symbol] - // this is owned by bobl1Wallet - L1ERC20 = await Factory__L1ERC20.deploy( - initialAmount, - tokenName, - tokenDecimals, - tokenSymbol - ) - await L1ERC20.deployTransaction.wait() - console.log("L1ERC20 deployed to:", L1ERC20.address) - - // Who? sets up things on L2 for this new token - // [l2MessengerAddress, name, symbol] - L2DepositedERC20 = await Factory__L2DepositedERC20.deploy( - env.watcher.l2.messengerAddress, - tokenName, - tokenSymbol - ) - await L2DepositedERC20.deployTransaction.wait() - console.log("L2DepositedERC20 deployed to:", L2DepositedERC20.address) - - // Who? deploys a gateway for this new token - // [L1_ERC20.address, OVM_L2DepositedERC20.address, l1MessengerAddress] - L1ERC20Gateway = await Factory__L1ERC20Gateway.deploy( - L1ERC20.address, - L2DepositedERC20.address, - env.watcher.l1.messengerAddress, - ) - await L1ERC20Gateway.deployTransaction.wait() - console.log("L1ERC20Gateway deployed to:", L1ERC20Gateway.address) - - // Who initializes the contracts for the new token - const initL2 = await L2DepositedERC20.init(L1ERC20Gateway.address); - await initL2.wait(); - console.log('L2 ERC20 initialized:',initL2.hash); - - // Mint a new NFT on L2 - // [nftSymbol, nftName] - // this is owned by bobl1Wallet - L2ERC721 = await Factory__L2ERC721.deploy( - nftSymbol, - nftName - ) - await L2ERC721.deployTransaction.wait() - console.log("Marc's BioBase NFT L2ERC721 deployed to:", L2ERC721) - console.log("Marc's BioBase NFT L2ERC721 deployed to:", L2ERC721.address) - - }) - - before(async () => { - //keep track of where things are for future use by the front end - console.log("\n\n********************************\nSaving all key addresses") - - const addresses = { - L1ERC20: L1ERC20.address, - L2DepositedERC20: L2DepositedERC20.address, - L1ERC20Gateway: L1ERC20Gateway.address, - l1ETHGatewayAddress: env.L1ETHGateway.address, - l1MessengerAddress: env.l1MessengerAddress, - L2ERC721: L2ERC721.address - } - - console.log(JSON.stringify(addresses, null, 2)) - - fs.writeFile('./deployment/local/addresses.json', JSON.stringify(addresses, null, 2), err => { - if (err) { - console.log('Error writing addresses to file:', err) - } else { - console.log('Successfully wrote addresses to file') - } - }) - - console.log('********************************\n\n') - - }) - - it('should transfer ERC20 from Bob to Alice', async () => { - - const depositAmount = utils.parseEther("50") - const preBalances = await getBalances("0x0000000000000000000000000000000000000000") - - console.log("\n Depositing...") - - const { tx, receipt } = await env.waitForXDomainTransaction( - env.L1ETHGateway.deposit({ value: depositAmount }), - Direction.L1ToL2 - ) - - const l1FeePaid = receipt.gasUsed.mul(tx.gasPrice) - const postBalances = await getBalances("0x0000000000000000000000000000000000000000") - - expect(postBalances.bobL2Balance).to.deep.eq( - preBalances.bobL2Balance.add(depositAmount) - ) - expect(postBalances.bobL1Balance).to.deep.eq( - preBalances.bobL1Balance.sub(l1FeePaid.add(depositAmount)) - ) - - }) - - it('should mint a new ERC721 and transfer it from Bob to Alice', async () => { - - //const name = 'Non Fungible Token' - //const symbol = 'NFT' - //const firstTokenId = BigNumber.from('5042') - - //const baseURI = 'https://api.com/v1/' - //const sampleUri = 'mock://mytoken' - - const owner = env.bobl2Wallet.address; - const recipient = env.alicel2Wallet.address; - - let nft = await L2ERC721.mintNFT( - recipient, - BigNumber.from(1), - 'https://www.atcc.org/products/all/CCL-2.aspx' - ) - await nft.wait() - console.log("ERC721:",nft) - - nft = await L2ERC721.mintNFT( - recipient, - BigNumber.from(2), - 'https://www.atcc.org/products/all/CCL-2.aspx#characteristics' - ) - await nft.wait() - console.log("ERC7212:",nft) - - nft = await L2ERC721.mintNFT( - recipient, - BigNumber.from(42), - 'https://www.atcc.org/products/all/CCL-2.aspx#characte' - ) - await nft.wait() - console.log("ERC7212:",nft) - - const filter = { - address: L2ERC721.address, - topics: [ - // the name of the event, parnetheses containing the data type of each event, no spaces - utils.id("Transfer(address,address,uint256)") - ] - } - - env.l2Provider.on(filter, (res) => { - // do whatever you want here - // I'm pretty sure this returns a promise, so don't forget to resolve it - console.log("res:",res) - }) - - await L2ERC721.on("Transfer", (to, amount, from) => { - console.log("Transfer:",to, amount, from); - }); - - const balanceOwner = await L2ERC721.balanceOf(owner); - console.log("balanceOwner:",balanceOwner.toString()) - - const balanceRecipient = await L2ERC721.balanceOf(recipient); - console.log("balanceRecipient:",balanceRecipient.toString()) - - let nftURL = await L2ERC721.getTokenURI(BigNumber.from(2)); - console.log("nftURL:",nftURL) - nftURL = await L2ERC721.getTokenURI(BigNumber.from(42)); - console.log("nftURL:",nftURL) - - //it('returns the amount of tokens owned by the given address', async function () { - //expect(await L2ERC721.balanceOf(owner)).to.deep.eq('1'); - //}); - - //it('returns the owner of the given token ID', async function () { - //expect(await L2ERC721.ownerOf(nft)).to.deep.eq(recipient); - //}); - - }) -}) \ No newline at end of file diff --git a/wallet/package.json b/wallet/package.json index 87f6f36105f3..28ad7f396b86 100644 --- a/wallet/package.json +++ b/wallet/package.json @@ -18,8 +18,6 @@ "bn.js": "^5.1.3", "buffer": "^6.0.3", "dotenv": "^8.2.0", - "elliptic": "^6.5.4", - "enyafhe": "git+https://github.com/enyalabs/EnyaFHE.git#4d0b48e", "eth-sig-util": "^3.0.1", "ethereumjs-abi": "^0.6.8", "ethereumjs-util": "^7.0.5", From 626bc700d04795294a6f7cc2425bb4c1074a1abc Mon Sep 17 00:00:00 2001 From: Jan Liphardt <jan@enya.ai> Date: Thu, 6 May 2021 14:48:32 -0700 Subject: [PATCH 04/10] reliable now --- deployment/local/addresses.json | 13 ++++++------- test/b_nft.spec.ts | 18 ++++++++++-------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/deployment/local/addresses.json b/deployment/local/addresses.json index ecee8d0e7fd2..3b86d76ca275 100644 --- a/deployment/local/addresses.json +++ b/deployment/local/addresses.json @@ -1,10 +1,9 @@ { - "L1LiquidityPool": "0x9C5E699dF0dBEAb06afa715a4104D6C2F9a19A78", - "L2LiquidityPool": "0x64D8e4ebFB202c459d831F0FdCD49e4e7872D97d", - "L1ERC20": "0x998b1F40925f4F7bB55784EFC9569E236AB3ca23", - "L2DepositedERC20": "0x6271d5bB96218ca26EFFd94b47eb6517f095d168", - "L1ERC20Gateway": "0x2C717D2ACd2FC736dFC2CA9b95ccb2dCFAcC574c", + "L1LiquidityPool": "0x889ED4496E34b51eE0770c87cF53ea45526fc172", + "L2LiquidityPool": "0xC18B2a0ee61B77eE020A746f6568828AE053053E", + "L1ERC20": "0xb3a56259e9d3f85a697BeeEdF5Ff2bA4393BbB63", + "L2DepositedERC20": "0x844fa7848A648b359A1ACc7B77313AdEAAaDdaBb", + "L1ERC20Gateway": "0x361D9fE9A3Ead0F94B2d816b73308aBfE1c5d3f3", "l1ETHGatewayAddress": "0x4F53A01556Dc6f120db9a1b4caE786D5f0b5792C", - "l1MessengerAddress": "0xA6404B184Ad3f6F41b6472f02ba45f25C6A66d49", - "L2ERC721": "0x8c0D00a0C3E7F2F6f34CaaA251C19Cd4EacD43D4" + "l1MessengerAddress": "0xA6404B184Ad3f6F41b6472f02ba45f25C6A66d49" } \ No newline at end of file diff --git a/test/b_nft.spec.ts b/test/b_nft.spec.ts index 3263c5ec5aa4..278485e0760a 100644 --- a/test/b_nft.spec.ts +++ b/test/b_nft.spec.ts @@ -64,7 +64,6 @@ describe('NFT Test', async () => { nftName ) await L2ERC721.deployTransaction.wait() - console.log("Marc's BioBase NFT L2ERC721 deployed to:", L2ERC721) console.log("Marc's BioBase NFT L2ERC721 deployed to:", L2ERC721.address) }) @@ -98,30 +97,33 @@ describe('NFT Test', async () => { const owner = env.bobl2Wallet.address; const recipient = env.alicel2Wallet.address; + const tokenID = BigNumber.from(String(50)); let nft = await L2ERC721.mintNFT( recipient, - BigNumber.from(42), + tokenID, 'https://www.atcc.org/products/all/CCL-2.aspx' ) await nft.wait() - console.log("ERC721:",nft) + //console.log("ERC721:",nft) - const balanceOwner = await L2ERC721.balanceOf(owner); + const balanceOwner = await L2ERC721.balanceOf(owner) console.log("balanceOwner:",balanceOwner.toString()) - const balanceRecipient = await L2ERC721.balanceOf(recipient); + const balanceRecipient = await L2ERC721.balanceOf(recipient) console.log("balanceRecipient:",balanceRecipient.toString()) - let nftURL = await L2ERC721.getTokenURI(BigNumber.from(42)); + let nftURL = await L2ERC721.getTokenURI(tokenID) + //for some strange reason need a string here + //no idea why that matters console.log("nftURL:",nftURL) //it('returns the amount of tokens owned by the given address', async function () { - expect(await L2ERC721.balanceOf(owner)).to.deep.eq('0'); + expect(await L2ERC721.balanceOf(owner)).to.deep.eq(BigNumber.from(0)); //}); //it('returns the owner of the given token ID', async function () { - //expect(await L2ERC721.ownerOf(nft)).to.deep.eq(recipient); + expect(await L2ERC721.ownerOf(tokenID)).to.deep.eq(recipient); //}); }) From ab729f29b7794bd377e51185427f88315164fd23 Mon Sep 17 00:00:00 2001 From: Jan Liphardt <jan@enya.ai> Date: Thu, 6 May 2021 17:07:31 -0700 Subject: [PATCH 05/10] finally all tested --- contracts/ERC721Mock.sol | 20 +- deployment/local/addresses.json | 13 +- optional/setup_test.spec.ts | 508 ------------------ test/a_setup.spec.ts | 486 ++++++++--------- test/b_nft.spec.ts | 48 +- wallet/src/containers/home/Home.js | 11 + .../containers/{pool/Pool.js => nft/Nft.js} | 90 +++- .../Pool.module.scss => nft/Nft.module.scss} | 0 8 files changed, 386 insertions(+), 790 deletions(-) delete mode 100644 optional/setup_test.spec.ts rename wallet/src/containers/{pool/Pool.js => nft/Nft.js} (91%) rename wallet/src/containers/{pool/Pool.module.scss => nft/Nft.module.scss} (100%) diff --git a/contracts/ERC721Mock.sol b/contracts/ERC721Mock.sol index bade43b9fef3..f21c88e61b68 100644 --- a/contracts/ERC721Mock.sol +++ b/contracts/ERC721Mock.sol @@ -8,13 +8,23 @@ import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; */ contract ERC721Mock is ERC721 { - constructor (string memory name, string memory symbol) ERC721(name, symbol) { } + uint256 tID; - function mintNFT(address recipient, uint256 tokenId, string memory tokenURI) public returns (uint256) + constructor (string memory name, string memory symbol, uint256 tID_start) + ERC721(name, symbol) { + tID = tID_start; + } + + function mintNFT(address recipient, string memory tokenURI) public returns (uint256) { - safeMint(recipient, tokenId); - setTokenURI(tokenId, tokenURI); - return tokenId; + safeMint(recipient, tID); + setTokenURI(tID, tokenURI); + tID += 1; + return tID; + } + + function getLastTID() public view returns(uint256) { + return tID; } function baseURI() public view override returns (string memory) { diff --git a/deployment/local/addresses.json b/deployment/local/addresses.json index 3b86d76ca275..d94679b4152b 100644 --- a/deployment/local/addresses.json +++ b/deployment/local/addresses.json @@ -1,9 +1,10 @@ { - "L1LiquidityPool": "0x889ED4496E34b51eE0770c87cF53ea45526fc172", - "L2LiquidityPool": "0xC18B2a0ee61B77eE020A746f6568828AE053053E", - "L1ERC20": "0xb3a56259e9d3f85a697BeeEdF5Ff2bA4393BbB63", - "L2DepositedERC20": "0x844fa7848A648b359A1ACc7B77313AdEAAaDdaBb", - "L1ERC20Gateway": "0x361D9fE9A3Ead0F94B2d816b73308aBfE1c5d3f3", + "L1LiquidityPool": "0xb0737d5fbf7564ac3F30Be485758190614e951e4", + "L2LiquidityPool": "0x462b5B6d8408D9260bf260005C57B32028B6F5cA", + "L1ERC20": "0x045cac94a3F18b50edfFc2151E314E8819b73e87", + "L2DepositedERC20": "0xa09f1ec7d5eE70B782A54c4a532A3CC0440b1F0B", + "L1ERC20Gateway": "0x3547F79c3cd9EcE0fF0e574068f5Dc0534960640", "l1ETHGatewayAddress": "0x4F53A01556Dc6f120db9a1b4caE786D5f0b5792C", - "l1MessengerAddress": "0xA6404B184Ad3f6F41b6472f02ba45f25C6A66d49" + "l1MessengerAddress": "0xA6404B184Ad3f6F41b6472f02ba45f25C6A66d49", + "L2ERC721": "0x9BCf511194cbE902278230CF67c913642D760cd5" } \ No newline at end of file diff --git a/optional/setup_test.spec.ts b/optional/setup_test.spec.ts deleted file mode 100644 index 29493e682b29..000000000000 --- a/optional/setup_test.spec.ts +++ /dev/null @@ -1,508 +0,0 @@ -import { expect } from 'chai' - -import { Contract, ContractFactory, BigNumber, Wallet, utils, providers } from 'ethers' -import { Direction } from './shared/watcher-utils' - -import L1LiquidityPoolJson from '../artifacts/contracts/L1LiquidityPool.sol/L1LiquidityPool.json' -import L1ERC20Json from '../artifacts/contracts/ERC20.sol/ERC20.json' -import L1ERC20GatewayJson from '../artifacts/contracts/L1ERC20Gateway.sol/L1ERC20Gateway.json' - -import L2LiquidityPoolJson from '../artifacts-ovm/contracts/L2LiquidityPool.sol/L2LiquidityPool.json' -import L2DepositedERC20Json from '../artifacts-ovm/contracts/L2DepositedERC20.sol/L2DepositedERC20.json' -import L2ERC721Json from '../artifacts-ovm/contracts/NFT/ERC721Mock.sol/ERC721Mock.json' - -import { OptimismEnv } from './shared/env' - -import * as fs from 'fs' - -describe('System Setup', async () => { - - let Factory__L1LiquidityPool: ContractFactory - let Factory__L2LiquidityPool: ContractFactory - let Factory__L1ERC20: ContractFactory - let Factory__L2DepositedERC20: ContractFactory - let Factory__L1ERC20Gateway: ContractFactory - let Factory__L2ERC721: ContractFactory - - let L1LiquidityPool: Contract - let L2LiquidityPool: Contract - let L1ERC20: Contract - let L2DepositedERC20: Contract - let L1ERC20Gateway: Contract - let L2ERC721: Contract - - let env: OptimismEnv - - //Test ERC20 - const initialAmount = utils.parseEther("10000000000") - const tokenName = 'JLKN Test' - const tokenDecimals = 18 - const tokenSymbol = 'JLKN' - - //Test Marc's BioBase NFT system - const nftName = 'BioBase' - const nftSymbol = 'BEE' //Bioeconomy Explodes - - const getBalances = async ( - _address: string, - _L1LiquidityPool=L1LiquidityPool, - _L2LiquidityPool=L2LiquidityPool, - _env=env - ) => { - - const L1LPFeeBalance = await _L1LiquidityPool.feeBalanceOf(_address) - const L2LPFeeBalance = await _L2LiquidityPool.feeBalanceOf(_address) - - const aliceL1Balance = await _env.alicel1Wallet.getBalance() - const aliceL2Balance = await _env.alicel2Wallet.getBalance() - - const bobL1Balance = await _env.bobl1Wallet.getBalance() - const bobL2Balance = await _env.bobl2Wallet.getBalance() -/* - console.log("\nbobL1Balance:", bobL1Balance.toString()) - console.log("bobL2Balance:", bobL2Balance.toString()) - console.log("aliceL1Balance:", aliceL1Balance.toString()) - console.log("aliceL2Balance:", aliceL2Balance.toString()) - console.log("L1LPBalance:", L1LPBalance.toString()) - console.log("L2LPBalance:", L2LPBalance.toString()) - console.log("L1LPFeeBalance:", L1LPFeeBalance.toString()) - console.log("L2LPFeeBalance:", L2LPFeeBalance.toString()) -*/ - return { - L1LPFeeBalance, - L2LPFeeBalance, - aliceL1Balance, - aliceL2Balance, - bobL1Balance, - bobL2Balance, - } - } - - /************* BOB owns all the pools, and ALICE Mints a new token ***********/ - before(async () => { - - env = await OptimismEnv.new() - - Factory__L1LiquidityPool = new ContractFactory( - L1LiquidityPoolJson.abi, - L1LiquidityPoolJson.bytecode, - env.bobl1Wallet - ) - - Factory__L2LiquidityPool = new ContractFactory( - L2LiquidityPoolJson.abi, - L2LiquidityPoolJson.bytecode, - env.bobl2Wallet - ) - - Factory__L1ERC20 = new ContractFactory( - L1ERC20Json.abi, - L1ERC20Json.bytecode, - env.bobl1Wallet - ) - - Factory__L2ERC721 = new ContractFactory( - L2ERC721Json.abi, - L2ERC721Json.bytecode, - env.bobl1Wallet - ) - - Factory__L2DepositedERC20 = new ContractFactory( - L2DepositedERC20Json.abi, - L2DepositedERC20Json.bytecode, - env.bobl2Wallet - ) - - Factory__L1ERC20Gateway = new ContractFactory( - L1ERC20GatewayJson.abi, - L1ERC20GatewayJson.bytecode, - env.bobl1Wallet - ) - - }) - - before(async () => { - - //Who? sets up the L2LP - L2LiquidityPool = await Factory__L2LiquidityPool.deploy( - env.watcher.l2.messengerAddress, - ) - await L2LiquidityPool.deployTransaction.wait() - console.log("L2LiquidityPool deployed to:", L2LiquidityPool.address) - - L1LiquidityPool = await Factory__L1LiquidityPool.deploy( - L2LiquidityPool.address, - env.watcher.l1.messengerAddress, - env.L2ETHGateway.address, - 3 - ) - await L1LiquidityPool.deployTransaction.wait() - console.log("L1LiquidityPool deployed to:", L1LiquidityPool.address) - - const L2LiquidityPoolTX = await L2LiquidityPool.init(L1LiquidityPool.address, /*this is the fee = */ "3", env.L2ETHGateway.address) - //The '3' here denotes the fee to charge, i.e. fee = 3% - await L2LiquidityPoolTX.wait() - console.log('L2 LP initialized with the L1LiquidityPool.address:',L2LiquidityPoolTX.hash); - - //Who? mints a new token and sets up the L1 and L2 infrastructure - // Mint a new token on L1 - // [initialSupply, name, decimals, symbol] - // this is owned by bobl1Wallet - L1ERC20 = await Factory__L1ERC20.deploy( - initialAmount, - tokenName, - tokenDecimals, - tokenSymbol - ) - await L1ERC20.deployTransaction.wait() - console.log("L1ERC20 deployed to:", L1ERC20.address) - - // Who? sets up things on L2 for this new token - // [l2MessengerAddress, name, symbol] - L2DepositedERC20 = await Factory__L2DepositedERC20.deploy( - env.watcher.l2.messengerAddress, - tokenName, - tokenSymbol - ) - await L2DepositedERC20.deployTransaction.wait() - console.log("L2DepositedERC20 deployed to:", L2DepositedERC20.address) - - // Who? deploys a gateway for this new token - // [L1_ERC20.address, OVM_L2DepositedERC20.address, l1MessengerAddress] - L1ERC20Gateway = await Factory__L1ERC20Gateway.deploy( - L1ERC20.address, - L2DepositedERC20.address, - env.watcher.l1.messengerAddress, - ) - await L1ERC20Gateway.deployTransaction.wait() - console.log("L1ERC20Gateway deployed to:", L1ERC20Gateway.address) - - // Who initializes the contracts for the new token - const initL2 = await L2DepositedERC20.init(L1ERC20Gateway.address); - await initL2.wait(); - console.log('L2 ERC20 initialized:',initL2.hash); - - // register erc20 token addresses on both Liquidity pools - await L1LiquidityPool.registerTokenAddress(L1ERC20.address, L2DepositedERC20.address); - await L2LiquidityPool.registerTokenAddress(L1ERC20.address, L2DepositedERC20.address); - - // Mint a new NFT on L2 - // [nftSymbol, nftName] - // this is owned by bobl1Wallet - L2ERC721 = await Factory__L2ERC721.deploy( - nftSymbol, - nftName - ) - await L2ERC721.deployTransaction.wait() - console.log("Marc's BioBase NFT L2ERC721 deployed to:", L2ERC721.address) - - }) - - before(async () => { - //keep track of where things are for future use by the front end - console.log("\n\n********************************\nSaving all key addresses") - - const addresses = { - L1LiquidityPool: L1LiquidityPool.address, - L2LiquidityPool: L2LiquidityPool.address, - L1ERC20: L1ERC20.address, - L2DepositedERC20: L2DepositedERC20.address, - L1ERC20Gateway: L1ERC20Gateway.address, - l1ETHGatewayAddress: env.L1ETHGateway.address, - l1MessengerAddress: env.l1MessengerAddress, - L2ERC721: L2ERC721.address - } - - console.log(JSON.stringify(addresses, null, 2)) - - fs.writeFile('./deployment/local/addresses.json', JSON.stringify(addresses, null, 2), err => { - if (err) { - console.log('Error writing addresses to file:', err) - } else { - console.log('Successfully wrote addresses to file') - } - }) - - console.log('********************************\n\n') - - }) - - it('should transfer ERC20 from Bob to Alice', async () => { - const transferAmount = utils.parseEther("500") - - //L1ERC20 is owned by Bob - const preERC20Balances = await L1ERC20.balanceOf(env.alicel1Wallet.address); - - const transferERC20TX = await L1ERC20.transfer( - env.alicel1Wallet.address, - transferAmount, - ) - await transferERC20TX.wait() - - const postERC20Balance = await L1ERC20.balanceOf(env.alicel1Wallet.address); - - expect(postERC20Balance).to.deep.eq( - preERC20Balances.add(transferAmount) - ) - }) - - it('should add initial ETH and ERC20 to the L1 Liquidity Pool', async () => { - - // ************************************************** - // Only the contract owner (Bob) can deposit ETH into L1 LP - // ************************************************** - const addETHAmount = utils.parseEther("5") - const addERC20Amount = utils.parseEther("500") - - const l1ProviderLL = providers.getDefaultProvider(process.env.L1_NODE_WEB3_URL) - - // Add ETH - const preETHBalances = await getBalances("0x0000000000000000000000000000000000000000") - const preL1LPETHBalance = await l1ProviderLL.getBalance(L1LiquidityPool.address) - - //const fee = BigNumber.from(189176000000000) - const chainID = await env.bobl1Wallet.getChainId() - const gasPrice = await env.bobl1Wallet.getGasPrice() - - const gasEstimate = await env.bobl1Wallet.estimateGas({ - from: env.bobl1Wallet.address, - to: L1LiquidityPool.address, - value: addETHAmount - }) - - //Bob, the owner of the L1LiquidityPool, sends funds into the L1LP - const depositETHTX = await env.bobl1Wallet.sendTransaction({ - from: env.bobl1Wallet.address, - to: L1LiquidityPool.address, - value: addETHAmount - }) - await depositETHTX.wait() - - const postETHBalances = await getBalances("0x0000000000000000000000000000000000000000") - const postL1LPETHBalance = await l1ProviderLL.getBalance(L1LiquidityPool.address) - - const receipt = await l1ProviderLL.getTransactionReceipt(depositETHTX.hash); - //console.log("transaction receipt:",receipt) - - //add fee calculation - const feeSimple = depositETHTX.gasLimit.mul(depositETHTX.gasPrice) - - console.log('\nFEE DEBUG INFORMATION') - console.log("ChainID:",chainID) - console.log("GasPrice (gWei):",utils.formatUnits(gasPrice, "gwei")) - console.log("Fee actually paid:",utils.formatUnits(preETHBalances.bobL1Balance.sub(addETHAmount).sub(postETHBalances.bobL1Balance), "gwei")) - console.log("Fee gasLimit*gasPrice:",utils.formatUnits(feeSimple, "gwei")) - console.log("GasEstimate (gWei):",utils.formatUnits(gasEstimate, "gwei")) - console.log("GasUsed (gWei):",utils.formatUnits(receipt.gasUsed, "gwei")) - console.log('\n') - - /* - console.log("Fee actually paid:",postETHBalances.bobL1Balance.sub(addAmount).sub(postETHBalances.bobL1Balance).toString()) - console.log("Fee gasLimit*gasPrice:",feeSimple.toString()) - console.log("GasEstimate:",gasEstimate.toString()) - console.log("GasUsed:",gasUsed.toString()) - */ - - /* - IF YOU SEND ZERO AMOUNT, these numbers will be off. For example, - - Fee actually paid: 189176.0 - Fee gasLimit*gasPrice: 202464.0 - GasEstimate (gWei): 0.000025308 - GasUsed (gWei): 0.000023647 - - IF YOU SEND NONZERO AMOUNT - - Fee actually paid: 342776.0 - Fee gasLimit*gasPrice: 342776.0 - GasEstimate (gWei): 0.000042847 - GasUsed (gWei): 0.000042847 - */ - - //Bob should have less money now - this breaks for zero value transfer - expect(postETHBalances.bobL1Balance).to.deep.eq( - preETHBalances.bobL1Balance.sub(addETHAmount).sub(feeSimple) - ) - - //He paid into the L1LP - expect(postL1LPETHBalance).to.deep.eq( - preL1LPETHBalance.add(addETHAmount) - ) - - //Alice did not pay, so no change - expect(postETHBalances.aliceL1Balance).to.deep.eq( - preETHBalances.aliceL1Balance - ) - - // Add ERC20 Token - const preL1LPERC20Balance = await L1ERC20.balanceOf(L1LiquidityPool.address) - - const approveERC20TX = await L1ERC20.approve( - L1LiquidityPool.address, - addERC20Amount, - ) - await approveERC20TX.wait() - - const depositERC20TX = await L1LiquidityPool.ownerAddERC20Liquidity( - addERC20Amount, - L1ERC20.address, - ); - await depositERC20TX.wait(); - - const postL1LPERC20Balance = await L1ERC20.balanceOf(L1LiquidityPool.address) - - expect(postL1LPERC20Balance).to.deep.eq( - preL1LPERC20Balance.add(addERC20Amount) - ) - }) - - it('should add initial oETH and ERC20 to the L2 Liquidity Pool', async () => { - - const depositL2oWETHAmount = utils.parseEther("5.1") - const addoWETHAmount = utils.parseEther("5") - const depositL2ERC20Amount = utils.parseEther("510") - const addERC20Amount = utils.parseEther("500") - - // Add ETH - const preL2LPEthBalance = await env.L2ETHGateway.balanceOf(L2LiquidityPool.address) - - await env.waitForXDomainTransaction( - env.L1ETHGateway.deposit({ value: depositL2oWETHAmount }), - Direction.L1ToL2 - ) - - const approveETHTX = await env.L2ETHGateway.approve( - L2LiquidityPool.address, - addoWETHAmount, - ); - await approveETHTX.wait() - - const depositETHTX = await L2LiquidityPool.ownerAddERC20Liquidity( - addoWETHAmount, - env.L2ETHGateway.address, - ); - await depositETHTX.wait() - - const postL2LPEthBalance = await env.L2ETHGateway.balanceOf(L2LiquidityPool.address) - - expect(postL2LPEthBalance).to.deep.eq( - preL2LPEthBalance.add(addoWETHAmount) - ) - // Add ERC20 - const preL2LPERC20Balance = await L2DepositedERC20.balanceOf(L2LiquidityPool.address) - - const approveL1ERC20TX = await L1ERC20.approve( - L1ERC20Gateway.address, - depositL2ERC20Amount, - ) - await approveL1ERC20TX.wait() - - await env.waitForXDomainTransaction( - L1ERC20Gateway.deposit(depositL2ERC20Amount), - Direction.L1ToL2 - ) - - const approveL2ERC20TX = await L2DepositedERC20.approve( - L2LiquidityPool.address, - addERC20Amount, - ) - await approveL2ERC20TX.wait() - - const depositERC20TX = await L2LiquidityPool.ownerAddERC20Liquidity( - addERC20Amount, - L2DepositedERC20.address, - ); - await depositERC20TX.wait() - - const postL2LPERC20Balance = await L2DepositedERC20.balanceOf(L2LiquidityPool.address) - - expect(postL2LPERC20Balance).to.deep.eq( - preL2LPERC20Balance.add(addERC20Amount) - ) - }) - - it('should move ETH from L1 LP to L2', async () => { - - const swapAmount = utils.parseEther("0.50") - const preBalances = await getBalances("0x0000000000000000000000000000000000000000") - - //this triggers the receive - await env.waitForXDomainTransaction( - env.alicel1Wallet.sendTransaction({ - from: env.alicel1Wallet.address, - to: L1LiquidityPool.address, - value: swapAmount - }), - Direction.L1ToL2 - ) - - const postBalance = await getBalances("0x0000000000000000000000000000000000000000") - - expect(postBalance.aliceL2Balance).to.deep.eq( - preBalances.aliceL2Balance.add(swapAmount.mul(97).div(100)) - ) - expect(postBalance.L1LPFeeBalance).to.deep.eq( - preBalances.L1LPFeeBalance.add(swapAmount.mul(3).div(100)) - ) - }) - - it('should swap oETH from L2 LP to ETH in L1 user wallet', async () => { - - //basically, the swap-exit - const swapAmount = utils.parseEther("0.05") - const preBalances = await getBalances(env.L2ETHGateway.address) - - const approveTX = await env.L2ETHGateway.approve( - L2LiquidityPool.address, - swapAmount - ) - await approveTX.wait() - - await env.waitForXDomainTransaction( - L2LiquidityPool.clientDepositL2( - swapAmount, - env.L2ETHGateway.address - ), - Direction.L2ToL1 - ) - - const postBalance = await getBalances(env.L2ETHGateway.address) - - expect(postBalance.bobL1Balance).to.deep.eq( - preBalances.bobL1Balance.add(swapAmount.mul(97).div(100)) - ) - expect(postBalance.L2LPFeeBalance).to.deep.eq( - preBalances.L2LPFeeBalance.add(swapAmount.mul(3).div(100)) - ) - }) - - it('should mint a new ERC721 and transfer it from Bob to Alice', async () => { - - const name = 'Non Fungible Token' - const symbol = 'NFT' - const firstTokenId = BigNumber.from('5042') - - const baseURI = 'https://api.com/v1/' - const sampleUri = 'mock://mytoken' - - const owner = env.bobl2Wallet.address; - const recipient = env.alicel2Wallet.address; - - const nft = await L2ERC721.mintNFT( - recipient, - 'https://www.atcc.org/products/all/CCL-2.aspx' - ) - - await nft.wait() - console.log("ERC721:",nft) - - //it('returns the amount of tokens owned by the given address', async function () { - expect(await L2ERC721.balanceOf(owner)).to.be.bignumber.equal('1'); - //}); - - //it('returns the owner of the given token ID', async function () { - expect(await L2ERC721.ownerOf(nft)).to.be.equal(recipient); - //}); - - }) -}) \ No newline at end of file diff --git a/test/a_setup.spec.ts b/test/a_setup.spec.ts index 7ca24ba8dbee..25715a041d11 100644 --- a/test/a_setup.spec.ts +++ b/test/a_setup.spec.ts @@ -36,6 +36,41 @@ describe('System setup', async () => { const tokenDecimals = 18 const tokenSymbol = 'OMGX' + const getBalances = async ( + _address: string, + _L1LiquidityPool=L1LiquidityPool, + _L2LiquidityPool=L2LiquidityPool, + _env=env + ) => { + + const L1LPFeeBalance = await _L1LiquidityPool.feeBalanceOf(_address) + const L2LPFeeBalance = await _L2LiquidityPool.feeBalanceOf(_address) + + const aliceL1Balance = await _env.alicel1Wallet.getBalance() + const aliceL2Balance = await _env.alicel2Wallet.getBalance() + + const bobL1Balance = await _env.bobl1Wallet.getBalance() + const bobL2Balance = await _env.bobl2Wallet.getBalance() +/* + console.log("\nbobL1Balance:", bobL1Balance.toString()) + console.log("bobL2Balance:", bobL2Balance.toString()) + console.log("aliceL1Balance:", aliceL1Balance.toString()) + console.log("aliceL2Balance:", aliceL2Balance.toString()) + console.log("L1LPBalance:", L1LPBalance.toString()) + console.log("L2LPBalance:", L2LPBalance.toString()) + console.log("L1LPFeeBalance:", L1LPFeeBalance.toString()) + console.log("L2LPFeeBalance:", L2LPFeeBalance.toString()) +*/ + return { + L1LPFeeBalance, + L2LPFeeBalance, + aliceL1Balance, + aliceL2Balance, + bobL1Balance, + bobL2Balance, + } + } + /************* BOB owns all the pools, and ALICE mints a new token ***********/ before(async () => { @@ -186,263 +221,234 @@ describe('System setup', async () => { ) }) - // it('should add initial ETH and ERC20 to the L1 Liquidity Pool', async () => { + it('should add initial ETH and ERC20 to the L1 Liquidity Pool', async () => { - // // ************************************************** - // // Only the contract owner (Bob) can deposit ETH into L1 LP - // // ************************************************** - // const addETHAmount = utils.parseEther("5") - // const addERC20Amount = utils.parseEther("500") + // ************************************************** + // Only the contract owner (Bob) can deposit ETH into L1 LP + // ************************************************** + const addETHAmount = utils.parseEther("5") + const addERC20Amount = utils.parseEther("500") - // const l1ProviderLL = providers.getDefaultProvider(process.env.L1_NODE_WEB3_URL) + const l1ProviderLL = providers.getDefaultProvider(process.env.L1_NODE_WEB3_URL) - // // Add ETH - // const preETHBalances = await getBalances("0x0000000000000000000000000000000000000000") - // const preL1LPETHBalance = await l1ProviderLL.getBalance(L1LiquidityPool.address) - - // //const fee = BigNumber.from(189176000000000) - // const chainID = await env.bobl1Wallet.getChainId() - // const gasPrice = await env.bobl1Wallet.getGasPrice() - - // const gasEstimate = await env.bobl1Wallet.estimateGas({ - // from: env.bobl1Wallet.address, - // to: L1LiquidityPool.address, - // value: addETHAmount - // }) + // Add ETH + const preETHBalances = await getBalances("0x0000000000000000000000000000000000000000") + const preL1LPETHBalance = await l1ProviderLL.getBalance(L1LiquidityPool.address) + + //const fee = BigNumber.from(189176000000000) + const chainID = await env.bobl1Wallet.getChainId() + const gasPrice = await env.bobl1Wallet.getGasPrice() + + const gasEstimate = await env.bobl1Wallet.estimateGas({ + from: env.bobl1Wallet.address, + to: L1LiquidityPool.address, + value: addETHAmount + }) - // //Bob, the owner of the L1LiquidityPool, sends funds into the L1LP - // const depositETHTX = await env.bobl1Wallet.sendTransaction({ - // from: env.bobl1Wallet.address, - // to: L1LiquidityPool.address, - // value: addETHAmount - // }) - // await depositETHTX.wait() - - // const postETHBalances = await getBalances("0x0000000000000000000000000000000000000000") - // const postL1LPETHBalance = await l1ProviderLL.getBalance(L1LiquidityPool.address) - - // const receipt = await l1ProviderLL.getTransactionReceipt(depositETHTX.hash); - // //console.log("transaction receipt:",receipt) - - // //add fee calculation - // const feeSimple = depositETHTX.gasLimit.mul(depositETHTX.gasPrice) + //Bob, the owner of the L1LiquidityPool, sends funds into the L1LP + const depositETHTX = await env.bobl1Wallet.sendTransaction({ + from: env.bobl1Wallet.address, + to: L1LiquidityPool.address, + value: addETHAmount + }) + await depositETHTX.wait() + + const postETHBalances = await getBalances("0x0000000000000000000000000000000000000000") + const postL1LPETHBalance = await l1ProviderLL.getBalance(L1LiquidityPool.address) + + const receipt = await l1ProviderLL.getTransactionReceipt(depositETHTX.hash); + //console.log("transaction receipt:",receipt) + + //add fee calculation + const feeSimple = depositETHTX.gasLimit.mul(depositETHTX.gasPrice) - // console.log('\nFEE DEBUG INFORMATION') - // console.log("ChainID:",chainID) - // console.log("GasPrice (gWei):",utils.formatUnits(gasPrice, "gwei")) - // console.log("Fee actually paid:",utils.formatUnits(preETHBalances.bobL1Balance.sub(addETHAmount).sub(postETHBalances.bobL1Balance), "gwei")) - // console.log("Fee gasLimit*gasPrice:",utils.formatUnits(feeSimple, "gwei")) - // console.log("GasEstimate (gWei):",utils.formatUnits(gasEstimate, "gwei")) - // console.log("GasUsed (gWei):",utils.formatUnits(receipt.gasUsed, "gwei")) - // console.log('\n') + console.log('\nFEE DEBUG INFORMATION') + console.log("ChainID:",chainID) + console.log("GasPrice (gWei):",utils.formatUnits(gasPrice, "gwei")) + console.log("Fee actually paid:",utils.formatUnits(preETHBalances.bobL1Balance.sub(addETHAmount).sub(postETHBalances.bobL1Balance), "gwei")) + console.log("Fee gasLimit*gasPrice:",utils.formatUnits(feeSimple, "gwei")) + console.log("GasEstimate (gWei):",utils.formatUnits(gasEstimate, "gwei")) + console.log("GasUsed (gWei):",utils.formatUnits(receipt.gasUsed, "gwei")) + console.log('\n') - // /* - // console.log("Fee actually paid:",postETHBalances.bobL1Balance.sub(addAmount).sub(postETHBalances.bobL1Balance).toString()) - // console.log("Fee gasLimit*gasPrice:",feeSimple.toString()) - // console.log("GasEstimate:",gasEstimate.toString()) - // console.log("GasUsed:",gasUsed.toString()) - // */ + /* + console.log("Fee actually paid:",postETHBalances.bobL1Balance.sub(addAmount).sub(postETHBalances.bobL1Balance).toString()) + console.log("Fee gasLimit*gasPrice:",feeSimple.toString()) + console.log("GasEstimate:",gasEstimate.toString()) + console.log("GasUsed:",gasUsed.toString()) + */ - // /* - // IF YOU SEND ZERO AMOUNT, these numbers will be off. For example, + /* + IF YOU SEND ZERO AMOUNT, these numbers will be off. For example, - // Fee actually paid: 189176.0 - // Fee gasLimit*gasPrice: 202464.0 - // GasEstimate (gWei): 0.000025308 - // GasUsed (gWei): 0.000023647 - - // IF YOU SEND NONZERO AMOUNT - - // Fee actually paid: 342776.0 - // Fee gasLimit*gasPrice: 342776.0 - // GasEstimate (gWei): 0.000042847 - // GasUsed (gWei): 0.000042847 - // */ - - // //Bob should have less money now - this breaks for zero value transfer - // expect(postETHBalances.bobL1Balance).to.deep.eq( - // preETHBalances.bobL1Balance.sub(addETHAmount).sub(feeSimple) - // ) - - // //He paid into the L1LP - // expect(postL1LPETHBalance).to.deep.eq( - // preL1LPETHBalance.add(addETHAmount) - // ) - - // //Alice did not pay, so no change - // expect(postETHBalances.aliceL1Balance).to.deep.eq( - // preETHBalances.aliceL1Balance - // ) + Fee actually paid: 189176.0 + Fee gasLimit*gasPrice: 202464.0 + GasEstimate (gWei): 0.000025308 + GasUsed (gWei): 0.000023647 + + IF YOU SEND NONZERO AMOUNT + + Fee actually paid: 342776.0 + Fee gasLimit*gasPrice: 342776.0 + GasEstimate (gWei): 0.000042847 + GasUsed (gWei): 0.000042847 + */ + + //Bob should have less money now - this breaks for zero value transfer + expect(postETHBalances.bobL1Balance).to.deep.eq( + preETHBalances.bobL1Balance.sub(addETHAmount).sub(feeSimple) + ) + + //He paid into the L1LP + expect(postL1LPETHBalance).to.deep.eq( + preL1LPETHBalance.add(addETHAmount) + ) + + //Alice did not pay, so no change + expect(postETHBalances.aliceL1Balance).to.deep.eq( + preETHBalances.aliceL1Balance + ) - // // Add ERC20 Token - // const preL1LPERC20Balance = await L1ERC20.balanceOf(L1LiquidityPool.address) - - // const approveERC20TX = await L1ERC20.approve( - // L1LiquidityPool.address, - // addERC20Amount, - // ) - // await approveERC20TX.wait() - - // const depositERC20TX = await L1LiquidityPool.ownerAddERC20Liquidity( - // addERC20Amount, - // L1ERC20.address, - // ); - // await depositERC20TX.wait(); - - // const postL1LPERC20Balance = await L1ERC20.balanceOf(L1LiquidityPool.address) + // Add ERC20 Token + const preL1LPERC20Balance = await L1ERC20.balanceOf(L1LiquidityPool.address) + + const approveERC20TX = await L1ERC20.approve( + L1LiquidityPool.address, + addERC20Amount, + ) + await approveERC20TX.wait() + + const depositERC20TX = await L1LiquidityPool.ownerAddERC20Liquidity( + addERC20Amount, + L1ERC20.address, + ); + await depositERC20TX.wait(); + + const postL1LPERC20Balance = await L1ERC20.balanceOf(L1LiquidityPool.address) - // expect(postL1LPERC20Balance).to.deep.eq( - // preL1LPERC20Balance.add(addERC20Amount) - // ) - // }) + expect(postL1LPERC20Balance).to.deep.eq( + preL1LPERC20Balance.add(addERC20Amount) + ) + }) - // it('should add initial oETH and ERC20 to the L2 Liquidity Pool', async () => { + it('should add initial oETH and ERC20 to the L2 Liquidity Pool', async () => { - // const depositL2oWETHAmount = utils.parseEther("5.1") - // const addoWETHAmount = utils.parseEther("5") - // const depositL2ERC20Amount = utils.parseEther("510") - // const addERC20Amount = utils.parseEther("500") - - // // Add ETH - // const preL2LPEthBalance = await env.L2ETHGateway.balanceOf(L2LiquidityPool.address) - - // await env.waitForXDomainTransaction( - // env.L1ETHGateway.deposit({ value: depositL2oWETHAmount }), - // Direction.L1ToL2 - // ) + const depositL2oWETHAmount = utils.parseEther("5.1") + const addoWETHAmount = utils.parseEther("5") + const depositL2ERC20Amount = utils.parseEther("510") + const addERC20Amount = utils.parseEther("500") + + // Add ETH + const preL2LPEthBalance = await env.L2ETHGateway.balanceOf(L2LiquidityPool.address) + + await env.waitForXDomainTransaction( + env.L1ETHGateway.deposit({ value: depositL2oWETHAmount }), + Direction.L1ToL2 + ) - // const approveETHTX = await env.L2ETHGateway.approve( - // L2LiquidityPool.address, - // addoWETHAmount, - // ); - // await approveETHTX.wait() - - // const depositETHTX = await L2LiquidityPool.ownerAddERC20Liquidity( - // addoWETHAmount, - // env.L2ETHGateway.address, - // ); - // await depositETHTX.wait() - - // const postL2LPEthBalance = await env.L2ETHGateway.balanceOf(L2LiquidityPool.address) - - // expect(postL2LPEthBalance).to.deep.eq( - // preL2LPEthBalance.add(addoWETHAmount) - // ) - // // Add ERC20 - // const preL2LPERC20Balance = await L2DepositedERC20.balanceOf(L2LiquidityPool.address) - - // const approveL1ERC20TX = await L1ERC20.approve( - // L1ERC20Gateway.address, - // depositL2ERC20Amount, - // ) - // await approveL1ERC20TX.wait() - - // await env.waitForXDomainTransaction( - // L1ERC20Gateway.deposit(depositL2ERC20Amount), - // Direction.L1ToL2 - // ) - - // const approveL2ERC20TX = await L2DepositedERC20.approve( - // L2LiquidityPool.address, - // addERC20Amount, - // ) - // await approveL2ERC20TX.wait() - - // const depositERC20TX = await L2LiquidityPool.ownerAddERC20Liquidity( - // addERC20Amount, - // L2DepositedERC20.address, - // ); - // await depositERC20TX.wait() - - // const postL2LPERC20Balance = await L2DepositedERC20.balanceOf(L2LiquidityPool.address) - - // expect(postL2LPERC20Balance).to.deep.eq( - // preL2LPERC20Balance.add(addERC20Amount) - // ) - // }) - - // it('should move ETH from L1 LP to L2', async () => { - - // const swapAmount = utils.parseEther("0.50") - // const preBalances = await getBalances("0x0000000000000000000000000000000000000000") - - // //this triggers the receive - // await env.waitForXDomainTransaction( - // env.alicel1Wallet.sendTransaction({ - // from: env.alicel1Wallet.address, - // to: L1LiquidityPool.address, - // value: swapAmount - // }), - // Direction.L1ToL2 - // ) - - // const postBalance = await getBalances("0x0000000000000000000000000000000000000000") - - // expect(postBalance.aliceL2Balance).to.deep.eq( - // preBalances.aliceL2Balance.add(swapAmount.mul(97).div(100)) - // ) - // expect(postBalance.L1LPFeeBalance).to.deep.eq( - // preBalances.L1LPFeeBalance.add(swapAmount.mul(3).div(100)) - // ) - // }) + const approveETHTX = await env.L2ETHGateway.approve( + L2LiquidityPool.address, + addoWETHAmount, + ); + await approveETHTX.wait() + + const depositETHTX = await L2LiquidityPool.ownerAddERC20Liquidity( + addoWETHAmount, + env.L2ETHGateway.address, + ); + await depositETHTX.wait() + + const postL2LPEthBalance = await env.L2ETHGateway.balanceOf(L2LiquidityPool.address) + + expect(postL2LPEthBalance).to.deep.eq( + preL2LPEthBalance.add(addoWETHAmount) + ) + // Add ERC20 + const preL2LPERC20Balance = await L2DepositedERC20.balanceOf(L2LiquidityPool.address) + + const approveL1ERC20TX = await L1ERC20.approve( + L1ERC20Gateway.address, + depositL2ERC20Amount, + ) + await approveL1ERC20TX.wait() + + await env.waitForXDomainTransaction( + L1ERC20Gateway.deposit(depositL2ERC20Amount), + Direction.L1ToL2 + ) + + const approveL2ERC20TX = await L2DepositedERC20.approve( + L2LiquidityPool.address, + addERC20Amount, + ) + await approveL2ERC20TX.wait() + + const depositERC20TX = await L2LiquidityPool.ownerAddERC20Liquidity( + addERC20Amount, + L2DepositedERC20.address, + ); + await depositERC20TX.wait() + + const postL2LPERC20Balance = await L2DepositedERC20.balanceOf(L2LiquidityPool.address) + + expect(postL2LPERC20Balance).to.deep.eq( + preL2LPERC20Balance.add(addERC20Amount) + ) + }) + + it('should move ETH from L1 LP to L2', async () => { + + const swapAmount = utils.parseEther("0.50") + const preBalances = await getBalances("0x0000000000000000000000000000000000000000") + + //this triggers the receive + await env.waitForXDomainTransaction( + env.alicel1Wallet.sendTransaction({ + from: env.alicel1Wallet.address, + to: L1LiquidityPool.address, + value: swapAmount + }), + Direction.L1ToL2 + ) + + const postBalance = await getBalances("0x0000000000000000000000000000000000000000") + + expect(postBalance.aliceL2Balance).to.deep.eq( + preBalances.aliceL2Balance.add(swapAmount.mul(97).div(100)) + ) + expect(postBalance.L1LPFeeBalance).to.deep.eq( + preBalances.L1LPFeeBalance.add(swapAmount.mul(3).div(100)) + ) + }) - // it('should swap oETH from L2 LP to ETH in L1 user wallet', async () => { - - // //basically, the swap-exit - // const swapAmount = utils.parseEther("0.05") - // const preBalances = await getBalances(env.L2ETHGateway.address) - - // const approveTX = await env.L2ETHGateway.approve( - // L2LiquidityPool.address, - // swapAmount - // ) - // await approveTX.wait() - - // await env.waitForXDomainTransaction( - // L2LiquidityPool.clientDepositL2( - // swapAmount, - // env.L2ETHGateway.address - // ), - // Direction.L2ToL1 - // ) - - // const postBalance = await getBalances(env.L2ETHGateway.address) - - // expect(postBalance.bobL1Balance).to.deep.eq( - // preBalances.bobL1Balance.add(swapAmount.mul(97).div(100)) - // ) - // expect(postBalance.L2LPFeeBalance).to.deep.eq( - // preBalances.L2LPFeeBalance.add(swapAmount.mul(3).div(100)) - // ) - // }) - - // it('should mint a new ERC721 and transfer it from Bob to Alice', async () => { - - // const name = 'Non Fungible Token' - // const symbol = 'NFT' - // const firstTokenId = BigNumber.from('5042') - - // const baseURI = 'https://api.com/v1/' - // const sampleUri = 'mock://mytoken' + it('should swap oETH from L2 LP to ETH in L1 user wallet', async () => { - // const owner = env.bobl2Wallet.address; - // const recipient = env.alicel2Wallet.address; + //basically, the swap-exit + const swapAmount = utils.parseEther("0.05") + const preBalances = await getBalances(env.L2ETHGateway.address) - // const nft = await L2ERC721.mintNFT( - // recipient, - // 'https://www.atcc.org/products/all/CCL-2.aspx' - // ) - - // await nft.wait() - // console.log("ERC721:",nft) + const approveTX = await env.L2ETHGateway.approve( + L2LiquidityPool.address, + swapAmount + ) + await approveTX.wait() + + await env.waitForXDomainTransaction( + L2LiquidityPool.clientDepositL2( + swapAmount, + env.L2ETHGateway.address + ), + Direction.L2ToL1 + ) - // //it('returns the amount of tokens owned by the given address', async function () { - // expect(await L2ERC721.balanceOf(owner)).to.be.bignumber.equal('1'); - // //}); + const postBalance = await getBalances(env.L2ETHGateway.address) - // //it('returns the owner of the given token ID', async function () { - // expect(await L2ERC721.ownerOf(nft)).to.be.equal(recipient); - // //}); + expect(postBalance.bobL1Balance).to.deep.eq( + preBalances.bobL1Balance.add(swapAmount.mul(97).div(100)) + ) + expect(postBalance.L2LPFeeBalance).to.deep.eq( + preBalances.L2LPFeeBalance.add(swapAmount.mul(3).div(100)) + ) + }) - // }) }) \ No newline at end of file diff --git a/test/b_nft.spec.ts b/test/b_nft.spec.ts index 278485e0760a..2bcf0ac68d3c 100644 --- a/test/b_nft.spec.ts +++ b/test/b_nft.spec.ts @@ -1,4 +1,8 @@ -import { expect } from 'chai' +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +chai.use(chaiAsPromised) +const expect = chai.expect; + import { Contract, ContractFactory, BigNumber, Wallet, utils, providers } from 'ethers' import { Direction } from './shared/watcher-utils' import L2ERC721Json from '../artifacts-ovm/contracts/ERC721Mock.sol/ERC721Mock.json' @@ -61,7 +65,8 @@ describe('NFT Test', async () => { // this is owned by bobl1Wallet L2ERC721 = await Factory__L2ERC721.deploy( nftSymbol, - nftName + nftName, + BigNumber.from(String(0)) //starting index for the tokenIDs ) await L2ERC721.deployTransaction.wait() console.log("Marc's BioBase NFT L2ERC721 deployed to:", L2ERC721.address) @@ -97,34 +102,53 @@ describe('NFT Test', async () => { const owner = env.bobl2Wallet.address; const recipient = env.alicel2Wallet.address; + + //for some strange reason need a string here + //no idea why that matters const tokenID = BigNumber.from(String(50)); + //mint one NFT let nft = await L2ERC721.mintNFT( recipient, - tokenID, 'https://www.atcc.org/products/all/CCL-2.aspx' ) await nft.wait() //console.log("ERC721:",nft) const balanceOwner = await L2ERC721.balanceOf(owner) - console.log("balanceOwner:",balanceOwner.toString()) - const balanceRecipient = await L2ERC721.balanceOf(recipient) + + console.log("balanceOwner:",balanceOwner.toString()) console.log("balanceRecipient:",balanceRecipient.toString()) - let nftURL = await L2ERC721.getTokenURI(tokenID) - //for some strange reason need a string here - //no idea why that matters + //Get the URL + let nftURL = await L2ERC721.getTokenURI(BigNumber.from(String(0))) console.log("nftURL:",nftURL) + //Should be 1 + let TID = await L2ERC721.getLastTID() + console.log("TID:",TID.toString()) + + //mint a second NFT + nft = await L2ERC721.mintNFT( + recipient, + 'https://www.atcc.org/products/all/CCL-3.aspx' + ) + await nft.wait() + + //Should be 2 + TID = await L2ERC721.getLastTID() + console.log("TID:",TID.toString()) + //it('returns the amount of tokens owned by the given address', async function () { - expect(await L2ERC721.balanceOf(owner)).to.deep.eq(BigNumber.from(0)); + expect(await L2ERC721.balanceOf(owner)).to.deep.eq(BigNumber.from(String(0))); //}); - //it('returns the owner of the given token ID', async function () { - expect(await L2ERC721.ownerOf(tokenID)).to.deep.eq(recipient); - //}); + // Token 1 should be owned by recipient + expect(await L2ERC721.ownerOf(BigNumber.from(String(1)))).to.deep.eq(recipient); + //And Token 50 should not exist (at this point) + expect(L2ERC721.ownerOf(BigNumber.from(String(50)))).to.be.eventually.rejectedWith("ERC721: owner query for nonexistent token"); }) + }) \ No newline at end of file diff --git a/wallet/src/containers/home/Home.js b/wallet/src/containers/home/Home.js index 4b703e86927d..e63613fc8f78 100644 --- a/wallet/src/containers/home/Home.js +++ b/wallet/src/containers/home/Home.js @@ -44,6 +44,8 @@ import Status from 'containers/status/Status'; import Account from 'containers/account/Account'; import Transactions from 'containers/transactions/Transactions'; +//NFT Example Page +import NFT from 'containers/nft/Nft'; import MobileHeader from 'components/mobileheader/MobileHeader'; import MobileMenu from 'components/mobilemenu/MobileMenu'; @@ -147,6 +149,12 @@ function Home () { > Wallet </h2> + <h2 + className={pageDisplay === "NFT" ? styles.subtitletextActive : styles.subtitletext} + onClick={()=>{handleSetPage("NFT")}} + > + NFT + </h2> </div> {pageDisplay === "AccountNow" && <> @@ -154,6 +162,9 @@ function Home () { <Transactions/> </> } + {pageDisplay === "NFT" && + <NFT/> + } </div> </div> </> diff --git a/wallet/src/containers/pool/Pool.js b/wallet/src/containers/nft/Nft.js similarity index 91% rename from wallet/src/containers/pool/Pool.js rename to wallet/src/containers/nft/Nft.js index b0dc9739650d..3847f185d17c 100644 --- a/wallet/src/containers/pool/Pool.js +++ b/wallet/src/containers/nft/Nft.js @@ -5,17 +5,16 @@ import { isEqual } from 'lodash'; import InputSelect from 'components/inputselect/InputSelect'; import Input from 'components/input/Input'; import Button from 'components/button/Button'; - import { openError, openAlert } from 'actions/uiAction'; - import { logAmount } from 'util/amountConvert'; - import networkService from 'services/networkService'; -import * as styles from './Pool.module.scss'; +import * as styles from './Nft.module.scss'; + +class Nft extends React.Component { -class Pool extends React.Component { constructor(props) { + super(props); const { balance } = this.props; @@ -48,6 +47,10 @@ class Pool extends React.Component { L2FeeWithdrawAmount: '', loading: false, + + receiverAddress: '', + tokenID: 1, + tokenURI: '' } } @@ -72,6 +75,7 @@ class Pool extends React.Component { } componentDidUpdate(prevState) { + const { balance } = this.props; const { initialL1Currency, initialL2Currency, @@ -83,6 +87,7 @@ class Pool extends React.Component { const L1Token = balance.rootchain.filter(i => i.symbol !== 'WETH')[0]; const L2Token = balance.childchain.filter(i => i.symbol !== 'ETH')[0]; + if (!isEqual(prevState.balance, balance)) { this.setState({ balance }); if (!initialL1Currency) this.setState({initialL1Currency : L1Token.currency}); @@ -252,23 +257,54 @@ class Pool extends React.Component { this.setState({ loading: false }) } + async handleMintAndSend() { + + const { receiverAddress, tokenID, tokenURI } = this.state; + + //we are doing this on L2 + + const networkStatus = await this.props.dispatch(networkService.checkNetwork('L2')); + if (!networkStatus) { + this.props.dispatch(openError('Please use L2 network.')); + return; + } + + this.setState({ loading: true }); + + const mintTX = await networkService.mintAndSendNFT( + receiverAddress, + tokenID, + tokenURI + ); + + if (mintTX) { + this.props.dispatch(openAlert(`You minted a new NFT for ${receiverAddress}`)); + } else { + this.props.dispatch(openError('NFT minting error')); + } + + this.setState({ loading: false }) + } + render() { + const { balance, - initialL1Value, initialL1Currency, L1Value, L1Currency, LPL1SearchToken, LPL1Balance, LPL1FeeSearchToken, LPL1FeeBalance, L1FeeReceiverAddress, L1FeeWithdrawAmount, - initialL2Value, initialL2Currency, L2Value, L2Currency, LPL2SearchToken, LPL2Balance, LPL2FeeSearchToken, LPL2FeeBalance, L2FeeReceiverAddress, L2FeeWithdrawAmount, + loading, - loading + receiverAddress, + tokenID, + tokenURI } = this.state; const rootChainBalance = balance.rootchain; @@ -288,17 +324,33 @@ class Pool extends React.Component { return ( <div className={styles.container}> <div className={styles.boxContainer}> - <h2>Layer 1 Liquidity Pool</h2> - <h3>Initial Deposit L1</h3> - <InputSelect - label='Amount to deposit (No fund on L2)' - placeholder={0} - value={initialL1Value} - onChange={i => {this.setState({initialL1Value: i.target.value})}} - selectOptions={selectL1Options} - onSelect={i => {this.setState({initialL1Currency: i.target.value})}} - selectValue={initialL1Currency} + + <h2>Minter/Owner Functions</h2> + + <h3>Mint and Send</h3> + + <Input + placeholder="Receiver Address (e.g. Ox.....)" + onChange={i=>{this.setState({receiverAddress: i.target.value})}} + /> + <Input + placeholder="NFT Unique ID (e.g. 7)" + onChange={i=>{this.setState({tokenID: i.target.value})}} /> + <Input + placeholder="NFT URL (e.g. https://jimb.stanford.edu)" + onChange={i=>{this.setState({tokenURI: i.target.value})}} + /> + <Button + className={styles.button} + onClick={() => {this.handleMintAndSend()}} + type='primary' + loading={loading} + disabled={!receiverAddress || !tokenID || !tokenURI} + > + Mint and Send + </Button> + <Button className={styles.button} onClick={() => {this.handleInitialDepositL1();}} @@ -576,4 +628,4 @@ const mapStateToProps = state => ({ tokenList: state.tokenList }); -export default connect(mapStateToProps)(Pool); \ No newline at end of file +export default connect(mapStateToProps)(Nft); \ No newline at end of file diff --git a/wallet/src/containers/pool/Pool.module.scss b/wallet/src/containers/nft/Nft.module.scss similarity index 100% rename from wallet/src/containers/pool/Pool.module.scss rename to wallet/src/containers/nft/Nft.module.scss From c353f5ae8d6625790099793aca3d925c09289bdc Mon Sep 17 00:00:00 2001 From: Jan Liphardt <jan@enya.ai> Date: Thu, 6 May 2021 20:25:35 -0700 Subject: [PATCH 06/10] wip --- contracts/ERC721Mock.sol | 5 +++++ deployment/local/addresses.json | 12 +++++----- wallet/src/services/networkService.js | 32 +++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/contracts/ERC721Mock.sol b/contracts/ERC721Mock.sol index f21c88e61b68..b20e0854438e 100644 --- a/contracts/ERC721Mock.sol +++ b/contracts/ERC721Mock.sol @@ -23,6 +23,10 @@ contract ERC721Mock is ERC721 { return tID; } + //function balanceOf(address owner) public view override returns (uint256) { + // return _balanceOf(owner); + //} + function getLastTID() public view returns(uint256) { return tID; } @@ -39,6 +43,7 @@ contract ERC721Mock is ERC721 { _setTokenURI(tokenId, _tokenURI); } + //for a specific tokenId, get the associated NFT function getTokenURI(uint256 tokenId) public view returns (string memory) { return tokenURI(tokenId); } diff --git a/deployment/local/addresses.json b/deployment/local/addresses.json index d94679b4152b..b1b969ad4a77 100644 --- a/deployment/local/addresses.json +++ b/deployment/local/addresses.json @@ -1,10 +1,10 @@ { - "L1LiquidityPool": "0xb0737d5fbf7564ac3F30Be485758190614e951e4", - "L2LiquidityPool": "0x462b5B6d8408D9260bf260005C57B32028B6F5cA", - "L1ERC20": "0x045cac94a3F18b50edfFc2151E314E8819b73e87", - "L2DepositedERC20": "0xa09f1ec7d5eE70B782A54c4a532A3CC0440b1F0B", - "L1ERC20Gateway": "0x3547F79c3cd9EcE0fF0e574068f5Dc0534960640", + "L1LiquidityPool": "0x844fa7848A648b359A1ACc7B77313AdEAAaDdaBb", + "L2LiquidityPool": "0x2de5FA6BF0806E406dbd003b3Ac8D701F8E844Bc", + "L1ERC20": "0x51Bfa6F6341dA87455424b41FF525D43408c55C0", + "L2DepositedERC20": "0x286297F13E2A0940699c71c7a9616d6def4c7e27", + "L1ERC20Gateway": "0x8Ce4E35356589273ca38C9e6BBdAa841eaF488D9", "l1ETHGatewayAddress": "0x4F53A01556Dc6f120db9a1b4caE786D5f0b5792C", "l1MessengerAddress": "0xA6404B184Ad3f6F41b6472f02ba45f25C6A66d49", - "L2ERC721": "0x9BCf511194cbE902278230CF67c913642D760cd5" + "L2ERC721": "0x379DB09522c4184a07E3f45cF9A4FF9D14037715" } \ No newline at end of file diff --git a/wallet/src/services/networkService.js b/wallet/src/services/networkService.js index 9a3bae791553..ff1e0accb119 100644 --- a/wallet/src/services/networkService.js +++ b/wallet/src/services/networkService.js @@ -38,6 +38,7 @@ import L2LPJson from '../deployment/artifacts-ovm/contracts/L2LiquidityPool.sol/ import L1ERC20Json from '../deployment/artifacts/contracts/ERC20.sol/ERC20.json' import L2DepositedERC20Json from '../deployment/artifacts-ovm/contracts/L2DepositedERC20.sol/L2DepositedERC20.json' import L1ERC20GatewayJson from '../deployment/artifacts/contracts/L1ERC20Gateway.sol/L1ERC20Gateway.json' +import L2ERC721Json from '../deployment/artifacts-ovm/contracts/ERC721Mock.sol/ERC721Mock.json' import { powAmount, logAmount } from 'util/amountConvert'; import { getAllNetworks } from 'util/networkName'; @@ -81,6 +82,8 @@ class NetworkService { this.ERC20L1Contract = null; this.ERC20L2Contract = null; + this.ERC721Contract = null; + // L1 or L2 this.L1orL2 = null; this.networkName = null; @@ -91,6 +94,7 @@ class NetworkService { // addresses this.ERC20Address = null; + this.ERC721Address = null; this.l1ETHGatewayAddress = null; this.L1ERC20GatewayAddress = null; this.L2DepositedERC20Address = null; @@ -196,6 +200,7 @@ class NetworkService { this.l1MessengerAddress = addresses.l1MessengerAddress; this.L1LPAddress = addresses.L1LiquidityPool; this.L2LPAddress = addresses.L2LiquidityPool; + this.ERC721Address = addresses.L2ERC721; this.L1ETHGatewayContract = new ethers.Contract( this.l1ETHGatewayAddress, @@ -245,6 +250,12 @@ class NetworkService { this.web3Provider.getSigner(), ); + this.ERC721Contract = new ethers.Contract( + this.ERC721Address, + L2ERC721Json.abi, + this.web3Provider.getSigner(), + ); + //Fire up the new watcher //const addressManager = getAddressManager(bobl1Wallet) //const watcher = await initWatcher(l1Provider, this.l2Provider, addressManager) @@ -290,6 +301,14 @@ class NetworkService { const childChainBalance = await this.l2Provider.getBalance(this.account); const ERC20L2Balance = await this.ERC20L2Contract.methods.balanceOf(this.account).call({from: this.account}); + //const ERC721L2Balance = 0; //await this.ERC721Contract.balanceOf(this.account); + + const ERC721L2Balance = await this.ERC721Contract.balanceOf(this.account); + + console.log("ERC721L2Balance",ERC721L2Balance) + console.log("this.account",this.account) + console.log(this.ERC721Contract); + const ethToken = await getToken(OmgUtil.transaction.ETH_CURRENCY); let testToken = null; @@ -299,6 +318,14 @@ class NetworkService { } else { testToken = await getToken(this.L2DepositedERC20Address); } + + const nftInfo = { + currency: this.ERC721Address, + symbol: "BBE (NFT)", + decimals: 0, + name: "BioEcon", + redalert: false + } const rootchainEthBalance = [ { @@ -324,6 +351,11 @@ class NetworkService { currency: this.L2DepositedERC20Address, amount: new BN(ERC20L2Balance.toString()), }, + { + ...nftInfo, + currency: this.ERC721Address, + amount: new BN(ERC721L2Balance.toString()), + }, ] return { From ad8da029e90ce523bcf1f121dbad280e9795759f Mon Sep 17 00:00:00 2001 From: Jan Liphardt <jan@enya.ai> Date: Fri, 7 May 2021 17:37:22 -0700 Subject: [PATCH 07/10] enumeration working --- contracts/ERC721Mock.sol | 4 -- deployment/local/addresses.json | 12 ++-- test/b_nft.spec.ts | 16 ++--- wallet/src/actions/nftAction.js | 91 +++++++++++++++++++++++++++ wallet/src/reducers/index.js | 2 + wallet/src/reducers/nftReducer.js | 46 ++++++++++++++ wallet/src/services/networkService.js | 57 +++++++++++++++-- 7 files changed, 205 insertions(+), 23 deletions(-) create mode 100644 wallet/src/actions/nftAction.js create mode 100644 wallet/src/reducers/nftReducer.js diff --git a/contracts/ERC721Mock.sol b/contracts/ERC721Mock.sol index b20e0854438e..d60a59f610ba 100644 --- a/contracts/ERC721Mock.sol +++ b/contracts/ERC721Mock.sol @@ -23,10 +23,6 @@ contract ERC721Mock is ERC721 { return tID; } - //function balanceOf(address owner) public view override returns (uint256) { - // return _balanceOf(owner); - //} - function getLastTID() public view returns(uint256) { return tID; } diff --git a/deployment/local/addresses.json b/deployment/local/addresses.json index b1b969ad4a77..e3567b7050cd 100644 --- a/deployment/local/addresses.json +++ b/deployment/local/addresses.json @@ -1,10 +1,10 @@ { - "L1LiquidityPool": "0x844fa7848A648b359A1ACc7B77313AdEAAaDdaBb", - "L2LiquidityPool": "0x2de5FA6BF0806E406dbd003b3Ac8D701F8E844Bc", - "L1ERC20": "0x51Bfa6F6341dA87455424b41FF525D43408c55C0", - "L2DepositedERC20": "0x286297F13E2A0940699c71c7a9616d6def4c7e27", - "L1ERC20Gateway": "0x8Ce4E35356589273ca38C9e6BBdAa841eaF488D9", + "L1LiquidityPool": "0x196A1b2750D9d39ECcC7667455DdF1b8d5D65696", + "L2LiquidityPool": "0x3e4CFaa8730092552d9425575E49bB542e329981", + "L1ERC20": "0x60ba97F7cE0CD19Df71bAd0C3BE9400541f6548E", + "L2DepositedERC20": "0x65F72DF8a668BC6272B059BB7F53ADc91066540C", + "L1ERC20Gateway": "0xf952b82ED61797Cd0b93F02e42aC6B85EB4dF127", "l1ETHGatewayAddress": "0x4F53A01556Dc6f120db9a1b4caE786D5f0b5792C", "l1MessengerAddress": "0xA6404B184Ad3f6F41b6472f02ba45f25C6A66d49", - "L2ERC721": "0x379DB09522c4184a07E3f45cF9A4FF9D14037715" + "L2ERC721": "0x16Af4Db6548234c6463Ad6F0cf355260E96E741b" } \ No newline at end of file diff --git a/test/b_nft.spec.ts b/test/b_nft.spec.ts index 2bcf0ac68d3c..2816b152b81a 100644 --- a/test/b_nft.spec.ts +++ b/test/b_nft.spec.ts @@ -103,15 +103,17 @@ describe('NFT Test', async () => { const owner = env.bobl2Wallet.address; const recipient = env.alicel2Wallet.address; + const ownerName = "Henrietta Lacks" + //for some strange reason need a string here //no idea why that matters const tokenID = BigNumber.from(String(50)); + + let meta = ownerName + "#" + Date.now().toString() + '#https://www.atcc.org/products/all/CCL-2.aspx'; + console.log("meta:",meta) //mint one NFT - let nft = await L2ERC721.mintNFT( - recipient, - 'https://www.atcc.org/products/all/CCL-2.aspx' - ) + let nft = await L2ERC721.mintNFT(recipient,meta) await nft.wait() //console.log("ERC721:",nft) @@ -130,10 +132,8 @@ describe('NFT Test', async () => { console.log("TID:",TID.toString()) //mint a second NFT - nft = await L2ERC721.mintNFT( - recipient, - 'https://www.atcc.org/products/all/CCL-3.aspx' - ) + meta = ownerName + "#" + Date.now().toString() + '#https://www.atcc.org/products/all/CCL-185.aspx'; + nft = await L2ERC721.mintNFT(recipient,meta) await nft.wait() //Should be 2 diff --git a/wallet/src/actions/nftAction.js b/wallet/src/actions/nftAction.js new file mode 100644 index 000000000000..2c2f59b5ec5a --- /dev/null +++ b/wallet/src/actions/nftAction.js @@ -0,0 +1,91 @@ +/* +Copyright 2019-present OmiseGO Pte Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +import erc20abi from 'human-standard-token-abi'; +import networkService from 'services/networkService'; +import store from 'store'; + +/* +import { getNFTs, addNFT } from 'actions/nftAction'; +*/ + + +/* +Returns Token info +If we don't have the info, try to get it +*/ +/* +export async function getToken ( tokenContractAddress ) { + + //this *might* be coming from a person, and or copy-paste from Etherscan + //so need toLowerCase() + // ***************************************************************** + const _tokenContractAddress = tokenContractAddress.toLowerCase(); + // ***************************************************************** + + const state = store.getState(); + if (state.tokenList[_tokenContractAddress]) { + //console.log("tokenAction = token in list:",_tokenContractAddress) + return state.tokenList[_tokenContractAddress]; + } else { + console.log("tokenAction = token not yet in list:",_tokenContractAddress) + const tokenInfo = await addToken( _tokenContractAddress ) + return tokenInfo; + } +} +*/ + +export async function getNFTs () { + const state = store.getState() + //console.log("export async function getNFTs") + //console.log(state.nftList) + return state.nftList; +} + +/* +Get the token info from networkService.web3.eth.Contract +*/ +export async function addNFT ( NFTproperties ) { + + const state = store.getState(); + const UUID = NFTproperties.UUID; + + //if we already have looked it up, no need to look up again. + if (state.nftList[UUID]) { + console.log("nftAction - already in list:",UUID) + console.log(state.nftList[UUID]) + return state.nftList[UUID]; + } + + const nftInfo = { + //currency: _tokenContractAddress, + //symbol, + //name, + //redalert: _decimals ? false : true + UUID: NFTproperties.UUID, + owner: NFTproperties.owner, + url: NFTproperties.url, + mintedTime: NFTproperties.mintedTime, + decimals: 0, + }; + + store.dispatch({ + type: 'NFT/GET/SUCCESS', + payload: nftInfo + }) + + return nftInfo; + +} \ No newline at end of file diff --git a/wallet/src/reducers/index.js b/wallet/src/reducers/index.js index 76afa5a3c3c5..73b98fbd84c9 100644 --- a/wallet/src/reducers/index.js +++ b/wallet/src/reducers/index.js @@ -23,6 +23,7 @@ import balanceReducer from './balanceReducer'; import exitReducer from './exitReducer'; import queueReducer from './queueReducer'; import tokenReducer from './tokenReducer'; +import nftReducer from './nftReducer'; import feeReducer from './feeReducer'; import gasReducer from './gasReducer'; import uiReducer from './uiReducer'; @@ -38,6 +39,7 @@ const rootReducer = combineReducers({ exit: exitReducer, queue: queueReducer, tokenList: tokenReducer, + nftList: nftReducer, fees: feeReducer, gas: gasReducer, ui: uiReducer, diff --git a/wallet/src/reducers/nftReducer.js b/wallet/src/reducers/nftReducer.js new file mode 100644 index 000000000000..d71cd7bba18c --- /dev/null +++ b/wallet/src/reducers/nftReducer.js @@ -0,0 +1,46 @@ +/* +Copyright 2019-present OmiseGO Pte Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +const initialState = { + /* + [ETH0x]: { + currency: ETH0x, + decimals: 18, + symbol: 'ETH', + name: 'Ethereum' + }, + [oETH]: { + currency: oETH, + decimals: 18, + symbol: 'oETH', + name: 'Ethereum', + } + */ +}; + +function nftReducer (state = initialState, action) { + switch (action.type) { + case 'NFT/GET/SUCCESS': + console.log(state) + return { + ...state, + [action.payload.UUID]: action.payload, + }; + default: + return state; + } +} + +export default nftReducer; diff --git a/wallet/src/services/networkService.js b/wallet/src/services/networkService.js index ff1e0accb119..93c41d46221e 100644 --- a/wallet/src/services/networkService.js +++ b/wallet/src/services/networkService.js @@ -19,7 +19,7 @@ import { JsonRpcProvider, Web3Provider } from "@ethersproject/providers"; import { hexlify } from "@ethersproject/bytes"; import { parseUnits, parseEther } from "@ethersproject/units"; import { Watcher } from "@eth-optimism/watcher"; -import { ethers } from "ethers"; +import { ethers, BigNumber } from "ethers"; import Web3Modal from "web3modal"; @@ -30,6 +30,7 @@ import BN from 'bn.js'; import Web3 from 'web3'; import { getToken } from 'actions/tokenAction'; +import { getNFTs, addNFT } from 'actions/nftAction'; import { openAlert, openError } from 'actions/uiAction'; import { WebWalletError } from 'services/errorService'; @@ -303,11 +304,57 @@ class NetworkService { //const ERC721L2Balance = 0; //await this.ERC721Contract.balanceOf(this.account); + // //how many NFTs do I own? const ERC721L2Balance = await this.ERC721Contract.balanceOf(this.account); - console.log("ERC721L2Balance",ERC721L2Balance) - console.log("this.account",this.account) - console.log(this.ERC721Contract); + //console.log("ERC721L2Balance",ERC721L2Balance) + //console.log("this.account",this.account) + //console.log(this.ERC721Contract) + + //let see if we already know about them + const myNFTS = await getNFTs() + const numberOfNFTS = Object.keys(myNFTS).length; + console.log(myNFTS) + + //console.log(ERC721L2Balance.toString()) + //console.log(numberOfNFTS.toString()) + + if(ERC721L2Balance.toString() !== numberOfNFTS.toString()) { + //oh - something just changed - either got one, or sent one + //we need to do something! + //get the first one + console.log("NFT change detected!") + + let tokenID = BigNumber.from(0) + let nftTokenIDs = await this.ERC721Contract.tokenOfOwnerByIndex(this.account, tokenID) + let nftMeta = await this.ERC721Contract.getTokenURI(tokenID) + let meta = nftMeta.split("#") + + addNFT({ + UUID: this.ERC721Address.substring(1, 6) + "_" + nftTokenIDs.toString() + "_" + this.account.substring(1, 6), + owner: meta[0], + mintedTime: meta[1], + url: meta[2], + tokenID: tokenID + }) + + //get the second one + tokenID = BigNumber.from(1) + nftTokenIDs = await this.ERC721Contract.tokenOfOwnerByIndex(this.account, tokenID) + nftMeta = await this.ERC721Contract.getTokenURI(tokenID) + meta = nftMeta.split("#") + + addNFT({ + UUID: this.ERC721Address.substring(1, 6) + "_" + nftTokenIDs.toString() + "_" + this.account.substring(1, 6), + owner: meta[0], + mintedTime: meta[1], + url: meta[2], + tokenID: tokenID + }) + } else { + //console.log("No NFT changes") + //all set - do nothing + } const ethToken = await getToken(OmgUtil.transaction.ETH_CURRENCY); let testToken = null; @@ -318,7 +365,7 @@ class NetworkService { } else { testToken = await getToken(this.L2DepositedERC20Address); } - + const nftInfo = { currency: this.ERC721Address, symbol: "BBE (NFT)", From 8ccb1608f41356ffb9cf91f773a0b82c075f28cb Mon Sep 17 00:00:00 2001 From: Jan Liphardt <jan@enya.ai> Date: Sun, 9 May 2021 15:56:05 -0700 Subject: [PATCH 08/10] added tiles --- contracts/ERC721Mock.sol | 8 + deployment/local/addresses.json | 12 +- wallet/src/actions/nftAction.js | 12 +- wallet/src/components/nft/NftCard.js | 58 ++ wallet/src/components/nft/NftCard.module.scss | 42 ++ .../components/walletpicker/WalletPicker.js | 3 - wallet/src/containers/nft/Nft.js | 527 ++---------------- wallet/src/containers/nft/Nft.module.scss | 9 + wallet/src/images/hela.jpg | Bin 0 -> 44662 bytes wallet/src/services/networkService.js | 79 +-- 10 files changed, 211 insertions(+), 539 deletions(-) create mode 100644 wallet/src/components/nft/NftCard.js create mode 100644 wallet/src/components/nft/NftCard.module.scss create mode 100644 wallet/src/images/hela.jpg diff --git a/contracts/ERC721Mock.sol b/contracts/ERC721Mock.sol index d60a59f610ba..a924bf4809cc 100644 --- a/contracts/ERC721Mock.sol +++ b/contracts/ERC721Mock.sol @@ -44,6 +44,14 @@ contract ERC721Mock is ERC721 { return tokenURI(tokenId); } + function getName() public view returns (string memory) { + return name(); + } + + function getSymbol() public view returns (string memory) { + return symbol(); + } + function exists(uint256 tokenId) public view returns (bool) { return _exists(tokenId); } diff --git a/deployment/local/addresses.json b/deployment/local/addresses.json index e3567b7050cd..8d34dd26ca12 100644 --- a/deployment/local/addresses.json +++ b/deployment/local/addresses.json @@ -1,10 +1,10 @@ { - "L1LiquidityPool": "0x196A1b2750D9d39ECcC7667455DdF1b8d5D65696", - "L2LiquidityPool": "0x3e4CFaa8730092552d9425575E49bB542e329981", - "L1ERC20": "0x60ba97F7cE0CD19Df71bAd0C3BE9400541f6548E", - "L2DepositedERC20": "0x65F72DF8a668BC6272B059BB7F53ADc91066540C", - "L1ERC20Gateway": "0xf952b82ED61797Cd0b93F02e42aC6B85EB4dF127", + "L1LiquidityPool": "0x8d5cC5C837EE7898133cCf56c71D64d0e78e16DF", + "L2LiquidityPool": "0xBF3013B96cBc021BC27D8e07ee6dfDD1bEc10bcE", + "L1ERC20": "0x2673a37B287b9896fbc9fB8E29Ed1d899BD4281E", + "L2DepositedERC20": "0x63531d3472ff8d012274AE2c0A9Ddf30ACC54Cc8", + "L1ERC20Gateway": "0x858B8623Ca51F089E7a51Dc81F7630901E6CCF55", "l1ETHGatewayAddress": "0x4F53A01556Dc6f120db9a1b4caE786D5f0b5792C", "l1MessengerAddress": "0xA6404B184Ad3f6F41b6472f02ba45f25C6A66d49", - "L2ERC721": "0x16Af4Db6548234c6463Ad6F0cf355260E96E741b" + "L2ERC721": "0xeB4cbbCE5B52ca1b167898dac418da2C68ca9c01" } \ No newline at end of file diff --git a/wallet/src/actions/nftAction.js b/wallet/src/actions/nftAction.js index 2c2f59b5ec5a..b80eae863f25 100644 --- a/wallet/src/actions/nftAction.js +++ b/wallet/src/actions/nftAction.js @@ -64,23 +64,23 @@ export async function addNFT ( NFTproperties ) { //if we already have looked it up, no need to look up again. if (state.nftList[UUID]) { - console.log("nftAction - already in list:",UUID) - console.log(state.nftList[UUID]) + //console.log("nftAction - already in list:",UUID) + //console.log(state.nftList[UUID]) return state.nftList[UUID]; } const nftInfo = { - //currency: _tokenContractAddress, - //symbol, - //name, - //redalert: _decimals ? false : true UUID: NFTproperties.UUID, owner: NFTproperties.owner, url: NFTproperties.url, mintedTime: NFTproperties.mintedTime, decimals: 0, + name: NFTproperties.name, + symbol: NFTproperties.symbol, }; + console.log("nftInfo0:",nftInfo) + store.dispatch({ type: 'NFT/GET/SUCCESS', payload: nftInfo diff --git a/wallet/src/components/nft/NftCard.js b/wallet/src/components/nft/NftCard.js new file mode 100644 index 000000000000..c4cb9749cb45 --- /dev/null +++ b/wallet/src/components/nft/NftCard.js @@ -0,0 +1,58 @@ +import React from 'react'; +import Card from '@material-ui/core/Card'; +import CardActionArea from '@material-ui/core/CardActionArea'; +import CardActions from '@material-ui/core/CardActions'; +import CardContent from '@material-ui/core/CardContent'; +import CardHeader from '@material-ui/core/CardHeader'; +import CardMedia from '@material-ui/core/CardMedia'; +import Button from '@material-ui/core/Button'; +import Typography from '@material-ui/core/Typography'; + +import * as styles from './NftCard.module.scss'; + +import cells from 'images/hela.jpg'; + +function NFTCard({ UUID, owner, URL, time, name, symbol }) { + + return ( + <Card className={styles.NFTroot}> + <CardActionArea> + <CardMedia + className={styles.NFTmedia} + image={cells} + title="Cell line" + /> + <CardContent> + <Typography gutterBottom variant="h4" component="h2"> + {name} + </Typography> + <Typography gutterBottom variant="h5" component="h3"> + {symbol} + </Typography> + <Typography variant="body2" color="textSecondary" component="p"> + <span style={{fontWeight: '600'}}>NFT ID: </span>{UUID}<br/> + <span style={{fontWeight: '600'}}>Owner: </span>{owner}<br/> + <span style={{fontWeight: '600'}}>Time Minted: </span>{time}<br/> + </Typography> + <Typography variant="body2" color="textSecondary" component="p"> + <a style={{color: 'blue'}} + href={URL} + > + LINK to datasheet + </a> + </Typography> + </CardContent> + </CardActionArea> + <CardActions> + <Button size="small" color="primary"> + Transfer + </Button> + <Button size="small" color="primary"> + Delete + </Button> + </CardActions> + </Card> + ); +} + +export default NFTCard; \ No newline at end of file diff --git a/wallet/src/components/nft/NftCard.module.scss b/wallet/src/components/nft/NftCard.module.scss new file mode 100644 index 000000000000..a88c2fba36db --- /dev/null +++ b/wallet/src/components/nft/NftCard.module.scss @@ -0,0 +1,42 @@ +@import 'index.scss'; + +.container { + padding-right: 20px; + + .boxContainer{ + display: inline-block; + max-width: 500px; + padding: 20px; + padding-top: 0px; + vertical-align: top; + + .button { + max-width: 500px; + margin-top: 10px; + margin-bottom: 10px; + } + .select { + flex: 1 1; + background-color: #d2d2d2; + color: #565656; + font-size: 1em; + border: none; + outline: none; + border: 1px solid #d2d2d2; + background-color: #F8F8F8; + padding: 10px; + width: 100%; + max-width: 500px; + } + } +} + +.NFTroot { + flexGrow: 1; + padding: 10px; + maxWidth: 345px; +} + +.NFTmedia { + height: 140px; +} \ No newline at end of file diff --git a/wallet/src/components/walletpicker/WalletPicker.js b/wallet/src/components/walletpicker/WalletPicker.js index ac968d36b267..6482daaaeffe 100644 --- a/wallet/src/components/walletpicker/WalletPicker.js +++ b/wallet/src/components/walletpicker/WalletPicker.js @@ -24,7 +24,6 @@ import { selectModalState } from 'selectors/uiSelector'; import { selectWalletMethod, selectNetwork, - //selectLayer, } from 'selectors/setupSelector'; import { openModal } from 'actions/uiAction'; @@ -48,11 +47,9 @@ function WalletPicker ({ onEnable }) { const walletMethod = useSelector(selectWalletMethod()) const networkName = useSelector(selectNetwork()) - //const netLayer = useSelector(selectLayer()) console.log("walletMethod:",walletMethod) console.log("networkName:",networkName) - //console.log("netLayer:",netLayer) const wrongNetworkModalState = useSelector(selectModalState('wrongNetworkModal')); diff --git a/wallet/src/containers/nft/Nft.js b/wallet/src/containers/nft/Nft.js index 3847f185d17c..2bc37e8d6bbf 100644 --- a/wallet/src/containers/nft/Nft.js +++ b/wallet/src/containers/nft/Nft.js @@ -4,10 +4,11 @@ import { isEqual } from 'lodash'; import InputSelect from 'components/inputselect/InputSelect'; import Input from 'components/input/Input'; +import NFTCard from 'components/nft/NftCard'; import Button from 'components/button/Button'; import { openError, openAlert } from 'actions/uiAction'; -import { logAmount } from 'util/amountConvert'; import networkService from 'services/networkService'; +import { Grid } from '@material-ui/core/' import * as styles from './Nft.module.scss'; @@ -17,37 +18,11 @@ class Nft extends React.Component { super(props); - const { balance } = this.props; + const { nftList } = this.props; this.state = { - balance, - - initialL1Value: '', - initialL1Currency: '', - L1Value: '', - L1Currency: '', - LPL1SearchToken: '', - LPL1Balance: '', - LPL1FeeSearchToken: '', - LPL1FeeBalance: '', - L1FeeWithdrawToken: '', - L1FeeReceiverAddress: '', - L1FeeWithdrawAmount: '', - - initialL2Value: '', - initialL2Currency: '', - L2Value: '', - L2Currency: '', - LPL2SearchToken: '', - LPL2Balance: '', - LPL2FeeSearchToken: '', - LPL2FeeBalance: '', - L2FeeWithdrawToken: '', - L2FeeReceiverAddress: '', - L2FeeWithdrawAmount: '', - + NFTs: nftList, loading: false, - receiverAddress: '', tokenID: 1, tokenURI: '' @@ -55,6 +30,7 @@ class Nft extends React.Component { } componentDidMount() { +/* const { balance } = this.props; if (balance.rootchain.length && balance.childchain.length) { const L1Token = balance.rootchain.filter(i => i.symbol !== 'WETH')[0]; @@ -72,189 +48,20 @@ class Nft extends React.Component { L2FeeWithdrawToken : L2Token }); } +*/ } componentDidUpdate(prevState) { - +/* const { balance } = this.props; - const { - initialL1Currency, initialL2Currency, - L1Currency, L2Currency, - LPL1SearchToken, LPL2SearchToken, - LPL1FeeSearchToken, LPL2FeeSearchToken, - L1FeeWithdrawToken, L2FeeWithdrawToken, - } = this.state; - const L1Token = balance.rootchain.filter(i => i.symbol !== 'WETH')[0]; - const L2Token = balance.childchain.filter(i => i.symbol !== 'ETH')[0]; + const { } = this.state; if (!isEqual(prevState.balance, balance)) { this.setState({ balance }); if (!initialL1Currency) this.setState({initialL1Currency : L1Token.currency}); - if (!initialL2Currency) this.setState({initialL2Currency : L2Token.currency}); - if (!L1Currency) this.setState({L1Currency : L1Token.currency}); - if (!L2Currency) this.setState({L2Currency : L2Token.currency}); - if (!LPL1SearchToken) this.setState({LPL1SearchToken : L1Token}); - if (!LPL2SearchToken) this.setState({LPL2SearchToken : L2Token}); - if (!LPL1FeeSearchToken) this.setState({LPL1FeeSearchToken : L1Token}); - if (!LPL2FeeSearchToken) this.setState({LPL2FeeSearchToken : L2Token}); - if (!L1FeeWithdrawToken) this.setState({L1FeeWithdrawToken : L1Token}); - if (!L2FeeWithdrawToken) this.setState({L2FeeWithdrawToken : L2Token}); - } - } - - async handleInitialDepositL1() { - const { initialL1Value, initialL1Currency } = this.state; - - const networkStatus = await this.props.dispatch(networkService.checkNetwork('L1')); - if (!networkStatus) { - this.props.dispatch(openError('Please use L1 network.')); - return; - } - - this.setState({ loading: true }); - const depositTX = await networkService.initialDepositL1LP(initialL1Currency, initialL1Value); - this.setState({ loading: false }); - if (depositTX) { - this.props.dispatch(openAlert(`Deposited ${initialL1Value} ${initialL1Currency} into L1 Liquidity Pool.`)) - } else { - this.props.dispatch(openError('Unknown error')); - } - } - - async handleInitialDepositL2() { - const { initialL2Value, initialL2Currency } = this.state; - - const networkStatus = await this.props.dispatch(networkService.checkNetwork('L2')); - if (!networkStatus) { - this.props.dispatch(openError('Please use L2 network.')); - return; - } - - this.setState({ loading: true }); - const depositTX = await networkService.initialDepositL2LP(initialL2Currency, initialL2Value); - this.setState({ loading: false }); - if (depositTX) { - this.props.dispatch(openAlert(`Deposited ${initialL2Value} ${initialL2Currency} into L2 Liquidity Pool.`)) - } else { - this.props.dispatch(openError('Unknown error')); - } - } - - async handleDepositL1() { - const { L1Value, L1Currency } = this.state; - - const networkStatus = await this.props.dispatch(networkService.checkNetwork('L1')); - if (!networkStatus) { - this.props.dispatch(openError('Please use L1 network.')); - return; - } - - this.setState({ loading: true }); - const depositTX = await networkService.depositL1LP(L1Currency, L1Value); - this.setState({ loading: false }); - if (depositTX) { - this.props.dispatch(openAlert(`Deposited ${L1Value} ${L1Currency} into L1 Liquidity Pool.`)) - } else { - this.props.dispatch(openError('L2 Liquidity Pool doesn\'t have enough balance to cover this.')); - } - } - - async handleDepositL2() { - const { L2Value, L2Currency } = this.state; - - const networkStatus = await this.props.dispatch(networkService.checkNetwork('L2')); - if (!networkStatus) { - this.props.dispatch(openError('Please use L2 network.')); - return; } - - this.setState({ loading: true }); - const depositTX = await networkService.depositL2LP(L2Currency, L2Value); - if (depositTX) { - this.setState({ loading: false }); - } - } - - async handleBalanceL1() { - const { LPL1SearchToken } = this.state; - const balanceTX = await networkService.L1LPBalance(LPL1SearchToken.currency); - if (balanceTX) { - this.setState({ LPL1Balance: balanceTX }); - } - } - - async handleBalanceL2() { - const { LPL2SearchToken } = this.state; - const balanceTX = await networkService.L2LPBalance(LPL2SearchToken.currency); - console.log({ balanceTX }); - if (balanceTX) { - this.setState({ LPL2Balance: balanceTX }); - } - } - - async handleFeeBalanceL1() { - const { LPL1FeeSearchToken } = this.state; - const balanceTX = await networkService.L1LPFeeBalance(LPL1FeeSearchToken.currency); - if (balanceTX) { - this.setState({ LPL1FeeBalance: balanceTX }); - } - } - - async handleFeeBalanceL2() { - const { LPL2FeeSearchToken } = this.state; - const balanceTX = await networkService.L2LPFeeBalance(LPL2FeeSearchToken.currency); - if (balanceTX) { - this.setState({ LPL2FeeBalance: balanceTX }); - } - } - - async handleWithdrawFeeL1() { - const { L1FeeWithdrawToken, L1FeeReceiverAddress, L1FeeWithdrawAmount } = this.state; - - const networkStatus = await this.props.dispatch(networkService.checkNetwork('L1')); - if (!networkStatus) { - this.props.dispatch(openError('Please use L1 network.')); - return; - } - - this.setState({ loading: true }); - const withdrawTX = await networkService.L1LPWithdrawFee( - L1FeeWithdrawToken.currency, - L1FeeReceiverAddress, - L1FeeWithdrawAmount - ); - - if (withdrawTX) { - this.props.dispatch(openAlert(`You withdraw ${L1FeeWithdrawAmount} ${L1FeeWithdrawToken.symbol}.`)); - } else { - this.props.dispatch(openError('You don\'t have enough fee amount to be withdrew')); - } - this.setState({ loading: false }) - } - - async handleWithdrawFeeL2() { - const { L2FeeWithdrawToken, L2FeeReceiverAddress, L2FeeWithdrawAmount } = this.state; - - const networkStatus = await this.props.dispatch(networkService.checkNetwork('L2')); - if (!networkStatus) { - this.props.dispatch(openError('Please use L2 network.')); - return; - } - - this.setState({ loading: true }); - const withdrawTX = await networkService.L2LPWithdrawFee( - L2FeeWithdrawToken.currency, - L2FeeReceiverAddress, - L2FeeWithdrawAmount - ); - - if (withdrawTX) { - this.props.dispatch(openAlert(`You withdraw ${L2FeeWithdrawAmount} ${L2FeeWithdrawToken.symbol}.`)); - } else { - this.props.dispatch(openError('You don\'t have enough fee amount to be withdrew')); - } - this.setState({ loading: false }) +*/ } async handleMintAndSend() { @@ -289,38 +96,13 @@ class Nft extends React.Component { render() { const { - balance, - initialL1Value, initialL1Currency, - L1Value, L1Currency, - LPL1SearchToken, LPL1Balance, - LPL1FeeSearchToken, LPL1FeeBalance, - L1FeeReceiverAddress, L1FeeWithdrawAmount, - initialL2Value, initialL2Currency, - L2Value, L2Currency, - LPL2SearchToken, LPL2Balance, - LPL2FeeSearchToken, LPL2FeeBalance, - L2FeeReceiverAddress, L2FeeWithdrawAmount, loading, - receiverAddress, tokenID, - tokenURI + tokenURI, + NFTs, } = this.state; - const rootChainBalance = balance.rootchain; - const selectL1Options = rootChainBalance.map(i => ({ - title: i.symbol, - value: i.currency, - subTitle: `Balance: ${logAmount(i.amount, i.decimals)}` - })).filter(i => i.title !== 'WETH'); - - const childChainBalance = balance.childchain; - const selectL2Options = childChainBalance.map(i => ({ - title: i.symbol, - value: i.currency, - subTitle: `Balance: ${logAmount(i.amount, i.decimals)}` - })).filter(i => i.title !== 'ETH'); - return ( <div className={styles.container}> <div className={styles.boxContainer}> @@ -351,267 +133,32 @@ class Nft extends React.Component { Mint and Send </Button> - <Button - className={styles.button} - onClick={() => {this.handleInitialDepositL1();}} - type='primary' - loading={loading} - tooltip='Your deposit transaction is still pending. Please wait for confirmation.' - disabled={!initialL1Value || !initialL1Currency} - > - Initial Deposit L1 - </Button> - - <h3>Deposit L1</h3> - <InputSelect - label='Amount to deposit' - placeholder={0} - value={L1Value} - onChange={i => {this.setState({L1Value: i.target.value})}} - selectOptions={selectL1Options} - onSelect={i => {this.setState({L1Currency: i.target.value})}} - selectValue={L1Currency} - /> - - <Button - className={styles.button} - onClick={() => {this.handleDepositL1();}} - type='primary' - loading={loading} - tooltip='Your deposit transaction is still pending. Please wait for confirmation.' - disabled={!L1Value || !L1Currency} - > - Deposit L1 - </Button> - - <h3>L1 Liquidity Pool Balance</h3> - <select - className={styles.select} - onChange={i => { - const token = balance.rootchain.filter(j => j.currency === i.target.value)[0]; - this.setState({ LPL1SearchToken: token, LPL1Balance: '' }); - }} - > - {selectL1Options.map((option, i) => ( - <option key={i} value={option.value}>{option.title} - {option.value}</option> - ))} - </select> - - <Button - className={styles.button} - onClick={() => {this.handleBalanceL1();}} - type='primary' - loading={loading} - tooltip='Your deposit transaction is still pending. Please wait for confirmation.' - > - L1 Balance - </Button> - - {LPL1Balance && - <h3> - {`The L1 liquidity pool has ${LPL1Balance} ${LPL1SearchToken.symbol}.`} - </h3> - } - - <h3>L1 Liquidity Pool Fee Balance</h3> - <select - className={styles.select} - onChange={i => { - const token = balance.rootchain.filter(j => j.currency === i.target.value)[0]; - this.setState({ LPL1FeeSearchToken: token, LPL1FeeBalance: '' }); - }} - > - {selectL1Options.map((option, i) => ( - <option key={i} value={option.value}>{option.title} - {option.value}</option> - ))} - </select> - - <Button - className={styles.button} - onClick={() => {this.handleFeeBalanceL1();}} - type='primary' - loading={loading} - tooltip='Your deposit transaction is still pending. Please wait for confirmation.' - > - L1 Fee Balance - </Button> - - {LPL1FeeBalance && - <h3> - {`The L1 liquidity pool has ${LPL1FeeBalance} ${LPL1FeeSearchToken.symbol}.`} - </h3> - } - - <h3>Withdraw L1 Liquidity Pool Fee</h3> - <h4 style={{marginTop: 5, marginBottom: 5}}>Fee</h4> - <select - className={styles.select} - onChange={i => { - const token = balance.rootchain.filter(j => j.currency === i.target.value)[0]; - this.setState({ L1FeeWithdrawToken: token }); - }} - > - {selectL1Options.map((option, i) => ( - <option key={i} value={option.value}>{option.title} - {option.value}</option> - ))} - </select> - <h4 style={{marginTop: 5, marginBottom: 5}}>Receiver</h4> - <Input - placeholder="Receiver" - onChange={i => {this.setState({ L1FeeReceiverAddress: i.target.value })}} - /> - <h4 style={{marginTop: 5, marginBottom: 5}}>Amount</h4> - <Input - placeholder="Amount" - onChange={i => {this.setState({ L1FeeWithdrawAmount: i.target.value })}} - /> - <Button - className={styles.button} - onClick={() => {this.handleWithdrawFeeL1();}} - type='primary' - loading={loading} - disabled={!L1FeeReceiverAddress || !L1FeeWithdrawAmount} - > - Withdraw Fee - </Button> - - </div> - - <div className={styles.boxContainer}> - <h2>Layer 2 Liquidity Pool</h2> - <h3>Initial Deposit L2</h3> - <InputSelect - label='Amount to deposit (No fund on L1)' - placeholder={0} - value={initialL2Value} - onChange={i => {this.setState({initialL2Value: i.target.value})}} - selectOptions={selectL2Options} - onSelect={i => {this.setState({initialL2Currency: i.target.value})}} - selectValue={initialL2Currency} - /> - <Button - className={styles.button} - onClick={() => {this.handleInitialDepositL2();}} - type='primary' - loading={loading} - tooltip='Your deposit transaction is still pending. Please wait for confirmation.' - disabled={!initialL2Value || !initialL2Currency} - > - Initial Deposit L2 - </Button> - - <h3>Deposit L2</h3> - <InputSelect - label='Amount to deposit' - placeholder={0} - value={L2Value} - onChange={i => {this.setState({L2Value: i.target.value})}} - selectOptions={selectL2Options} - onSelect={i => {this.setState({L2Currency: i.target.value})}} - selectValue={L2Currency} - /> - - <Button - className={styles.button} - onClick={() => {this.handleDepositL2();}} - type='primary' - loading={loading} - tooltip='Your deposit transaction is still pending. Please wait for confirmation.' - disabled={!L2Value || !L2Currency} - > - Deposit L2 - </Button> - - <h3>L2 Liquidity Pool Balance</h3> - <select - className={styles.select} - onChange={i => { - const token = balance.childchain.filter(j => j.currency === i.target.value)[0]; - this.setState({ LPL2SearchToken: token, LPL2Balance: '' }); - }} - > - {selectL2Options.map((option, i) => ( - <option key={i} value={option.value}>{option.title} - {option.value}</option> - ))} - </select> - - <Button - className={styles.button} - onClick={() => {this.handleBalanceL2();}} - type='primary' - loading={loading} - tooltip='Your deposit transaction is still pending. Please wait for confirmation.' - > - L2 Balance - </Button> - - {LPL2Balance && - <h3> - {`The L2 liquidity pool has ${LPL2Balance} ${LPL2SearchToken.symbol}.`} - </h3> - } - - <h3>L2 Liquidity Pool Fee Balance</h3> - <select - className={styles.select} - onChange={i => { - const token = balance.childchain.filter(j => j.currency === i.target.value)[0]; - this.setState({ LPL2FeeSearchToken: token, LPL2FeeBalance: '' }); - }} - > - {selectL2Options.map((option, i) => ( - <option key={i} value={option.value}>{option.title} - {option.value}</option> - ))} - </select> - - <Button - className={styles.button} - onClick={() => {this.handleFeeBalanceL2();}} - type='primary' - loading={loading} - tooltip='Your deposit transaction is still pending. Please wait for confirmation.' - > - L1 Fee Balance - </Button> + <h3>My NFTS</h3> + + <div className={styles.root}> + <Grid + container + spacing={2} + direction="row" + justify="flex-start" + alignItems="flex-start" + > + {Object.keys(NFTs).map(elem => ( + <Grid item xs={12} sm={9} md={6} key={elem}> + <NFTCard + name={NFTs[elem].name} + symbol={NFTs[elem].symbol} + UUID={NFTs[elem].UUID} + owner={NFTs[elem].owner} + URL={NFTs[elem].url} + time={NFTs[elem].mintedTime} + > + </NFTCard> + </Grid> + ))} + </Grid> + </div> - {LPL2FeeBalance && - <h3> - {`The L2 liquidity pool has ${LPL2FeeBalance} ${LPL2FeeSearchToken.symbol}.`} - </h3> - } - - <h3>Withdraw L2 Liquidity Pool Fee</h3> - <h4 style={{marginTop: 5, marginBottom: 5}}>Fee</h4> - <select - className={styles.select} - onChange={i => { - const token = balance.childchain.filter(j => j.currency === i.target.value)[0]; - this.setState({ L2FeeWithdrawToken: token }); - }} - > - {selectL2Options.map((option, i) => ( - <option key={i} value={option.value}>{option.title} - {option.value}</option> - ))} - </select> - <h4 style={{marginTop: 5, marginBottom: 5}}>Receiver</h4> - <Input - placeholder="Receiver" - onChange={i => {this.setState({ L2FeeReceiverAddress: i.target.value })}} - /> - <h4 style={{marginTop: 5, marginBottom: 5}}>Amount</h4> - <Input - placeholder="Amount" - onChange={i => {this.setState({ L2FeeWithdrawAmount: i.target.value })}} - /> - <Button - className={styles.button} - onClick={() => {this.handleWithdrawFeeL2();}} - type='primary' - loading={loading} - disabled={!L2FeeReceiverAddress || !L2FeeWithdrawAmount} - > - Withdraw Fee - </Button> </div> </div> @@ -625,7 +172,7 @@ const mapStateToProps = state => ({ sellTask: state.sellTask, balance: state.balance, transaction: state.transaction, - tokenList: state.tokenList + nftList: state.nftList }); export default connect(mapStateToProps)(Nft); \ No newline at end of file diff --git a/wallet/src/containers/nft/Nft.module.scss b/wallet/src/containers/nft/Nft.module.scss index ba2926115baf..56a2751e723d 100644 --- a/wallet/src/containers/nft/Nft.module.scss +++ b/wallet/src/containers/nft/Nft.module.scss @@ -29,5 +29,14 @@ max-width: 500px; } } +} +.NFTroot { + flexGrow: 1; + padding: 10px; + maxWidth: 345; +} + +.NFTmedia { + height: 140; } \ No newline at end of file diff --git a/wallet/src/images/hela.jpg b/wallet/src/images/hela.jpg new file mode 100644 index 0000000000000000000000000000000000000000..03f6cf182a3c5f9773079770d944d53171b936a0 GIT binary patch literal 44662 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 literal 0 HcmV?d00001 diff --git a/wallet/src/services/networkService.js b/wallet/src/services/networkService.js index 93c41d46221e..d50724472894 100644 --- a/wallet/src/services/networkService.js +++ b/wallet/src/services/networkService.js @@ -39,7 +39,7 @@ import L2LPJson from '../deployment/artifacts-ovm/contracts/L2LiquidityPool.sol/ import L1ERC20Json from '../deployment/artifacts/contracts/ERC20.sol/ERC20.json' import L2DepositedERC20Json from '../deployment/artifacts-ovm/contracts/L2DepositedERC20.sol/L2DepositedERC20.json' import L1ERC20GatewayJson from '../deployment/artifacts/contracts/L1ERC20Gateway.sol/L1ERC20Gateway.json' -import L2ERC721Json from '../deployment/artifacts-ovm/contracts/ERC721Mock.sol/ERC721Mock.json' +import ERC721Json from '../deployment/artifacts-ovm/contracts/ERC721Mock.sol/ERC721Mock.json' import { powAmount, logAmount } from 'util/amountConvert'; import { getAllNetworks } from 'util/networkName'; @@ -253,7 +253,7 @@ class NetworkService { this.ERC721Contract = new ethers.Contract( this.ERC721Address, - L2ERC721Json.abi, + ERC721Json.abi, this.web3Provider.getSigner(), ); @@ -305,7 +305,7 @@ class NetworkService { //const ERC721L2Balance = 0; //await this.ERC721Contract.balanceOf(this.account); // //how many NFTs do I own? - const ERC721L2Balance = await this.ERC721Contract.balanceOf(this.account); + const ERC721L2Balance = await this.ERC721Contract.balanceOf(this.account) //console.log("ERC721L2Balance",ERC721L2Balance) //console.log("this.account",this.account) @@ -314,43 +314,54 @@ class NetworkService { //let see if we already know about them const myNFTS = await getNFTs() const numberOfNFTS = Object.keys(myNFTS).length; - console.log(myNFTS) + //console.log(myNFTS) //console.log(ERC721L2Balance.toString()) //console.log(numberOfNFTS.toString()) - if(ERC721L2Balance.toString() !== numberOfNFTS.toString()) { + if(ERC721L2Balance.toNumber() !== numberOfNFTS) { + //oh - something just changed - either got one, or sent one - //we need to do something! - //get the first one console.log("NFT change detected!") - - let tokenID = BigNumber.from(0) - let nftTokenIDs = await this.ERC721Contract.tokenOfOwnerByIndex(this.account, tokenID) - let nftMeta = await this.ERC721Contract.getTokenURI(tokenID) - let meta = nftMeta.split("#") - - addNFT({ - UUID: this.ERC721Address.substring(1, 6) + "_" + nftTokenIDs.toString() + "_" + this.account.substring(1, 6), - owner: meta[0], - mintedTime: meta[1], - url: meta[2], - tokenID: tokenID - }) - - //get the second one - tokenID = BigNumber.from(1) - nftTokenIDs = await this.ERC721Contract.tokenOfOwnerByIndex(this.account, tokenID) - nftMeta = await this.ERC721Contract.getTokenURI(tokenID) - meta = nftMeta.split("#") - - addNFT({ - UUID: this.ERC721Address.substring(1, 6) + "_" + nftTokenIDs.toString() + "_" + this.account.substring(1, 6), - owner: meta[0], - mintedTime: meta[1], - url: meta[2], - tokenID: tokenID - }) + + //we need to do something + //get the first one + + let tokenID = null + let nftTokenIDs = null + let nftMeta = null + let meta = null + + //always the same, no need to have in the loop + let nftName = await this.ERC721Contract.getName() + console.log("nftName:",nftName) + let nftSymbol = await this.ERC721Contract.getSymbol() + console.log("nftSymbol:",nftSymbol) + + for (var i = 0; i < ERC721L2Balance.toNumber(); i++) { + + tokenID = BigNumber.from(i) + nftTokenIDs = await this.ERC721Contract.tokenOfOwnerByIndex(this.account, tokenID) + nftMeta = await this.ERC721Contract.getTokenURI(tokenID) + meta = nftMeta.split("#") + + console.log("nftName:",nftName) + console.log("nftSymbol:",nftSymbol) + + const time = new Date(parseInt(meta[1])); + console.log(time) + + addNFT({ + UUID: this.ERC721Address.substring(1, 6) + "_" + nftTokenIDs.toString() + "_" + this.account.substring(1, 6), + owner: meta[0], + mintedTime: String(time.toLocaleString('en-US', { weekday: 'narrow', month: '2-digit', year: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true })), + url: meta[2], + tokenID: tokenID, + name: nftName, + symbol: nftSymbol + }) + } + } else { //console.log("No NFT changes") //all set - do nothing From e53e1297d8e1ba75e3c2090d4a08093059387f49 Mon Sep 17 00:00:00 2001 From: Jan Liphardt <jan@enya.ai> Date: Sun, 9 May 2021 18:03:54 -0700 Subject: [PATCH 09/10] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b4fd32f25e04..6b281ab40857 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "dependencies": { "@openzeppelin/contracts": "^3.3.0", "dotenv": "^8.2.0", - "enyalabs_contracts": "git+ssh://git@github.com:enyalabs/contracts.git#rinkeby", + "enyalabs_contracts": "git+ssh://git@github.com:enyalabs/contracts.git", "glob": "^7.1.6", "patch-package": "^6.4.7" } From bf1578453330bf88bdb87f08e5ded292f56c1f6a Mon Sep 17 00:00:00 2001 From: Jan Liphardt <jan@enya.ai> Date: Sun, 9 May 2021 18:26:22 -0700 Subject: [PATCH 10/10] cleanup --- .env.example | 4 +- patches/@openzeppelin+contracts+3.4.1.patch | 44 +++++++++++++++++ wallet/src/actions/nftAction.js | 38 +-------------- wallet/src/components/nft/NftCard.js | 1 - .../components/walletpicker/WalletPicker.js | 14 +++--- wallet/src/containers/nft/Nft.js | 47 +++---------------- wallet/src/reducers/nftReducer.js | 15 ------ wallet/src/services/networkService.js | 28 ++--------- 8 files changed, 65 insertions(+), 126 deletions(-) diff --git a/.env.example b/.env.example index 45dbcaa191e0..2151b913c4f6 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,7 @@ ETH1_ADDRESS_RESOLVER_ADDRESS=0xa32cf2433ba24595d3aCE5cc9A7079d3f1CC5E0c TEST_PRIVATE_KEY_1= TEST_PRIVATE_KEY_2= TARGET_GAS_LIMIT=9000000000 -CHAIN_ID=420 +CHAIN_ID=28 # Local NODE_ENV=local @@ -16,4 +16,4 @@ ETH1_ADDRESS_RESOLVER_ADDRESS=0x3e4CFaa8730092552d9425575E49bB542e329981 TEST_PRIVATE_KEY_1=0x754fde3f5e60ef2c7649061e06957c29017fe21032a8017132c0078e37f6193a TEST_PRIVATE_KEY_2=0x23d9aeeaa08ab710a57972eb56fc711d9ab13afdecc92c89586e0150bfa380a6 TARGET_GAS_LIMIT=9000000000 -CHAIN_ID=420 \ No newline at end of file +CHAIN_ID=28 \ No newline at end of file diff --git a/patches/@openzeppelin+contracts+3.4.1.patch b/patches/@openzeppelin+contracts+3.4.1.patch index e8d8e2d92626..4a74a382bda8 100644 --- a/patches/@openzeppelin+contracts+3.4.1.patch +++ b/patches/@openzeppelin+contracts+3.4.1.patch @@ -1,3 +1,47 @@ +diff --git a/node_modules/@openzeppelin/contracts/token/ERC721/ERC721.sol b/node_modules/@openzeppelin/contracts/token/ERC721/ERC721.sol +index 7958fdf..20b8972 100644 +--- a/node_modules/@openzeppelin/contracts/token/ERC721/ERC721.sol ++++ b/node_modules/@openzeppelin/contracts/token/ERC721/ERC721.sol +@@ -19,6 +19,7 @@ import "../../utils/Strings.sol"; + * @dev see https://eips.ethereum.org/EIPS/eip-721 + */ + contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable { ++ + using SafeMath for uint256; + using Address for address; + using EnumerableSet for EnumerableSet.UintSet; +@@ -136,18 +137,19 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable + require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token"); + + string memory _tokenURI = _tokenURIs[tokenId]; +- string memory base = baseURI(); +- +- // If there is no base URI, return the token URI. +- if (bytes(base).length == 0) { +- return _tokenURI; +- } +- // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked). +- if (bytes(_tokenURI).length > 0) { +- return string(abi.encodePacked(base, _tokenURI)); +- } +- // If there is a baseURI but no tokenURI, concatenate the tokenID to the baseURI. +- return string(abi.encodePacked(base, tokenId.toString())); ++ //string memory base = baseURI(); ++ ++ // // If there is no base URI, return the token URI. ++ // if (bytes(base).length == 0) { ++ return _tokenURI; ++ // } ++ // // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked). ++ // if (bytes(_tokenURI).length > 0) { ++ // return string(abi.encodePacked(base, _tokenURI)); ++ // } ++ // // If there is a baseURI but no tokenURI, concatenate the tokenID to the baseURI. ++ // return string(abi.encodePacked(base, tokenId.toString())); ++ //return "TestTestTest"; + } + + /** diff --git a/node_modules/@openzeppelin/contracts/utils/Address.sol b/node_modules/@openzeppelin/contracts/utils/Address.sol index 42a9dc1..bb610c2 100644 --- a/node_modules/@openzeppelin/contracts/utils/Address.sol diff --git a/wallet/src/actions/nftAction.js b/wallet/src/actions/nftAction.js index b80eae863f25..1ff0285555a8 100644 --- a/wallet/src/actions/nftAction.js +++ b/wallet/src/actions/nftAction.js @@ -13,44 +13,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import erc20abi from 'human-standard-token-abi'; -import networkService from 'services/networkService'; import store from 'store'; -/* -import { getNFTs, addNFT } from 'actions/nftAction'; -*/ - - -/* -Returns Token info -If we don't have the info, try to get it -*/ -/* -export async function getToken ( tokenContractAddress ) { - - //this *might* be coming from a person, and or copy-paste from Etherscan - //so need toLowerCase() - // ***************************************************************** - const _tokenContractAddress = tokenContractAddress.toLowerCase(); - // ***************************************************************** - - const state = store.getState(); - if (state.tokenList[_tokenContractAddress]) { - //console.log("tokenAction = token in list:",_tokenContractAddress) - return state.tokenList[_tokenContractAddress]; - } else { - console.log("tokenAction = token not yet in list:",_tokenContractAddress) - const tokenInfo = await addToken( _tokenContractAddress ) - return tokenInfo; - } -} -*/ - export async function getNFTs () { const state = store.getState() - //console.log("export async function getNFTs") - //console.log(state.nftList) return state.nftList; } @@ -64,8 +30,6 @@ export async function addNFT ( NFTproperties ) { //if we already have looked it up, no need to look up again. if (state.nftList[UUID]) { - //console.log("nftAction - already in list:",UUID) - //console.log(state.nftList[UUID]) return state.nftList[UUID]; } @@ -79,7 +43,7 @@ export async function addNFT ( NFTproperties ) { symbol: NFTproperties.symbol, }; - console.log("nftInfo0:",nftInfo) + //console.log("nftInfo0:",nftInfo) store.dispatch({ type: 'NFT/GET/SUCCESS', diff --git a/wallet/src/components/nft/NftCard.js b/wallet/src/components/nft/NftCard.js index c4cb9749cb45..a63006184353 100644 --- a/wallet/src/components/nft/NftCard.js +++ b/wallet/src/components/nft/NftCard.js @@ -3,7 +3,6 @@ import Card from '@material-ui/core/Card'; import CardActionArea from '@material-ui/core/CardActionArea'; import CardActions from '@material-ui/core/CardActions'; import CardContent from '@material-ui/core/CardContent'; -import CardHeader from '@material-ui/core/CardHeader'; import CardMedia from '@material-ui/core/CardMedia'; import Button from '@material-ui/core/Button'; import Typography from '@material-ui/core/Typography'; diff --git a/wallet/src/components/walletpicker/WalletPicker.js b/wallet/src/components/walletpicker/WalletPicker.js index 6482daaaeffe..30bbb26a1313 100644 --- a/wallet/src/components/walletpicker/WalletPicker.js +++ b/wallet/src/components/walletpicker/WalletPicker.js @@ -48,18 +48,18 @@ function WalletPicker ({ onEnable }) { const walletMethod = useSelector(selectWalletMethod()) const networkName = useSelector(selectNetwork()) - console.log("walletMethod:",walletMethod) - console.log("networkName:",networkName) + //console.log("walletMethod:",walletMethod) + //console.log("networkName:",networkName) const wrongNetworkModalState = useSelector(selectModalState('wrongNetworkModal')); const dispatchSetWalletMethod = useCallback((methodName) => { - console.log("dispatchSetWalletMethod:",methodName) + //console.log("dispatchSetWalletMethod:",methodName) dispatch(setWalletMethod(methodName)); }, [ dispatch ]) const dispatchSetNetwork = useCallback((network) => { - console.log("dispatchSetNetwork:",network) + //console.log("dispatchSetNetwork:",network) setShowAllNetworks(false); dispatch(setNetwork(network)); }, [ dispatch ]) @@ -71,10 +71,10 @@ function WalletPicker ({ onEnable }) { } async function enableBrowserWallet () { - console.log("enableBrowserWallet() for",networkName) + //console.log("enableBrowserWallet() for",networkName) const selectedNetwork = networkName ? networkName : "local"; const walletEnabled = await networkService.enableBrowserWallet(selectedNetwork); - console.log("walletEnabled:",walletEnabled) + //console.log("walletEnabled:",walletEnabled) return walletEnabled ? setWalletEnabled(true) : dispatchSetWalletMethod(null); @@ -86,7 +86,7 @@ function WalletPicker ({ onEnable }) { async function initializeAccounts () { - console.log("initializeAccounts() for:",networkName) + //console.log("initializeAccounts() for:",networkName) const initialized = await networkService.initializeAccounts(networkName); diff --git a/wallet/src/containers/nft/Nft.js b/wallet/src/containers/nft/Nft.js index 2bc37e8d6bbf..af1a1a3752e1 100644 --- a/wallet/src/containers/nft/Nft.js +++ b/wallet/src/containers/nft/Nft.js @@ -1,8 +1,5 @@ import React from 'react'; import { connect } from 'react-redux'; -import { isEqual } from 'lodash'; - -import InputSelect from 'components/inputselect/InputSelect'; import Input from 'components/input/Input'; import NFTCard from 'components/nft/NftCard'; import Button from 'components/button/Button'; @@ -30,47 +27,19 @@ class Nft extends React.Component { } componentDidMount() { -/* - const { balance } = this.props; - if (balance.rootchain.length && balance.childchain.length) { - const L1Token = balance.rootchain.filter(i => i.symbol !== 'WETH')[0]; - const L2Token = balance.childchain.filter(i => i.symbol !== 'ETH')[0]; - this.setState({ - initialL1Currency: L1Token.currency, - initialL2Currency: L2Token.currency, - L1Currency : L1Token.currency, - L2Currency : L2Token.currency, - LPL1SearchToken : L1Token, - LPL2SearchToken : L2Token, - LPL1FeeSearchToken : L1Token, - LPL2FeeSearchToken : L2Token, - L1FeeWithdrawToken : L1Token, - L2FeeWithdrawToken : L2Token - }); - } -*/ + //ToDo } componentDidUpdate(prevState) { -/* - const { balance } = this.props; - - const { } = this.state; - - if (!isEqual(prevState.balance, balance)) { - this.setState({ balance }); - if (!initialL1Currency) this.setState({initialL1Currency : L1Token.currency}); - } -*/ + //ToDo } async handleMintAndSend() { const { receiverAddress, tokenID, tokenURI } = this.state; - //we are doing this on L2 - const networkStatus = await this.props.dispatch(networkService.checkNetwork('L2')); + if (!networkStatus) { this.props.dispatch(openError('Please use L2 network.')); return; @@ -104,12 +73,13 @@ class Nft extends React.Component { } = this.state; return ( + <div className={styles.container}> <div className={styles.boxContainer}> <h2>Minter/Owner Functions</h2> - <h3>Mint and Send</h3> + <h3>Mint and Send (Contract Owner Only)</h3> <Input placeholder="Receiver Address (e.g. Ox.....)" @@ -133,7 +103,7 @@ class Nft extends React.Component { Mint and Send </Button> - <h3>My NFTS</h3> + <h3>My NFTs</h3> <div className={styles.root}> <Grid @@ -167,11 +137,6 @@ class Nft extends React.Component { } const mapStateToProps = state => ({ - login: state.login, - sell: state.sell, - sellTask: state.sellTask, - balance: state.balance, - transaction: state.transaction, nftList: state.nftList }); diff --git a/wallet/src/reducers/nftReducer.js b/wallet/src/reducers/nftReducer.js index d71cd7bba18c..71b43cbaa76f 100644 --- a/wallet/src/reducers/nftReducer.js +++ b/wallet/src/reducers/nftReducer.js @@ -14,26 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ const initialState = { - /* - [ETH0x]: { - currency: ETH0x, - decimals: 18, - symbol: 'ETH', - name: 'Ethereum' - }, - [oETH]: { - currency: oETH, - decimals: 18, - symbol: 'oETH', - name: 'Ethereum', - } - */ }; function nftReducer (state = initialState, action) { switch (action.type) { case 'NFT/GET/SUCCESS': - console.log(state) return { ...state, [action.payload.UUID]: action.payload, diff --git a/wallet/src/services/networkService.js b/wallet/src/services/networkService.js index d50724472894..0233281aa5fc 100644 --- a/wallet/src/services/networkService.js +++ b/wallet/src/services/networkService.js @@ -141,6 +141,7 @@ class NetworkService { async initializeAccounts ( networkName ) { console.log("NS: initializeAccounts() for",networkName) + try { let addresses; if (networkName === 'local') addresses = localAddresses; @@ -152,9 +153,9 @@ class NetworkService { const network = await this.web3Provider.getNetwork(); this.networkName = networkName; - console.log("NS: networkName:",this.networkName) - console.log("NS: account:",this.account) - console.log("NS: network:",network) + //console.log("NS: networkName:",this.networkName) + //console.log("NS: account:",this.account) + //console.log("NS: network:",network) //there are numerous possible chains we could be on //either local, rinkeby etc @@ -334,9 +335,7 @@ class NetworkService { //always the same, no need to have in the loop let nftName = await this.ERC721Contract.getName() - console.log("nftName:",nftName) let nftSymbol = await this.ERC721Contract.getSymbol() - console.log("nftSymbol:",nftSymbol) for (var i = 0; i < ERC721L2Balance.toNumber(); i++) { @@ -345,16 +344,12 @@ class NetworkService { nftMeta = await this.ERC721Contract.getTokenURI(tokenID) meta = nftMeta.split("#") - console.log("nftName:",nftName) - console.log("nftSymbol:",nftSymbol) - const time = new Date(parseInt(meta[1])); - console.log(time) addNFT({ UUID: this.ERC721Address.substring(1, 6) + "_" + nftTokenIDs.toString() + "_" + this.account.substring(1, 6), owner: meta[0], - mintedTime: String(time.toLocaleString('en-US', { weekday: 'narrow', month: '2-digit', year: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true })), + mintedTime: String(time.toLocaleString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true })), url: meta[2], tokenID: tokenID, name: nftName, @@ -377,14 +372,6 @@ class NetworkService { testToken = await getToken(this.L2DepositedERC20Address); } - const nftInfo = { - currency: this.ERC721Address, - symbol: "BBE (NFT)", - decimals: 0, - name: "BioEcon", - redalert: false - } - const rootchainEthBalance = [ { ...ethToken, @@ -409,11 +396,6 @@ class NetworkService { currency: this.L2DepositedERC20Address, amount: new BN(ERC20L2Balance.toString()), }, - { - ...nftInfo, - currency: this.ERC721Address, - amount: new BN(ERC721L2Balance.toString()), - }, ] return {