diff --git a/miniwallet/contracts/nft/HRC721/HRC721.sol b/miniwallet/contracts/nft/HRC721/HRC721.sol new file mode 100644 index 0000000..229221e --- /dev/null +++ b/miniwallet/contracts/nft/HRC721/HRC721.sol @@ -0,0 +1,691 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Creator: Chiru Labs +// Feb 23rd 2022, Modification for sms-wallet by John Whitton + +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/utils/Context.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +error ApprovalCallerNotOwnerNorApproved(); +error ApprovalQueryForNonexistentToken(); +error ApproveToCaller(); +error ApprovalToCurrentOwner(); +error BalanceQueryForZeroAddress(); +error MintedQueryForZeroAddress(); +error BurnedQueryForZeroAddress(); +error MintToZeroAddress(); +error MintZeroQuantity(); +error OwnerIndexOutOfBounds(); +error OwnerQueryForNonexistentToken(); +error TokenIndexOutOfBounds(); +error TransferCallerNotOwnerNorApproved(); +error TransferFromIncorrectOwner(); +error TransferToNonERC721ReceiverImplementer(); +error TransferToZeroAddress(); +error URIQueryForNonexistentToken(); + +/** + * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including + * the Metadata and Enumerable extension. Built to optimize for lower gas during batch mints. + * + * Assumes serials are sequentially minted starting at 0 (e.g. 0, 1, 2, 3..). + * + * Assumes that an owner cannot have more than 2**64 - 1 (max value of uint64) of supply. + * + * Assumes that the maximum token id cannot exceed 2**128 - 1 (max value of uint128). + */ +contract HRC721 is + Context, + ERC165, + IERC721, + IERC721Metadata, + IERC721Enumerable +{ + using Address for address; + using Strings for uint256; + + // Compiler will pack this into a single 256bit word. + struct TokenOwnership { + // The address of the owner. + address addr; + // Keeps track of the start time of ownership with minimal overhead for tokenomics. + uint64 startTimestamp; + // Whether the token has been burned. + bool burned; + } + + // Compiler will pack this into a single 256bit word. + struct AddressData { + // Realistically, 2**64-1 is more than enough. + uint64 balance; + // Keeps track of mint count with minimal overhead for tokenomics. + uint64 numberMinted; + // Keeps track of burn count with minimal overhead for tokenomics. + uint64 numberBurned; + } + + // Compiler will pack the following + // _currentIndex and _burnCounter into a single 256bit word. + + // The tokenId of the next token to be minted. + uint128 internal _currentIndex; + + // The number of tokens burned. + uint128 internal _burnCounter; + + // Token name + string private _name; + + // Token symbol + string private _symbol; + + // Mapping from token ID to ownership details + // An empty struct value does not necessarily mean the token is unowned. See ownershipOf implementation for details. + mapping(uint256 => TokenOwnership) internal _ownerships; + + // Mapping owner address to address data + mapping(address => AddressData) private _addressData; + + // 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; + + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev See {IERC721Enumerable-totalSupply}. + */ + function totalSupply() public view override returns (uint256) { + // Counter underflow is impossible as _burnCounter cannot be incremented + // more than _currentIndex times + unchecked { + return _currentIndex - _burnCounter; + } + } + + /** + * @dev See {IERC721Enumerable-tokenByIndex}. + * This read function is O(totalSupply). If calling from a separate contract, be sure to test gas first. + * It may also degrade with extremely large collection sizes (e.g >> 10000), test for your use case. + */ + function tokenByIndex(uint256 index) + public + view + override + returns (uint256) + { + uint256 numMintedSoFar = _currentIndex; + uint256 tokenIdsIdx; + + // Counter overflow is impossible as the loop breaks when + // uint256 i is equal to another uint256 numMintedSoFar. + unchecked { + for (uint256 i; i < numMintedSoFar; i++) { + TokenOwnership memory ownership = _ownerships[i]; + if (!ownership.burned) { + if (tokenIdsIdx == index) { + return i; + } + tokenIdsIdx++; + } + } + } + revert TokenIndexOutOfBounds(); + } + + /** + * @dev See {IERC721Enumerable-tokenOfOwnerByIndex}. + * This read function is O(totalSupply). If calling from a separate contract, be sure to test gas first. + * It may also degrade with extremely large collection sizes (e.g >> 10000), test for your use case. + */ + function tokenOfOwnerByIndex(address owner, uint256 index) + public + view + override + returns (uint256) + { + if (index >= balanceOf(owner)) revert OwnerIndexOutOfBounds(); + uint256 numMintedSoFar = _currentIndex; + uint256 tokenIdsIdx; + address currOwnershipAddr; + + // Counter overflow is impossible as the loop breaks when + // uint256 i is equal to another uint256 numMintedSoFar. + unchecked { + for (uint256 i; i < numMintedSoFar; i++) { + TokenOwnership memory ownership = _ownerships[i]; + if (ownership.burned) { + continue; + } + if (ownership.addr != address(0)) { + currOwnershipAddr = ownership.addr; + } + if (currOwnershipAddr == owner) { + if (tokenIdsIdx == index) { + return i; + } + tokenIdsIdx++; + } + } + } + + // Execution should never reach this point. + revert(); + } + + /** + * @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 || + interfaceId == type(IERC721Enumerable).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * @dev See {IERC721-balanceOf}. + */ + function balanceOf(address owner) public view override returns (uint256) { + if (owner == address(0)) revert BalanceQueryForZeroAddress(); + return uint256(_addressData[owner].balance); + } + + function _numberMinted(address owner) internal view returns (uint256) { + if (owner == address(0)) revert MintedQueryForZeroAddress(); + return uint256(_addressData[owner].numberMinted); + } + + function _numberBurned(address owner) internal view returns (uint256) { + if (owner == address(0)) revert BurnedQueryForZeroAddress(); + return uint256(_addressData[owner].numberBurned); + } + + /** + * Gas spent here starts off proportional to the maximum mint batch size. + * It gradually moves to O(1) as tokens get transferred around in the collection over time. + */ + function ownershipOf(uint256 tokenId) + internal + view + returns (TokenOwnership memory) + { + uint256 curr = tokenId; + + unchecked { + if (curr < _currentIndex) { + TokenOwnership memory ownership = _ownerships[curr]; + if (!ownership.burned) { + if (ownership.addr != address(0)) { + return ownership; + } + // Invariant: + // There will always be an ownership that has an address and is not burned + // before an ownership that does not have an address and is not burned. + // Hence, curr will not underflow. + while (true) { + curr--; + ownership = _ownerships[curr]; + if (ownership.addr != address(0)) { + return ownership; + } + } + } + } + } + revert OwnerQueryForNonexistentToken(); + } + + /** + * @dev See {IERC721-ownerOf}. + */ + function ownerOf(uint256 tokenId) public view override returns (address) { + return ownershipOf(tokenId).addr; + } + + /** + * @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; + } + + /** + * @dev See {IERC721Metadata-tokenURI}. + */ + function tokenURI(uint256 tokenId) + public + view + virtual + override + returns (string memory) + { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _baseURI(); + return + bytes(baseURI).length != 0 + ? string(abi.encodePacked(baseURI, tokenId.toString())) + : ""; + } + + /** + * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each + * token will be the concatenation of the `baseURI` and the `tokenId`. Empty + * by default, can be overriden in child contracts. + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } + + /** + * @dev See {IERC721-approve}. + */ + function approve(address to, uint256 tokenId) public override { + address owner = HRC721.ownerOf(tokenId); + if (to == owner) revert ApprovalToCurrentOwner(); + + if (_msgSender() != owner && !isApprovedForAll(owner, _msgSender())) { + revert ApprovalCallerNotOwnerNorApproved(); + } + + _approve(to, tokenId, owner); + } + + /** + * @dev See {IERC721-getApproved}. + */ + function getApproved(uint256 tokenId) + public + view + override + returns (address) + { + if (!_exists(tokenId)) revert ApprovalQueryForNonexistentToken(); + + return _tokenApprovals[tokenId]; + } + + /** + * @dev See {IERC721-setApprovalForAll}. + */ + function setApprovalForAll(address operator, bool approved) + public + override + { + if (operator == _msgSender()) revert ApproveToCaller(); + + _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 { + _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 { + _transfer(from, to, tokenId); + if (!_checkOnERC721Received(from, to, tokenId, _data)) { + revert TransferToNonERC721ReceiverImplementer(); + } + } + + /** + * @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`), + */ + function _exists(uint256 tokenId) internal view returns (bool) { + return tokenId < _currentIndex && !_ownerships[tokenId].burned; + } + + function _safeMint(address to, uint256 quantity) internal { + _safeMint(to, quantity, ""); + } + + /** + * @dev Safely mints `quantity` tokens and transfers them to `to`. + * + * Requirements: + * + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called for each safe transfer. + * - `quantity` must be greater than 0. + * + * Emits a {Transfer} event. + */ + function _safeMint( + address to, + uint256 quantity, + bytes memory _data + ) internal { + _mint(to, quantity, _data, true); + } + + /** + * @dev Mints `quantity` tokens and transfers them to `to`. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `quantity` must be greater than 0. + * + * Emits a {Transfer} event. + */ + function _mint( + address to, + uint256 quantity, + bytes memory _data, + bool safe + ) internal { + uint256 startTokenId = _currentIndex; + if (to == address(0)) revert MintToZeroAddress(); + if (quantity == 0) revert MintZeroQuantity(); + + _beforeTokenTransfers(address(0), to, startTokenId, quantity); + + // Overflows are incredibly unrealistic. + // balance or numberMinted overflow if current value of either + quantity > 3.4e38 (2**128) - 1 + // updatedIndex overflows if _currentIndex + quantity > 3.4e38 (2**128) - 1 + unchecked { + _addressData[to].balance += uint64(quantity); + _addressData[to].numberMinted += uint64(quantity); + + _ownerships[startTokenId].addr = to; + _ownerships[startTokenId].startTimestamp = uint64(block.timestamp); + + uint256 updatedIndex = startTokenId; + + for (uint256 i; i < quantity; i++) { + emit Transfer(address(0), to, updatedIndex); + if ( + safe && + !_checkOnERC721Received(address(0), to, updatedIndex, _data) + ) { + revert TransferToNonERC721ReceiverImplementer(); + } + updatedIndex++; + } + + _currentIndex = uint128(updatedIndex); + } + _afterTokenTransfers(address(0), to, startTokenId, quantity); + } + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * + * 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 + ) private { + TokenOwnership memory prevOwnership = ownershipOf(tokenId); + + bool isApprovedOrOwner = (_msgSender() == prevOwnership.addr || + isApprovedForAll(prevOwnership.addr, _msgSender()) || + getApproved(tokenId) == _msgSender()); + + if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved(); + if (prevOwnership.addr != from) revert TransferFromIncorrectOwner(); + if (to == address(0)) revert TransferToZeroAddress(); + + _beforeTokenTransfers(from, to, tokenId, 1); + + // Clear approvals from the previous owner + _approve(address(0), tokenId, prevOwnership.addr); + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as tokenId would have to be 2**128. + unchecked { + _addressData[from].balance -= 1; + _addressData[to].balance += 1; + + _ownerships[tokenId].addr = to; + _ownerships[tokenId].startTimestamp = uint64(block.timestamp); + + // If the ownership slot of tokenId+1 is not explicitly set, that means the transfer initiator owns it. + // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. + uint256 nextTokenId = tokenId + 1; + if (_ownerships[nextTokenId].addr == address(0)) { + // This will suffice for checking _exists(nextTokenId), + // as a burned slot cannot contain the zero address. + if (nextTokenId < _currentIndex) { + _ownerships[nextTokenId].addr = prevOwnership.addr; + _ownerships[nextTokenId].startTimestamp = prevOwnership + .startTimestamp; + } + } + } + + emit Transfer(from, to, tokenId); + _afterTokenTransfers(from, to, tokenId, 1); + } + + /** + * @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 { + TokenOwnership memory prevOwnership = ownershipOf(tokenId); + + _beforeTokenTransfers(prevOwnership.addr, address(0), tokenId, 1); + + // Clear approvals from the previous owner + _approve(address(0), tokenId, prevOwnership.addr); + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as tokenId would have to be 2**128. + unchecked { + _addressData[prevOwnership.addr].balance -= 1; + _addressData[prevOwnership.addr].numberBurned += 1; + + // Keep track of who burned the token, and the timestamp of burning. + _ownerships[tokenId].addr = prevOwnership.addr; + _ownerships[tokenId].startTimestamp = uint64(block.timestamp); + _ownerships[tokenId].burned = true; + + // If the ownership slot of tokenId+1 is not explicitly set, that means the burn initiator owns it. + // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. + uint256 nextTokenId = tokenId + 1; + if (_ownerships[nextTokenId].addr == address(0)) { + // This will suffice for checking _exists(nextTokenId), + // as a burned slot cannot contain the zero address. + if (nextTokenId < _currentIndex) { + _ownerships[nextTokenId].addr = prevOwnership.addr; + _ownerships[nextTokenId].startTimestamp = prevOwnership + .startTimestamp; + } + } + } + + emit Transfer(prevOwnership.addr, address(0), tokenId); + _afterTokenTransfers(prevOwnership.addr, address(0), tokenId, 1); + + // Overflow not possible, as _burnCounter cannot be exceed _currentIndex times. + unchecked { + _burnCounter++; + } + } + + /** + * @dev Approve `to` to operate on `tokenId` + * + * Emits a {Approval} event. + */ + function _approve( + address to, + uint256 tokenId, + address owner + ) private { + _tokenApprovals[tokenId] = to; + emit Approval(owner, 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 TransferToNonERC721ReceiverImplementer(); + } else { + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } else { + return true; + } + } + + /** + * @dev Hook that is called before a set of serially-ordered token ids are about to be transferred. This includes minting. + * And also called before burning one token. + * + * startTokenId - the first token id to be transferred + * quantity - the amount to be transferred + * + * 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, `tokenId` will be burned by `from`. + * - `from` and `to` are never both zero. + */ + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual {} + + /** + * @dev Hook that is called after a set of serially-ordered token ids have been transferred. This includes + * minting. + * And also called after one token has been burned. + * + * startTokenId - the first token id to be transferred + * quantity - the amount to be transferred + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` has been + * transferred to `to`. + * - When `from` is zero, `tokenId` has been minted for `to`. + * - When `to` is zero, `tokenId` has been burned by `from`. + * - `from` and `to` are never both zero. + */ + function _afterTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual {} +} diff --git a/miniwallet/contracts/nft/HRC721/extensions/HRC721Burnable.sol b/miniwallet/contracts/nft/HRC721/extensions/HRC721Burnable.sol new file mode 100644 index 0000000..ccd031c --- /dev/null +++ b/miniwallet/contracts/nft/HRC721/extensions/HRC721Burnable.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Creator: Chiru Labs +// Sep 1st 2022, Modification for MiniWallet by John Whitton + +pragma solidity ^0.8.4; + +import "../HRC721.sol"; +import "@openzeppelin/contracts/utils/Context.sol"; + +/** + * @title HRC721 Burnable Token + * @dev HRC721 Token that can be irreversibly burned (destroyed). + */ +abstract contract HRC721Burnable is Context, HRC721 { + /** + * @dev Burns `tokenId`. See {HRC721-_burn}. + * + * Requirements: + * + * - The caller must own `tokenId` or be an approved operator. + */ + function burn(uint256 tokenId) public virtual { + TokenOwnership memory prevOwnership = ownershipOf(tokenId); + + bool isApprovedOrOwner = (_msgSender() == prevOwnership.addr || + isApprovedForAll(prevOwnership.addr, _msgSender()) || + getApproved(tokenId) == _msgSender()); + + if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved(); + + _burn(tokenId); + } +} diff --git a/miniwallet/contracts/nft/HRC721/extensions/HRC721OwnersExplicit.sol b/miniwallet/contracts/nft/HRC721/extensions/HRC721OwnersExplicit.sol new file mode 100644 index 0000000..7c2712e --- /dev/null +++ b/miniwallet/contracts/nft/HRC721/extensions/HRC721OwnersExplicit.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Creator: Chiru Labs +// Sep 1st 2022, Modification for MiniWallet by John Whitton + +pragma solidity ^0.8.4; + +import "../HRC721.sol"; + +error AllOwnershipsHaveBeenSet(); +error QuantityMustBeNonZero(); +error NoTokensMintedYet(); + +abstract contract HRC721OwnersExplicit is HRC721 { + uint256 public nextOwnerToExplicitlySet; + + /** + * @dev Explicitly set `owners` to eliminate loops in future calls of ownerOf(). + */ + function _setOwnersExplicit(uint256 quantity) internal { + if (quantity == 0) revert QuantityMustBeNonZero(); + if (_currentIndex == 0) revert NoTokensMintedYet(); + uint256 _nextOwnerToExplicitlySet = nextOwnerToExplicitlySet; + if (_nextOwnerToExplicitlySet >= _currentIndex) + revert AllOwnershipsHaveBeenSet(); + + // Index underflow is impossible. + // Counter or index overflow is incredibly unrealistic. + unchecked { + uint256 endIndex = _nextOwnerToExplicitlySet + quantity - 1; + + // Set the end index to be the last token index + if (endIndex + 1 > _currentIndex) { + endIndex = _currentIndex - 1; + } + + for (uint256 i = _nextOwnerToExplicitlySet; i <= endIndex; i++) { + if ( + _ownerships[i].addr == address(0) && !_ownerships[i].burned + ) { + TokenOwnership memory ownership = ownershipOf(i); + _ownerships[i].addr = ownership.addr; + _ownerships[i].startTimestamp = ownership.startTimestamp; + } + } + + nextOwnerToExplicitlySet = endIndex + 1; + } + } +} diff --git a/miniwallet/contracts/nft/Mini1155.sol b/miniwallet/contracts/nft/Mini1155.sol new file mode 100644 index 0000000..5f78462 --- /dev/null +++ b/miniwallet/contracts/nft/Mini1155.sol @@ -0,0 +1,419 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/security/Pausable.sol"; +import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol"; +import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol"; +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "./rarible/royalties/contracts/impl/RoyaltiesV2Impl.sol"; +import "./rarible/royalties/contracts/LibPart.sol"; +import "./rarible/royalties/contracts/LibRoyaltiesV2.sol"; + +/// Developed by: John Whitton (github: johnwhittton), Aaron Li (github: polymorpher) +contract Mini1155 is + ERC1155, + Ownable, + Pausable, + ERC1155Burnable, + ERC1155Supply, + RoyaltiesV2Impl +{ + // Contract logic variables + string public contractURI; + string public baseUri; + bytes32 public salt; + uint256 public mintPrice; + uint256 public maxPerMint; + uint256 public standardTokenId; + uint256 public rareTokenId; + uint256 public exchangeRatio; // # standard needed to get 1 rare + uint256 public rareProbabilityPercentage; // chance to get rare token during minting + + // Contract admin variables + address public revenueAccount; + string public name; + string public symbol; + bool public saleIsActive; + bool public saleStarted; + bool public metadataFrozen; + + // Token specific public variables + mapping(uint256 => uint256) public maxSupply; + mapping(uint256 => uint256) public maxPersonalCap; + mapping(uint256 => string) public metadataUris; + + bytes4 private constant _INTERFACE_ID_ERC2981 = 0x2a55205a; + + event SetBaseUri(string baseUri); + event Mini1155Mint( + uint256 standardTokenId, + uint256 numStandardTokens, + uint256 rareTokenId, + uint256 numRareTokens, + address initialOwner + ); + event Mini1155MintCommunity( + uint256 tokenId, + uint256 numTokens, + address initialOwner + ); + event Mini1155Transfer( + uint256 id, + address from, + address to, + address operator + ); + + constructor( + bool _saleIsActive, + bool _metadataFrozen, + uint256 _mintPrice, + uint256 _maxPerMint, + uint256 _standardTokenId, + uint256 _rareTokenId, + uint256 _exchangeRatio, + uint256 _rareProbabilityPercentage, + bytes32 _salt, + string memory _baseUri, + string memory _contractUri + ) ERC1155(_baseUri) { + saleIsActive = _saleIsActive; + if (saleIsActive) { + saleStarted = true; + } + metadataFrozen = _metadataFrozen; + mintPrice = _mintPrice; + maxPerMint = _maxPerMint; + standardTokenId = _standardTokenId; + rareTokenId = _rareTokenId; + exchangeRatio = _exchangeRatio; + rareProbabilityPercentage = _rareProbabilityPercentage; + salt = _salt; + contractURI = _contractUri; + baseUri = _baseUri; + } + + function setURI(string memory newuri) public onlyOwner { + _setURI(newuri); + } + + function pause() public onlyOwner { + _pause(); + } + + function unpause() public onlyOwner { + _unpause(); + } + + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal override(ERC1155, ERC1155Supply) whenNotPaused { + super._beforeTokenTransfer(operator, from, to, ids, amounts, data); + } + + // Begin Mini1155 Enhancements + modifier whenSaleActive() { + require(saleIsActive, "sale not active"); + _; + } + + modifier whenMetadataNotFrozen() { + require(!metadataFrozen, "metadata frozen"); + _; + } + + function supportsInterface(bytes4 interfaceId) + public + view + override + returns (bool) + { + return + interfaceId == this.name.selector || + interfaceId == this.symbol.selector || + interfaceId == LibRoyaltiesV2._INTERFACE_ID_ROYALTIES || + interfaceId == _INTERFACE_ID_ERC2981 || + super.supportsInterface(interfaceId); + } + + function mint(uint256 _amount) external payable whenSaleActive { + require(_amount > 0, "minting too few"); + require(_amount <= maxPerMint, "exceeded per mint limit"); + require(mintPrice * _amount <= msg.value, "insufficient payment"); + uint256 excess = msg.value - (_amount * mintPrice); + if (excess > 0) { + payable(msg.sender).transfer(excess); + } + + bool isRare = false; + uint256 standardBalance = ERC1155.balanceOf( + msg.sender, + standardTokenId + ); + uint256 rareBalance = ERC1155.balanceOf(msg.sender, rareTokenId); + uint256 rareSupply = ERC1155Supply.totalSupply(rareTokenId); + if ( + rareSupply < maxSupply[rareTokenId] && + rareBalance < maxPersonalCap[rareTokenId] + ) { + uint256 roll = uint256( + keccak256( + bytes.concat( + salt, + bytes20(msg.sender), + bytes32(standardBalance), + bytes32(rareBalance) + ) + ) + ) % 100; + if (roll < (rareProbabilityPercentage * _amount)) { + isRare = true; + } + } + if (isRare) { + _amount -= 1; + _mint(msg.sender, rareTokenId, 1, ""); + } + if (_amount > 0) { + require( + totalSupply(standardTokenId) + _amount <= + maxSupply[standardTokenId], + "standard token supply cap exceeded" + ); + require( + standardBalance + _amount <= maxPersonalCap[standardTokenId], + "standard token personal cap exceeded" + ); + _mint(msg.sender, standardTokenId, _amount, ""); + } + emit Mini1155Mint( + standardTokenId, + _amount, + rareTokenId, + isRare ? 1 : 0, + msg.sender + ); + } + + function exchange() public { + require(exchangeRatio > 0, "exchange not enabled"); + uint256 standardBalance = ERC1155.balanceOf( + msg.sender, + standardTokenId + ); + require(standardBalance >= exchangeRatio, "too few standard tokens"); + _burn(msg.sender, standardTokenId, exchangeRatio); + _mint(msg.sender, rareTokenId, 1, ""); + } + + function uri(uint256 id) public view override returns (string memory) { + if (bytes(metadataUris[id]).length == 0) { + return string(abi.encodePacked(baseUri, uint2str(id), ".json")); + } + return metadataUris[id]; + } + + // ------------------ + // Functions for the owner (MiniWallet minting contracts) + // ------------------ + + // Explicit Overrides + function burn( + address _address, + uint256 _tokenId, + uint256 _amount + ) public override(ERC1155Burnable) onlyOwner { + ERC1155Burnable.burn(_address, _tokenId, _amount); + } + + function burnBatch( + address _address, + uint256[] memory _tokenIds, + uint256[] memory _amounts + ) public override(ERC1155Burnable) onlyOwner { + ERC1155Burnable.burnBatch(_address, _tokenIds, _amounts); + } + + function freezeMetadata() external onlyOwner whenMetadataNotFrozen { + metadataFrozen = true; + } + + function toggleSaleState() external onlyOwner { + // require ((saleIsActive || (offsetValue != 0)), "cannot start sale until airdrop is complete and offset set"); + saleIsActive = !saleIsActive; + if (saleIsActive && !saleStarted) { + saleStarted = true; + } + } + + function setContractUri(string memory uri_) + public + whenMetadataNotFrozen + onlyOwner + { + contractURI = uri_; + } + + function setMaxPerMint(uint256 _maxPerMint) external onlyOwner { + maxPerMint = _maxPerMint; + } + + function setMintPrice(uint256 _mintPrice) external onlyOwner { + mintPrice = _mintPrice; + } + + function setMaxSupply(uint256 _tokenId, uint256 _cap) external onlyOwner { + maxSupply[_tokenId] = _cap; + } + + function setMaxPersonalCap(uint256 _tokenId, uint256 _cap) + external + onlyOwner + { + maxPersonalCap[_tokenId] = _cap; + } + + function setStandardTokenId(uint256 _tokenId) external onlyOwner { + standardTokenId = _tokenId; + } + + function setRareTokenId(uint256 _tokenId) external onlyOwner { + rareTokenId = _tokenId; + } + + function setExchangeRatio(uint256 _exchangeRatio) external onlyOwner { + exchangeRatio = _exchangeRatio; + } + + function setRareProbabilityPercentage(uint256 _rareProbabilityPercentage) + external + onlyOwner + { + rareProbabilityPercentage = _rareProbabilityPercentage; + } + + function setBaseUri(string memory _baseUri) + external + onlyOwner + whenMetadataNotFrozen + { + baseUri = _baseUri; + emit SetBaseUri(baseUri); + } + + function mintAsOwner( + address _to, + uint256 _tokenId, + uint256 _numberOfTokens + ) external onlyOwner { + require(_to != address(0), "zero to-address"); + if (maxSupply[_tokenId] > 0) { + require( + totalSupply(_tokenId) + _numberOfTokens <= maxSupply[_tokenId], + "supply exceeded" + ); + } + _mint(_to, _tokenId, _numberOfTokens, ""); + emit Mini1155MintCommunity(_tokenId, _numberOfTokens, _to); + } + + function withdraw(uint256 amount, bool shouldUseRevenueAccount) public { + require( + msg.sender == Ownable.owner() || msg.sender == revenueAccount, + "unauthorized" + ); + address a = shouldUseRevenueAccount ? revenueAccount : Ownable.owner(); + (bool success, ) = a.call{value: amount}(""); + require(success); + } + + function setUri(uint256 id, string memory uri_) + public + onlyOwner + whenMetadataNotFrozen + { + metadataUris[id] = uri_; + } + + function uint2str(uint256 _i) + internal + pure + returns (string memory _uintAsString) + { + if (_i == 0) { + return "0"; + } + uint256 j = _i; + uint256 len; + while (j != 0) { + len++; + j /= 10; + } + bytes memory bstr = new bytes(len); + uint256 k = len; + while (_i != 0) { + k = k - 1; + uint8 temp = (48 + uint8(_i - (_i / 10) * 10)); + bytes1 b1 = bytes1(temp); + bstr[k] = b1; + _i /= 10; + } + return string(bstr); + } + + function setRevenueAccount(address account) public onlyOwner { + revenueAccount = account; + } + + function setSalt(bytes32 _salt) public onlyOwner { + salt = _salt; + } + + function setNameSymbol(string memory name_, string memory symbol_) + public + onlyOwner + { + name = name_; + symbol = symbol_; + } + + function setRoyalties( + uint256 _tokenId, + address payable _royaltiesReceipientAddress, + uint96 _percentageBasisPoints + ) public onlyOwner { + LibPart.Part[] memory _royalties = new LibPart.Part[](1); + _royalties[0].value = _percentageBasisPoints; + _royalties[0].account = _royaltiesReceipientAddress; + _saveRoyalties(_tokenId, _royalties); + } + + // royalty stuff + + function royaltyInfo(uint256 _tokenId, uint256 _salePrice) + external + view + returns (address receiver, uint256 royaltyAmount) + { + LibPart.Part[] memory _royalties = royalties[_tokenId]; + if (_royalties.length > 0) { + return ( + _royalties[0].account, + (_salePrice * _royalties[0].value) / 10000 + ); + } + return (address(0), 0); + } + + // accept deposit + + receive() external payable {} +} diff --git a/miniwallet/contracts/nft/Mini721.sol b/miniwallet/contracts/nft/Mini721.sol new file mode 100644 index 0000000..2bd601c --- /dev/null +++ b/miniwallet/contracts/nft/Mini721.sol @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.4; + +import "./HRC721/HRC721.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./rarible/royalties/contracts/impl/RoyaltiesV2Impl.sol"; +import "./rarible/royalties/contracts/LibPart.sol"; +import "./rarible/royalties/contracts/LibRoyaltiesV2.sol"; + +contract Mini721 is HRC721, Ownable, RoyaltiesV2Impl { + bytes32 internal salt; + uint256 public maxMiniTokens; + uint256 public mintPrice; + uint256 public maxPerMint; + uint256 public startIndex; + + string public provenanceHash = ""; + uint256 public offsetValue; + + bool public metadataFrozen; + bool public provenanceFrozen; + bool public saleIsActive; + bool public saleStarted; + + mapping(uint256 => string) internal metadataUris; + string internal _contractUri; + string public temporaryTokenUri; + string internal baseUri; + address internal revenueAccount; + + bytes4 private constant _INTERFACE_ID_ERC2981 = 0x2a55205a; + + event SetBaseUri(string baseUri); + event SetStartIndex(uint256 index); + event MiniMint( + uint256 lastTokenId, + uint256 numTokens, + address initialOwner + ); + event MiniMintCommunity( + uint256 lastTokenId, + uint256 numTokens, + address initialOwner + ); + event MiniBurn(uint256 id); + event MiniBatchBurn(uint256[] ids); + event MiniTransfer(uint256 id, address from, address to, address operator); + event MiniSetup( + uint32 coolingPeriod_, + uint32 shipNumber_, + string contractUri + ); + + constructor( + bool _saleIsActive, + bool _metadataFrozen, + bool _provenanceFrozen, + uint256 _maxMiniTokens, + uint256 _mintPrice, + uint256 _maxPerMint, + string memory _baseUri, + string memory contractUri_ + ) HRC721("MiniWallet NFT", "Mini721") { + saleIsActive = _saleIsActive; + if (saleIsActive) { + saleStarted = true; + } + // false + metadataFrozen = _metadataFrozen; + //false + provenanceFrozen = _provenanceFrozen; + //false + maxMiniTokens = _maxMiniTokens; + // 10000 + mintPrice = _mintPrice; + // 100000000000000000 = 0.01 ETH + maxPerMint = _maxPerMint; + // 10; + baseUri = _baseUri; + // "ipfs://QmPcY4yVQu4J2z3ztHWziWkoUEugpzdfftbGH8xD49DvRx/"; + _contractUri = contractUri_; + //"ipfs://Qmf8WkAVZtkBwngG4mTrPk23vDd6z8dZW2UshV9ywWGyB9/contract.json"; //TODO review URI + } + + modifier whenSaleActive() { + require(saleIsActive, "Mini721: Sale is not active"); + _; + } + + modifier whenMetadataNotFrozen() { + require(!metadataFrozen, "Mini721: Metadata is frozen"); + _; + } + + modifier whenProvenanceNotFrozen() { + require(!provenanceFrozen, "Mini721: Provenance is frozen"); + _; + } + + // ------------------ + // Explicit overrides + // ------------------ + + function _burn(uint256 tokenId) internal virtual override(HRC721) { + super._burn(tokenId); + } + + function setTemporaryTokenUri(string memory uri) public onlyOwner { + temporaryTokenUri = uri; + } + + function tokenURI(uint256 tokenId) + public + view + virtual + override(HRC721) + returns (string memory) + { + if (!metadataFrozen && bytes(temporaryTokenUri).length > 0) { + return temporaryTokenUri; + } + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + uint256 tid = tokenId; + if (tid >= offsetValue) { + tid = + ((startIndex + tid - offsetValue) % + (maxMiniTokens - offsetValue)) + + offsetValue; + } + + if (bytes(metadataUris[tokenId]).length == 0) { + return + bytes(baseUri).length != 0 + ? string(abi.encodePacked(baseUri, uint2str(tid))) + : ""; + } + return metadataUris[tokenId]; + } + + function setStartIndex() external onlyOwner { + startIndex = + uint256( + keccak256( + abi.encodePacked( + blockhash(block.number - 2), + bytes20(msg.sender), + bytes32(totalSupply()) + ) + ) + ) % + (maxMiniTokens - offsetValue); + emit SetStartIndex(startIndex); + } + + function _baseURI() internal view override returns (string memory) { + return baseUri; + } + + function supportsInterface(bytes4 interfaceId) + public + view + override + returns (bool) + { + return + interfaceId == this.name.selector || + interfaceId == this.symbol.selector || + interfaceId == LibRoyaltiesV2._INTERFACE_ID_ROYALTIES || + interfaceId == _INTERFACE_ID_ERC2981 || + HRC721.supportsInterface(interfaceId); + } + + // ------------------ + // Utility view functions + // ------------------ + + function exists(uint256 _tokenId) public view returns (bool) { + return _exists(_tokenId); + } + + //TODO review if we need to override the contractURI + function contractURI() public view returns (string memory) { + return _contractUri; + } + + // ------------------ + // Functions for external (user) minting + // ------------------ + + function mintMini(uint256 amount) external payable whenSaleActive { + require( + totalSupply() + amount < maxMiniTokens, + "Mini721: Purchase would exceed cap" + ); + require(amount <= maxPerMint, "Mini721: Amount exceeds max per mint"); + require( + mintPrice * amount <= msg.value, + "Mini721: Ether value sent is not correct" + ); + uint256 excess = msg.value - (amount * mintPrice); + if (excess > 0) { + payable(msg.sender).transfer(excess); + } + _safeMint(msg.sender, amount); + emit MiniMint(totalSupply(), amount, msg.sender); + } + + function burn(uint256 id) public onlyOwner whenMetadataNotFrozen { + HRC721._burn(id); + emit MiniBurn(id); + } + + function batchBurn(uint256[] memory ids) + public + onlyOwner + whenMetadataNotFrozen + { + for (uint32 i = 0; i < ids.length; i++) { + uint256 id = ids[i]; + HRC721._burn(id); + } + emit MiniBatchBurn(ids); + } + + // ------------------ + // Functions for the owner (Mini minting contracts) + // ------------------ + + function freezeMetadata() external onlyOwner whenMetadataNotFrozen { + metadataFrozen = true; + } + + function freezeProvenance() external onlyOwner whenProvenanceNotFrozen { + provenanceFrozen = true; + } + + function toggleSaleState() external onlyOwner { + require( + (saleIsActive || (offsetValue != 0)), + "cannot start sale until airdrop is complete and offset set" + ); + saleIsActive = !saleIsActive; + if (saleIsActive && !saleStarted) { + saleStarted = true; + } + } + + function setContractUri(string memory uri_) public onlyOwner { + _contractUri = uri_; + } + + function setProvenanceHash(string memory _provenanceHash) + external + onlyOwner + whenProvenanceNotFrozen + { + provenanceHash = _provenanceHash; + } + + function setOffsetValue(uint256 _offsetValue) external onlyOwner { + require(!saleStarted, "sale already begun"); + offsetValue = _offsetValue; + } + + function setMaxPerMint(uint256 _maxPerMint) external onlyOwner { + maxPerMint = _maxPerMint; + } + + function setMintPrice(uint256 _mintPrice) external onlyOwner { + mintPrice = _mintPrice; + } + + function setBaseUri(string memory _baseUri) + external + onlyOwner + whenMetadataNotFrozen + { + baseUri = _baseUri; + emit SetBaseUri(baseUri); + } + + function mintForCommunity(address _to, uint256 _numberOfTokens) + external + onlyOwner + { + require(_to != address(0), "Mini721: Cannot mint to zero address."); + require( + totalSupply() + _numberOfTokens < maxMiniTokens, + "Mini721: Minting would exceed cap" + ); + _safeMint(_to, _numberOfTokens); + emit MiniMintCommunity(totalSupply(), _numberOfTokens, _to); + } + + function withdraw(uint256 amount, bool shouldUseRevenueAccount) public { + require( + msg.sender == Ownable.owner() || msg.sender == revenueAccount, + "unauthorized" + ); + address a = shouldUseRevenueAccount ? revenueAccount : Ownable.owner(); + (bool success, ) = a.call{value: amount}(""); + require(success); + } + + function setUri(uint256 id, string memory uri_) + public + onlyOwner + whenMetadataNotFrozen + { + metadataUris[id] = uri_; + } + + function uint2str(uint256 _i) + internal + pure + returns (string memory _uintAsString) + { + if (_i == 0) { + return "0"; + } + uint256 j = _i; + uint256 len; + while (j != 0) { + len++; + j /= 10; + } + bytes memory bstr = new bytes(len); + uint256 k = len; + while (_i != 0) { + k = k - 1; + uint8 temp = (48 + uint8(_i - (_i / 10) * 10)); + bytes1 b1 = bytes1(temp); + bstr[k] = b1; + _i /= 10; + } + return string(bstr); + } + + function setRevenueAccount(address account) public onlyOwner { + revenueAccount = account; + } + + function setRoyalties( + uint256 _tokenId, + address payable _royaltiesReceipientAddress, + uint96 _percentageBasisPoints + ) public onlyOwner { + LibPart.Part[] memory _royalties = new LibPart.Part[](1); + _royalties[0].value = _percentageBasisPoints; + _royalties[0].account = _royaltiesReceipientAddress; + _saveRoyalties(_tokenId, _royalties); + } + + function royaltyInfo(uint256 _tokenId, uint256 _salePrice) + external + view + returns (address receiver, uint256 royaltyAmount) + { + LibPart.Part[] memory _royalties = royalties[_tokenId]; + if (_royalties.length > 0) { + return ( + _royalties[0].account, + (_salePrice * _royalties[0].value) / 10000 + ); + } + return (address(0), 0); + } + + receive() external payable {} + + // ------------------ + // Utility function for getting the tokens of a certain address + // ------------------ + + function tokensOfOwner(address _owner) + external + view + returns (uint256[] memory) + { + uint256 tokenCount = balanceOf(_owner); + if (tokenCount == 0) { + return new uint256[](0); + } else { + uint256[] memory result = new uint256[](tokenCount); + for (uint256 index; index < tokenCount; index++) { + result[index] = tokenOfOwnerByIndex(_owner, index); + } + return result; + } + } +} diff --git a/miniwallet/contracts/nft/rarible/royalties/contracts/IRoyaltiesProvider.sol b/miniwallet/contracts/nft/rarible/royalties/contracts/IRoyaltiesProvider.sol new file mode 100644 index 0000000..47d7f17 --- /dev/null +++ b/miniwallet/contracts/nft/rarible/royalties/contracts/IRoyaltiesProvider.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; +import "./LibPart.sol"; +interface IRoyaltiesProvider { + function getRoyalties(address token, uint tokenId) external returns (LibPart.Part[] memory); +} diff --git a/miniwallet/contracts/nft/rarible/royalties/contracts/LibPart.sol b/miniwallet/contracts/nft/rarible/royalties/contracts/LibPart.sol new file mode 100644 index 0000000..140a07c --- /dev/null +++ b/miniwallet/contracts/nft/rarible/royalties/contracts/LibPart.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +library LibPart { + bytes32 public constant TYPE_HASH = keccak256("Part(address account,uint96 value)"); + + struct Part { + address payable account; + uint96 value; + } + + function hash(Part memory part) internal pure returns (bytes32) { + return keccak256(abi.encode(TYPE_HASH, part.account, part.value)); + } +} diff --git a/miniwallet/contracts/nft/rarible/royalties/contracts/LibRoyaltiesV2.sol b/miniwallet/contracts/nft/rarible/royalties/contracts/LibRoyaltiesV2.sol new file mode 100644 index 0000000..59438ea --- /dev/null +++ b/miniwallet/contracts/nft/rarible/royalties/contracts/LibRoyaltiesV2.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +library LibRoyaltiesV2 { + /* + * bytes4(keccak256('getRoyalties(LibAsset.AssetType)')) == 0x44c74bcc + */ + bytes4 constant _INTERFACE_ID_ROYALTIES = 0x44c74bcc; +} diff --git a/miniwallet/contracts/nft/rarible/royalties/contracts/RoyaltiesV2.sol b/miniwallet/contracts/nft/rarible/royalties/contracts/RoyaltiesV2.sol new file mode 100644 index 0000000..4559d26 --- /dev/null +++ b/miniwallet/contracts/nft/rarible/royalties/contracts/RoyaltiesV2.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "./LibPart.sol"; + +interface RoyaltiesV2 { + event RoyaltiesSet(uint256 tokenId, LibPart.Part[] royalties); + + function getRaribleV2Royalties(uint256 id) external view returns (LibPart.Part[] memory); +} diff --git a/miniwallet/contracts/nft/rarible/royalties/contracts/impl/AbstractRoyalties.sol b/miniwallet/contracts/nft/rarible/royalties/contracts/impl/AbstractRoyalties.sol new file mode 100644 index 0000000..14d7f79 --- /dev/null +++ b/miniwallet/contracts/nft/rarible/royalties/contracts/impl/AbstractRoyalties.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "../LibPart.sol"; + +abstract contract AbstractRoyalties { + mapping (uint256 => LibPart.Part[]) public royalties; + + function _saveRoyalties(uint256 _id, LibPart.Part[] memory _royalties) internal { + for (uint i = 0; i < _royalties.length; i++) { + require(_royalties[i].account != address(0x0), "Recipient should be present"); + require(_royalties[i].value != 0, "Royalty value should be positive"); + royalties[_id].push(_royalties[i]); + } + _onRoyaltiesSet(_id, _royalties); + } + + function _updateAccount(uint256 _id, address _from, address _to) internal { + uint length = royalties[_id].length; + for(uint i = 0; i < length; i++) { + if (royalties[_id][i].account == _from) { + royalties[_id][i].account = payable(address(uint160(_to))); + } + } + } + + function _onRoyaltiesSet(uint256 _id, LibPart.Part[] memory _royalties) virtual internal; +} diff --git a/miniwallet/contracts/nft/rarible/royalties/contracts/impl/RoyaltiesV2Impl.sol b/miniwallet/contracts/nft/rarible/royalties/contracts/impl/RoyaltiesV2Impl.sol new file mode 100644 index 0000000..932b961 --- /dev/null +++ b/miniwallet/contracts/nft/rarible/royalties/contracts/impl/RoyaltiesV2Impl.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "./AbstractRoyalties.sol"; +import "../RoyaltiesV2.sol"; + +contract RoyaltiesV2Impl is AbstractRoyalties, RoyaltiesV2 { + function getRaribleV2Royalties(uint256 id) override external view returns (LibPart.Part[] memory) { + return royalties[id]; + } + + function _onRoyaltiesSet(uint256 _id, LibPart.Part[] memory _royalties) override internal { + emit RoyaltiesSet(_id, _royalties); + } +} diff --git a/miniwallet/hardhat.config.ts b/miniwallet/hardhat.config.ts index b1fd4eb..9cce9d1 100644 --- a/miniwallet/hardhat.config.ts +++ b/miniwallet/hardhat.config.ts @@ -53,6 +53,9 @@ const hardhatUserconfig: HardhatUserConfig = { defaultNetwork: 'hardhat', networks: { hardhat: { + accounts: { + count: 200 + }, mining: { auto: true } diff --git a/miniwallet/test/Mini1155.ts b/miniwallet/test/Mini1155.ts new file mode 100644 index 0000000..ece2a66 --- /dev/null +++ b/miniwallet/test/Mini1155.ts @@ -0,0 +1,597 @@ +import { expect } from 'chai' +import { ethers, network } from 'hardhat' + +import { mini1155Mint, mini1155SafeTransferFrom, mini1155configure } from './utilities' + +export const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000' + +// Deployment configuration +const d = { + name: 'Mini Wallet', + symbol: 'Mini1155', + salt: ethers.utils.formatBytes32String('1'), + baseUri: 'ipfs://QmPcY4yVQu4J2z3ztHWziWkoUEugpzdfftbGH8xD49DvRx/', + contractUri: 'ipfs://QmdKB6d1zT7R8dNmEQzc6N1m5p2LDJZ66Hzu8F4fGhdVrq', + revenueAccount: '0xa636a88102a7821b7e42292a4A3920A25a5d49b5', + saleIsActive: false, + metadataFrozen: false, + mintPrice: ethers.utils.parseEther('0.042'), + exchangeRatio: 0, // will be 10 after sale + rareProbabilityPercentage: 1, + maxPerMint: 10, + // standard token configuration + s: { + tokenId: 1, + maxSupply: 770, + personalCap: 10 + }, + // rare token configuration + r: { + tokenId: 2, + maxSupply: 7, // will be 84 after Sale + personalCap: 1 + } +} + +// Collection 1 configuration +const c1 = { + revenueAccount: '0xa636a88102a7821b7e42292a4A3920A25a5d49b5', + maxPerMint: 10, + mintPrice: ethers.utils.parseEther('0.042'), + exchangeRatio: 0, // will be 20 after sale + rareProbabilityPercentage: 1, + // standard token configuration + s: { + tokenId: 1, + maxSupply: 770, + personalCap: 10 + }, + // rare token configuration + r: { + tokenId: 2, + maxSupply: 7, // will be 84 after Sale + personalCap: 1 + } +} + +// Collection 2 configuration +const c2 = { + revenueAccount: '0xa636a88102a7821b7e42292a4A3920A25a5d49b5', + maxPerMint: 20, + mintPrice: ethers.utils.parseEther('0.084'), + exchangeRatio: 0, + rareProbabilityPercentage: 2, + // standard token configuration + s: { + tokenId: 3, + maxSupply: 1540, + personalCap: 20 + }, + // rare token configuration + r: { + tokenId: 4, + maxSupply: 28, // will be 168 after sale + personalCap: 2 + } +} +describe('Mini1155', function () { + before(async function (this) { + this.signers = await ethers.getSigners() + this.deployer = this.signers[0] + this.royalties = this.signers[1] + this.minter = this.signers[2] + this.alice = this.signers[3] + this.bob = this.signers[4] + this.carol = this.signers[5] + this.Mini1155 = await ethers.getContractFactory('Mini1155') + }) + + beforeEach(async function (this) { + this.snapshotId = await ethers.provider.send('evm_snapshot', []) + const Mini1155 = await ethers.getContractFactory('Mini1155') + this.mini1155 = await Mini1155.deploy( + d.saleIsActive, + d.metadataFrozen, + d.mintPrice, + d.maxPerMint, + d.s.tokenId, + d.r.tokenId, + d.exchangeRatio, + d.rareProbabilityPercentage, + d.salt, + d.baseUri, + d.contractUri + ) + await this.mini1155.deployed() + // set the revenue account + await this.mini1155.setRevenueAccount(d.revenueAccount) + // set the standard and rare maxSupply + await this.mini1155.setMaxSupply(d.s.tokenId, d.s.maxSupply) // standard tokenId (Access Pass) + await this.mini1155.setMaxSupply(d.r.tokenId, d.r.maxSupply) // rare TokenId (Collector Pass) + // set the standard and rare maxPersonalCap + await this.mini1155.setMaxPersonalCap(d.s.tokenId, d.s.personalCap) // standard tokenId (Access Pass) + await this.mini1155.setMaxPersonalCap(d.r.tokenId, d.r.personalCap) // rare TokenId (Collector Pass) + await this.mini1155.setNameSymbol(d.name, d.symbol) + }) + + afterEach(async function (this) { + // console.log(`Reverting Snapshot : ${snapshotId}`); + await network.provider.send('evm_revert', [this.snapshotId]) + }) + + describe('Mini1155Tests', function (this) { + // Deployment Tests + it('Mini1155-1 Deployment Validation', async function () { + expect(await this.mini1155.address).to.equal( + '0x5FbDB2315678afecb367f032d93F642f64180aa3' + ) + // Test View Functions + const owner = await this.mini1155.owner() + const tid = 1 + expect(owner).to.equal(this.deployer.address) + expect(await this.mini1155.balanceOf(owner, tid)).to.equal(0) + expect((await this.mini1155.balanceOfBatch([owner, owner], [1, 2])).map(x => x.toString())).to.deep.equal(['0', '0']) + expect(await this.mini1155.baseUri()).to.equal(d.baseUri) + expect(await this.mini1155.contractURI()).to.equal(d.contractUri) + expect(await this.mini1155.exchangeRatio()).to.equal(d.exchangeRatio) + expect(await this.mini1155.exists(d.s.tokenId)).to.equal(false) + expect(await this.mini1155.getRaribleV2Royalties(1)).to.deep.equal([]) + expect(await this.mini1155.isApprovedForAll(this.alice.address, owner)).to.equal(false) + expect(await this.mini1155.maxPerMint()).to.equal(d.maxPerMint) + expect(await this.mini1155.maxPersonalCap(d.s.tokenId)).to.equal(d.s.personalCap) + expect(await this.mini1155.maxSupply(d.s.tokenId)).to.equal(d.s.maxSupply) + expect(await this.mini1155.metadataFrozen()).to.equal(false) + expect(await this.mini1155.metadataUris(d.s.tokenId)).to.equal('') + expect(await this.mini1155.mintPrice()).to.equal(d.mintPrice) + expect(await this.mini1155.name()).to.equal(d.name) + expect(await this.mini1155.owner()).to.equal(this.deployer.address) + expect(await this.mini1155.paused()).to.equal(false) + expect(await this.mini1155.rareProbabilityPercentage()).to.equal(d.rareProbabilityPercentage) + expect(await this.mini1155.revenueAccount()).to.equal(d.revenueAccount) + // expect(await this.mini1155.royalties(1, 1)).to.equal(0, this.deployer.address) + // expect(await this.mini1155.royaltyInfo(1, 1)).to.equal(this.deployer.address, 0) + expect(await this.mini1155.saleIsActive()).to.equal(d.saleIsActive) + expect(await this.mini1155.saleStarted()).to.equal(false) + expect(await this.mini1155.salt()).to.equal(d.salt) + expect(await this.mini1155.standardTokenId()).to.equal(d.s.tokenId) + expect(await this.mini1155.supportsInterface(0x2a55205a)).to.equal(true) + expect(await this.mini1155.symbol()).to.equal(d.symbol) + expect(await this.mini1155.totalSupply(d.s.tokenId)).to.equal(0) + expect(await this.mini1155.uri(d.s.tokenId)).to.equal('ipfs://QmPcY4yVQu4J2z3ztHWziWkoUEugpzdfftbGH8xD49DvRx/1.json') + }) + + it('Mini1155-2 Configuration Validation', async function () { + // Set Standard and Rare Tokens + // await mini1155configure({ mini1155: this.mini1155, revenueAccount: await this.mini1155.owner() }) + await mini1155configure({ mini1155: this.mini1155, config: c1 }) + // Check all readOnlyFunctions + expect(this.mini1155.address).to.equal( + '0x5FbDB2315678afecb367f032d93F642f64180aa3' + ) + // Test Token Configuration + expect(await this.mini1155.owner()).to.equal(this.deployer.address) + expect(await this.mini1155.metadataFrozen()).to.equal(false) + expect(await this.mini1155.standardTokenId()).to.equal(c1.s.tokenId) + expect(await this.mini1155.maxSupply(c1.s.tokenId)).to.equal(c1.s.maxSupply) + expect(await this.mini1155.maxPersonalCap(c1.s.tokenId)).to.equal(c1.s.personalCap) + expect(await this.mini1155.uri(c1.s.tokenId)).to.equal('ipfs://QmPcY4yVQu4J2z3ztHWziWkoUEugpzdfftbGH8xD49DvRx/1.json') + expect(await this.mini1155.rareTokenId()).to.equal(c1.r.tokenId) + expect(await this.mini1155.maxSupply(c1.r.tokenId)).to.equal(c1.r.maxSupply) + expect(await this.mini1155.maxPersonalCap(c1.r.tokenId)).to.equal(c1.r.personalCap) + expect(await this.mini1155.uri(2)).to.equal('ipfs://QmPcY4yVQu4J2z3ztHWziWkoUEugpzdfftbGH8xD49DvRx/2.json') + // Although configured tokens don't exist till minted + expect(await this.mini1155.exists(c1.s.tokenId)).to.equal(false) + expect(await this.mini1155.exists(c1.r.tokenId)).to.equal(false) + // Test View Functions + const owner = await this.mini1155.owner() + // const tid = 1 + expect(await this.mini1155.balanceOf(owner, c1.s.tokenId)).to.equal(0) + expect((await this.mini1155.balanceOfBatch([owner, owner], [1, 2])).map(x => x.toString())).to.deep.equal(['0', '0']) + expect(await this.mini1155.baseUri()).to.equal('ipfs://QmPcY4yVQu4J2z3ztHWziWkoUEugpzdfftbGH8xD49DvRx/') + expect(await this.mini1155.contractURI()).to.equal('ipfs://QmdKB6d1zT7R8dNmEQzc6N1m5p2LDJZ66Hzu8F4fGhdVrq') + expect(await this.mini1155.exchangeRatio()).to.equal(0) + expect(await this.mini1155.exists(c1.s.tokenId)).to.equal(false) + expect(await this.mini1155.getRaribleV2Royalties(c1.s.tokenId)).to.deep.equal([]) + expect(await this.mini1155.isApprovedForAll(this.alice.address, owner)).to.equal(false) + expect(await this.mini1155.maxPerMint()).to.equal(10) + expect(await this.mini1155.maxPersonalCap(c1.s.tokenId)).to.equal(c1.s.personalCap) + expect(await this.mini1155.maxSupply(c1.s.tokenId)).to.equal(c1.s.maxSupply) + expect(await this.mini1155.metadataFrozen()).to.equal(false) + expect(await this.mini1155.metadataFrozen()).to.equal(false) + expect(await this.mini1155.metadataUris(c1.s.tokenId)).to.equal('') + expect(await this.mini1155.mintPrice()).to.equal(c1.mintPrice) + expect(await this.mini1155.owner()).to.equal(this.deployer.address) + expect(await this.mini1155.paused()).to.equal(false) + expect(await this.mini1155.rareProbabilityPercentage()).to.equal(1) + expect(await this.mini1155.revenueAccount()).to.equal(c1.revenueAccount) + // expect(await this.mini1155.royalties(1, 1)).to.equal(0, this.deployer.address) + // expect(await this.mini1155.royaltyInfo(1, 1)).to.equal(this.deployer.address, 0) + expect(await this.mini1155.saleIsActive()).to.equal(false) + expect(await this.mini1155.saleStarted()).to.equal(false) + expect(await this.mini1155.salt()).to.equal(d.salt) + expect(await this.mini1155.standardTokenId()).to.equal(c1.s.tokenId) + expect(await this.mini1155.supportsInterface(0x2a55205a)).to.equal(true) + expect(await this.mini1155.totalSupply(c1.s.tokenId)).to.equal(0) + expect(await this.mini1155.uri(1)).to.equal('ipfs://QmPcY4yVQu4J2z3ztHWziWkoUEugpzdfftbGH8xD49DvRx/1.json') + }) + + it('Mini1155-3 Sale Validation', async function () { + // await mini1155configure({ mini1155: this.mini1155, revenueAccount: await this.mini1155.owner() }) + await mini1155configure({ mini1155: this.mini1155, config: c1 }) + let numTokens = 1 + // const mintPrice = await this.mini1155.mintPrice() + // Can only mint when sale is active + await expect( + this.mini1155.connect(this.alice).mint(numTokens, { + value: c1.mintPrice.mul(numTokens) + }) + ).to.be.revertedWith('sale not active') + expect(await this.mini1155.exists(c1.s.tokenId)).to.equal(false) + // set sale to active + await this.mini1155.toggleSaleState() + // * Mints a ERC1155 token + await this.mini1155.connect(this.alice).mint(numTokens, { + value: c1.mintPrice.mul(numTokens) + }) + expect(await this.mini1155.exists(c1.s.tokenId)).to.equal(true) + expect(await this.mini1155.totalSupply(c1.s.tokenId)).to.equal(1) + numTokens = 10 + await expect( + this.mini1155.connect(this.alice).mint(numTokens, { + value: c1.mintPrice.mul(numTokens) + }) + ).to.be.revertedWith('standard token personal cap exceeded') + // Validate Sale Changes + expect(await this.mini1155.exists(c1.s.tokenId)).to.equal(true) + expect(await this.mini1155.saleIsActive()).to.equal(true) + expect(await this.mini1155.saleStarted()).to.equal(true) + expect(await this.mini1155.totalSupply(c1.s.tokenId)).to.equal(1) + // Test View Functions + const owner = await this.mini1155.owner() + // const tid = 1 + expect(await this.mini1155.balanceOf(owner, c1.s.tokenId)).to.equal(0) + expect((await this.mini1155.balanceOfBatch([owner, owner], [1, 2])).map(x => x.toString())).to.deep.equal(['0', '0']) + expect(await this.mini1155.baseUri()).to.equal('ipfs://QmPcY4yVQu4J2z3ztHWziWkoUEugpzdfftbGH8xD49DvRx/') + expect(await this.mini1155.contractURI()).to.equal('ipfs://QmdKB6d1zT7R8dNmEQzc6N1m5p2LDJZ66Hzu8F4fGhdVrq') + expect(await this.mini1155.exchangeRatio()).to.equal(0) + expect(await this.mini1155.exists(c1.s.tokenId)).to.equal(true) + expect(await this.mini1155.getRaribleV2Royalties(c1.s.tokenId)).to.deep.equal([]) + expect(await this.mini1155.isApprovedForAll(this.alice.address, owner)).to.equal(false) + expect(await this.mini1155.maxPerMint()).to.equal(10) + expect(await this.mini1155.maxPersonalCap(c1.s.tokenId)).to.equal(c1.s.personalCap) + expect(await this.mini1155.maxSupply(c1.s.tokenId)).to.equal(c1.s.maxSupply) + expect(await this.mini1155.metadataFrozen()).to.equal(false) + expect(await this.mini1155.metadataFrozen()).to.equal(false) + expect(await this.mini1155.metadataUris(c1.s.tokenId)).to.equal('') + expect(await this.mini1155.mintPrice()).to.equal(c1.mintPrice) + expect(await this.mini1155.owner()).to.equal(this.deployer.address) + expect(await this.mini1155.paused()).to.equal(false) + expect(await this.mini1155.rareProbabilityPercentage()).to.equal(1) + expect(await this.mini1155.revenueAccount()).to.equal(c1.revenueAccount) + // expect(await this.mini1155.royalties(1, 1)).to.deep.equal(this.deployer.address, '0') + // expect(await this.mini1155.royaltyInfo(1, 1)).to.deep.equal(this.deployer.address, '0') + expect(await this.mini1155.saleIsActive()).to.equal(true) + expect(await this.mini1155.saleStarted()).to.equal(true) + expect(await this.mini1155.salt()).to.equal(d.salt) + expect(await this.mini1155.standardTokenId()).to.equal(c1.s.tokenId) + expect(await this.mini1155.supportsInterface(0x2a55205a)).to.equal(true) + expect(await this.mini1155.totalSupply(c1.s.tokenId)).to.equal(1) + expect(await this.mini1155.uri(1)).to.equal('ipfs://QmPcY4yVQu4J2z3ztHWziWkoUEugpzdfftbGH8xD49DvRx/1.json') + }) + + it('Mini1155-4 Owner Validation', async function () { + const standardURI = 'ipfs://QmPcY4yVQu4J2z3ztHWziWkoUEugpzdfftbGH8xD49DvRx/3.json' + const rareURI = 'ipfs://QmPcY4yVQu4J2z3ztHWziWkoUEugpzdfftbGH8xD49DvRx/4.json' + const royaltyPercentage = 10 + await mini1155configure({ mini1155: this.mini1155, config: c1 }) + let numTokens = 1 + // set sale to active + await this.mini1155.toggleSaleState() + + // Owner Tests + + // Setting all flags + // set the revenue account + await this.mini1155.setRevenueAccount(c1.revenueAccount) + // configure standard Token + await this.mini1155.setStandardTokenId(c1.s.tokenId) + await this.mini1155.setMaxSupply(c1.s.tokenId, c1.s.maxSupply) + await this.mini1155.setMaxPersonalCap(c1.s.tokenId, c1.s.personalCap) + await this.mini1155.setUri(c1.s.tokenId, standardURI) + // configure rare Token + await this.mini1155.setRareTokenId(c1.r.tokenId) + await this.mini1155.setMaxSupply(c1.r.tokenId, c1.r.maxSupply) + await this.mini1155.setMaxPersonalCap(c1.r.tokenId, c1.r.personalCap) + await this.mini1155.setUri(c1.r.tokenId, rareURI) + await this.mini1155.setRareProbabilityPercentage(c1.rareProbabilityPercentage) + // configure exchange rate + await this.mini1155.setExchangeRatio(c1.exchangeRatio) + + // test other setters + await this.mini1155.setBaseUri(d.baseUri) + await this.mini1155.setContractUri(d.contractUri) + await this.mini1155.setMaxPerMint(c1.maxPerMint) + await this.mini1155.setMintPrice(c1.mintPrice) + await this.mini1155.setRareProbabilityPercentage(c1.rareProbabilityPercentage) + await this.mini1155.setRoyalties(c1.s.tokenId, this.royalties.address, royaltyPercentage) + await this.mini1155.setSalt(d.salt) + await this.mini1155.pause() + await this.mini1155.unpause() + await this.mini1155.freezeMetadata() + await this.mini1155.toggleSaleState() + + // Owner Minting and Burning and Exchange + // "function mintAsOwner(address,uint256,uint256)", + await this.mini1155.toggleSaleState() + expect(await this.mini1155.exists(c1.s.tokenId)).to.equal(false) + await this.mini1155['mintAsOwner(address,uint256,uint256)'](this.deployer.address, c1.s.tokenId, 40) + expect(await this.mini1155.exists(c1.s.tokenId)).to.equal(true) + expect(await this.mini1155.totalSupply(c1.s.tokenId)).to.equal(40) + await this.mini1155['mintAsOwner(address,uint256,uint256)'](this.deployer.address, c1.s.tokenId, 60) + expect(await this.mini1155.totalSupply(c1.s.tokenId)).to.equal(100) + // "function mint(uint256) payable", + numTokens = 11 // amount exceeding mint limit + await expect( + this.mini1155.mint(numTokens) + ).to.be.revertedWith('exceeded per mint limit') + // expect(await this.mini1155.uri(c1.s.tokenId)).to.equal('ipfs://QmPcY4yVQu4J2z3ztHWziWkoUEugpzdfftbGH8xD49DvRx/3.json') + // await this.mini1155.connect(this.alice)['mint(uint256)'](numTokens, { + // value: c1.mintPrice.mul(numTokens) + // }) + // expect(await this.mini1155.exists(c1.s.tokenId)).to.equal(true) + // // expect(await this.mini1155.exists(2)).to.equal(true) + // expect(await this.mini1155.totalSupply(c1.s.tokenId)).to.equal(101) + // numTokens = 5 + // await this.mini1155.connect(this.alice).mint(numTokens, { + // value: c1.mintPrice.mul(numTokens) + // }) + // expect(await this.mini1155.totalSupply(c1.s.tokenId)).to.equal(106) + // expect(await this.mini1155.totalSupply(c1.r.tokenId)).to.equal(0) + // await expect( + // this.mini1155.connect(this.alice)['mint(uint256)'](numTokens, { + // value: c1.mintPrice.mul(numTokens) + // }) + // ).to.be.revertedWith('standard token personal cap exceeded') + // expect(await this.mini1155.totalSupply(2)).to.equal(1) + + // Batch Functions + const owner = await this.mini1155.owner() + const tid1 = 10 + const tid2 = 11 + // TODO "function mintBatch(address,uint256[],uint256[],bytes)", + await this.mini1155.mintAsOwner(owner, tid1, 1000) + await this.mini1155.mintAsOwner(owner, tid2, 100) + expect((await this.mini1155.balanceOfBatch([owner, owner], [tid1, tid2])).map(x => x.toString())).to.deep.equal(['1000', '100']) + await this.mini1155.burnBatch(owner, [tid1, tid2], [100, 10]) + expect((await this.mini1155.balanceOfBatch([owner, owner], [tid1, tid2])).map(x => x.toString())).to.deep.equal(['900', '90']) + await this.mini1155.safeBatchTransferFrom(owner, this.alice.address, [tid1, tid2], [70, 7], '0x') + expect((await this.mini1155.balanceOfBatch([owner, owner, this.alice.address, this.alice.address], [tid1, tid2, tid1, tid2])).map(x => x.toString())).to.deep.equal(['830', '83', '70', '7']) + await this.mini1155.burn(owner, tid1, 200) + expect((await this.mini1155.balanceOfBatch([owner], [tid1])).map(x => x.toString())).to.deep.equal(['630']) + await this.mini1155.safeTransferFrom(owner, this.alice.address, tid1, 100, '0x') + expect((await this.mini1155.balanceOfBatch([owner, this.alice.address], [tid1, tid1])).map(x => x.toString())).to.deep.equal(['530', '170']) + + // Ownership functions + // TODO "function setApprovalForAll(address,bool)", + // TODO "function renounceOwnership()", + // TODO "function transferOwnership(address)", + + // Royalty Functionality + // "function royalties(uint256,uint256) view returns (address, uint96)", + // "function royaltyInfo(uint256,uint256) view returns (address, uint256)", + // "function getRaribleV2Royalties(uint256) view returns (tuple(address,uint96)[])", + + // Withdrawal functionality + // TODO "function withdraw(uint256,bool)" + }) + + it('Mini1155-5 User Validation', async function () { + + }) + + it('Mini1155-6 Negative Use Case Validation', async function () { + // Ensure community can't use owner functions + await expect( + this.mini1155 + .connect(this.alice)['mintAsOwner(address,uint256,uint256)'](this.deployer.address, 1, 500) + ).to.be.revertedWith('Ownable: caller is not the owner') + + // One User should not be able to mint and exchange all the tokens + await mini1155configure({ mini1155: this.mini1155, config: c1 }) + const numTokens = 10 + // set sale to active + await this.mini1155.toggleSaleState() + // Should never have exchangeRatio set when sales is active otherwise users can mint and exchange to exceed personal limits + await this.mini1155.setExchangeRatio(10) + for (let i = 0; i < 77; i++) { + // console.log(`i: ${i}`) + // Mint 10 exchange them for rare and then + await this.mini1155.connect(this.bob).mint(numTokens, { + value: c1.mintPrice.mul(numTokens) + }) + // const standardCount = await this.mini1155.balanceOf(this.bob.address, c1.s.tokenId) + // const rareCount = await this.mini1155.balanceOf(this.bob.address, c1.r.tokenId) + // // console.log(`Bob Standard Token Balance: ${standardCount}`) + // // console.log(`Bob Rare Token Balance: ${rareCount}`) + await this.mini1155.connect(this.bob).exchange() + } + // Validate Sale Changes + expect(await this.mini1155.exists(c1.s.tokenId)).to.equal(false) + expect(await this.mini1155.exists(c1.r.tokenId)).to.equal(true) + expect(await this.mini1155.saleIsActive()).to.equal(true) + expect(await this.mini1155.saleStarted()).to.equal(true) + expect(await this.mini1155.totalSupply(c1.s.tokenId)).to.equal(0) + expect(await this.mini1155.totalSupply(c1.r.tokenId)).to.equal(77) + expect(await this.mini1155.balanceOf(this.bob.address, c1.s.tokenId)).to.equal(0) + expect(await this.mini1155.balanceOf(this.bob.address, c1.r.tokenId)).to.equal(77) + }) + + it('Mini1155-7 Event Validation', async function () { + + }) + + it('Mini1155-8 Second Edition Validation', async function () { + const owner = await this.mini1155.owner() + // Deploy (Owner done in before each) + // Configure Collection 1 (Owner) + await mini1155configure({ mini1155: this.mini1155, config: c1 }) + + // Start Sale (Owner) + await this.mini1155.toggleSaleState() + let contractBalance = await ethers.provider.getBalance(this.mini1155.address) + expect(contractBalance).to.equal(ethers.utils.parseEther('0')) + + // mint Tokens (Community) + for (let i = 1; i <= 77; i++) { + // console.log(`i: ${i}`) + // Mint 10 tokens + for (let j = 1; j <= 4; j++) { + await this.mini1155.connect(this.signers[i]).mint(j, { + value: c1.mintPrice.mul(j) + }) + } + } + console.log('Collection 1') + console.log(`Total Minted Standard Token : ${await this.mini1155.totalSupply(c1.s.tokenId)}`) + console.log(`Total Minted Rare Token : ${await this.mini1155.totalSupply(c1.r.tokenId)}`) + + // End the Sale and check token exists and mintAsOwner remaining Tokens + await this.mini1155.toggleSaleState() + expect(await this.mini1155.exists(c1.s.tokenId)).to.equal(true) + expect(await this.mini1155.exists(c1.s.tokenId)).to.equal(true) + let remainingStandard = (await this.mini1155.maxSupply(c1.s.tokenId)).sub(await this.mini1155.totalSupply(c1.s.tokenId)) + let remainingRare = (await this.mini1155.maxSupply(c1.r.tokenId)).sub(await this.mini1155.totalSupply(c1.r.tokenId)) + await this.mini1155.mintAsOwner(this.deployer.address, c1.s.tokenId, remainingStandard) + await this.mini1155.mintAsOwner(this.deployer.address, c1.r.tokenId, remainingRare) + expect(await this.mini1155.balanceOf(owner, c1.s.tokenId)).to.equal(remainingStandard) + expect(await this.mini1155.balanceOf(owner, c1.r.tokenId)).to.equal(remainingRare) + + // Exchange the Tokens (Community) + let exchangeRatio = 10 + await this.mini1155.setExchangeRatio(exchangeRatio) + for (let i = 1; i <= 70; i++) { + // console.log(`Exchange i: ${i}`) + const standardCount = await this.mini1155.balanceOf(this.signers[i].address, c1.s.tokenId) + // console.log(`Minted Rare Token : ${rareCount}`) + if (standardCount.toNumber() >= exchangeRatio) { + // Exchange standard tokens for rare + await this.mini1155.connect(this.signers[i]).exchange() + // console.log(`Exchanged`) + } + } + console.log(`Total Standard Token after exchange : ${await this.mini1155.totalSupply(c1.s.tokenId)}`) + console.log(`Total Rare Token after exchange : ${await this.mini1155.totalSupply(c1.r.tokenId)}`) + + // Redeem the tokens for Collectibles (Owner) + // Withdraw Proceedes from the Sale using to the owner and the revenue account + let revenueAccount = await this.mini1155.revenueAccount() + contractBalance = await ethers.provider.getBalance(this.mini1155.address) + let ownerBalance = await ethers.provider.getBalance(owner) + let revenueBalance = await ethers.provider.getBalance(revenueAccount) + expect(contractBalance).to.be.above(ethers.utils.parseEther('1')) + expect(ownerBalance).to.be.above(ethers.utils.parseEther('9999')) + expect(revenueBalance).to.equal(ethers.utils.parseEther('0')) + await this.mini1155.withdraw(ethers.utils.parseEther('1'), true) + expect(await ethers.provider.getBalance(this.mini1155.address)).to.equal(contractBalance.sub(ethers.utils.parseEther('1'))) + expect(await ethers.provider.getBalance(owner)).to.be.below(ownerBalance) // we paid a little gas to withdraw + expect(await ethers.provider.getBalance(revenueAccount)).to.equal(ethers.utils.parseEther('1')) + await this.mini1155.withdraw(contractBalance.sub(ethers.utils.parseEther('1')), false) + expect(await ethers.provider.getBalance(this.mini1155.address)).to.equal(ethers.BigNumber.from(0)) + expect(await ethers.provider.getBalance(owner)).to.be.above(ownerBalance) // we withdrew the funds and paid a little gas to withdraw + expect(await ethers.provider.getBalance(revenueAccount)).to.equal(ethers.utils.parseEther('1')) + + // TODO Withrdraw Royalties (Owner) + + // ==== LAUNCH A NEW COLLECTION ==== + // Configure Collection2 (Owner) + await mini1155configure({ mini1155: this.mini1155, config: c2 }) + + // Start Sale (Owner) + await this.mini1155.toggleSaleState() + contractBalance = await ethers.provider.getBalance(this.mini1155.address) + expect(contractBalance).to.equal(ethers.utils.parseEther('0')) + + // mint Tokens (Community) + for (let i = 1; i <= 77; i++) { + // console.log(`i: ${i}`) + // Mint 10 tokens + for (let j = 1; j <= 4; j++) { + await this.mini1155.connect(this.signers[i]).mint(j, { + value: c2.mintPrice.mul(j) + }) + } + } + console.log('Collection 2') + console.log(`Total Minted Standard Token : ${await this.mini1155.totalSupply(c2.s.tokenId)}`) + console.log(`Total Minted Rare Token : ${await this.mini1155.totalSupply(c2.r.tokenId)}`) + + // End the Sale and check token exists and mintAsOwner remaining Tokens + await this.mini1155.toggleSaleState() + expect(await this.mini1155.exists(c2.s.tokenId)).to.equal(true) + expect(await this.mini1155.exists(c2.s.tokenId)).to.equal(true) + remainingStandard = (await this.mini1155.maxSupply(c2.s.tokenId)).sub(await this.mini1155.totalSupply(c2.s.tokenId)) + remainingRare = (await this.mini1155.maxSupply(c2.r.tokenId)).sub(await this.mini1155.totalSupply(c2.r.tokenId)) + await this.mini1155.mintAsOwner(this.deployer.address, c2.s.tokenId, remainingStandard) + await this.mini1155.mintAsOwner(this.deployer.address, c2.r.tokenId, remainingRare) + expect(await this.mini1155.balanceOf(owner, c2.s.tokenId)).to.equal(remainingStandard) + expect(await this.mini1155.balanceOf(owner, c2.r.tokenId)).to.equal(remainingRare) + + // Exchange the Tokens (Community) + exchangeRatio = 20 + await this.mini1155.setExchangeRatio(exchangeRatio) + for (let i = 1; i <= 140; i++) { + // console.log(`Exchange i: ${i}`) + const standardCount = await this.mini1155.balanceOf(this.signers[i].address, c2.s.tokenId) + // console.log(`Minted Rare Token : ${rareCount}`) + if (standardCount.toNumber() >= exchangeRatio) { + // Exchange standard tokens for rare + await this.mini1155.connect(this.signers[i]).exchange() + // console.log(`Exchanged`) + } + } + console.log(`Total Standard Token after exchange : ${await this.mini1155.totalSupply(c2.s.tokenId)}`) + console.log(`Total Rare Token after exchange : ${await this.mini1155.totalSupply(c2.r.tokenId)}`) + + // Redeem the tokens for Collectibles (Owner) + // Withdraw Proceedes from the Sale using to the owner and the revenue account + revenueAccount = await this.mini1155.revenueAccount() + contractBalance = await ethers.provider.getBalance(this.mini1155.address) + ownerBalance = await ethers.provider.getBalance(owner) + revenueBalance = await ethers.provider.getBalance(revenueAccount) + expect(contractBalance).to.be.above(ethers.utils.parseEther('1')) + expect(ownerBalance).to.be.above(ethers.utils.parseEther('9999')) + expect(revenueBalance).to.equal(ethers.utils.parseEther('1')) // Revenue Transfer from Previous Collection + await this.mini1155.withdraw(ethers.utils.parseEther('1'), true) + expect(await ethers.provider.getBalance(this.mini1155.address)).to.equal(contractBalance.sub(ethers.utils.parseEther('1'))) + expect(await ethers.provider.getBalance(owner)).to.be.below(ownerBalance) // we paid a little gas to withdraw + expect(await ethers.provider.getBalance(revenueAccount)).to.equal(ethers.utils.parseEther('2')) + await this.mini1155.withdraw(contractBalance.sub(ethers.utils.parseEther('1')), false) + expect(await ethers.provider.getBalance(this.mini1155.address)).to.equal(ethers.BigNumber.from(0)) + expect(await ethers.provider.getBalance(owner)).to.be.above(ownerBalance) // we withdrew the funds and paid a little gas to withdraw + expect(await ethers.provider.getBalance(revenueAccount)).to.equal(ethers.utils.parseEther('2')) + + // TODO Withrdraw Royalties (Owner) + // Issue Non-sale Token (Owner) TokenId 5 with a quantity of 10 + const nonSaleTokenId = 5 + const nonSaleTokenQuantity = 10 + expect(await this.mini1155.exists(nonSaleTokenId)).to.equal(false) + await this.mini1155.mintAsOwner(this.deployer.address, nonSaleTokenId, nonSaleTokenQuantity) + expect(await this.mini1155.exists(nonSaleTokenId)).to.equal(true) + + // Pause Contract (Owner) + await this.mini1155.pause() + await this.mini1155.unpause() + + // Freeze Metadata (Owner) + await this.mini1155.freezeMetadata() + }) + + it('Mini1155-9 Royalty Validation', async function () { + // Do multiple transfers and check the royalties account balance gets updated. + }) + + it('Mini1155-10 GAS Usage', async function () { + + }) + }) +}) diff --git a/miniwallet/test/Mini721.ts b/miniwallet/test/Mini721.ts new file mode 100644 index 0000000..99e43af --- /dev/null +++ b/miniwallet/test/Mini721.ts @@ -0,0 +1,971 @@ +import { expect } from 'chai' +import { ethers, network } from 'hardhat' +import { ADDRESS_ZERO, prepare721, userMint721, communityMint721 } from './utilities' +import { BigNumber } from 'ethers' + +// let snapshotId: string; + +describe('Mini721', function () { + before(async function (this) { + await prepare721(this, []) + }) + + beforeEach(async function (this) { + this.snapshotId = await ethers.provider.send('evm_snapshot', []) + // console.log(`New Snapshot beforeEach: ${snapshotId}`); + this.mini721 = await this.Mini721.deploy( + false, // saleIsActive + false, // metadataFrozen + false, // provenanceFrozen + 10000, // maxMiniTokens + ethers.utils.parseEther('0.1'), // mintPrice 0.1 ETH + 10, // maxPerMint + '', // "ipfs://QmPcY4yVQu4J2z3ztHWziWkoUEugpzdfftbGH8xD49DvRx/", // baseUri + '' // "ipfs://Qmf8WkAVZtkBwngG4mTrPk23vDd6z8dZW2UshV9ywWGyB9/contract.json" // _contractUri + ) + await this.mini721.deployed() + // console.log("Mini721 contract", this.mini721.address); + }) + + afterEach(async function (this) { + // console.log(`Reverting Snapshot : ${snapshotId}`); + await network.provider.send('evm_revert', [this.snapshotId]) + }) + // TODO + // NFT Build Process (Images, Metadata, Provenance Hash) + + // Deployment Tests + describe('validate Initial Deploy', function (this) { + it('Mini721 contract should be created', async function () { + expect(await this.mini721.address).to.equal( + '0x5FbDB2315678afecb367f032d93F642f64180aa3' + ) + + // Public Variables + expect(await this.mini721.maxMiniTokens()).to.equal(10000) + expect(await this.mini721.mintPrice()).to.equal( + ethers.utils.parseEther('0.1') + ) // 0.1 ETH + expect(await this.mini721.maxPerMint()).to.equal(10) + expect(await this.mini721.startIndex()).to.equal(0) + expect(await this.mini721.provenanceHash()).to.equal('') + expect(await this.mini721.offsetValue()).to.equal(0) + expect(await this.mini721.metadataFrozen()).to.equal(false) + expect(await this.mini721.provenanceFrozen()).to.equal(false) + expect(await this.mini721.saleIsActive()).to.equal(false) + expect(await this.mini721.saleStarted()).to.equal(false) + expect(await this.mini721.temporaryTokenUri()).to.equal('') + + // Public Functions Mini721 + await expect(this.mini721.tokenURI(0)).to.be.revertedWith( + 'URIQueryForNonexistentToken()' + ) + expect(await this.mini721.supportsInterface('0x2a55205a')).to.equal(true) + expect(await this.mini721.exists(18)).to.equal(false) + expect(await this.mini721.contractURI()).to.equal('') + // Token should hold no eth initially + // expect(await ethers.provider.getBalance(this.mini721.address)).to.equal( + // ethers.utils.parseEther("0") + // ); + // Alices is the owner and there should be nothing to withdraw + // expect(await ethers.provider.getBalance(this.alice.address)).to.equal( + // ethers.utils.parseEther("9999.993762362500000000") + // ); + await this.mini721.withdraw(ethers.utils.parseEther('0'), false) + // expect(await ethers.provider.getBalance(this.alice.address)).to.equal( + // ethers.utils.parseEther("9999.993718938161152720") + // ); + expect(await this.mini721.tokensOfOwner(this.alice.address)).to.deep.equal( + [] + ) + + // Public Functions HRC721 + expect(await this.mini721.totalSupply()).to.equal(0) + expect(await this.mini721.name()).to.equal('MiniWallet NFT') + expect(await this.mini721.symbol()).to.equal('Mini721') + // Public Functions HRC721 (Tokens) + await expect(this.mini721.tokenByIndex(0)).to.be.revertedWith( + 'TokenIndexOutOfBounds()' + ) + await expect(this.mini721.ownerOf(0)).to.be.revertedWith( + 'OwnerQueryForNonexistentToken()' + ) + await expect(this.mini721.tokenURI(0)).to.be.revertedWith( + 'URIQueryForNonexistentToken()' + ) + // expect(await this.mini721.getApproved(0)).to.equal(""); + // Public Functions HRC721 (Users) + expect(await this.mini721.balanceOf(this.alice.address)).to.equal(0) + expect( + await this.mini721.isApprovedForAll(this.alice.address, this.bob.address) + ).to.equal(false) + // Public Functions HRC721 (User and Tokens) + await expect(this.mini721.tokenOfOwnerByIndex(this.alice.address, 0) + ).to.be.revertedWith('OwnerIndexOutOfBounds()') + + // Public Functions RoyaltiesV2 + expect(await this.mini721.getRaribleV2Royalties(0)).to.deep.equal([]) + }) + }) + + // Air Drop of 1000 Tokens using communityMint721 + describe('Mini721AirDropToOG', function (this) { + it('Minting 1000 Tokens for OG', async function () { + // Set the random start Index + await this.mini721.setTemporaryTokenUri( + 'ipfs://TemporaryTokenUriPlaceHolder/' + ) + await this.mini721.setStartIndex() + + // Mint 1000 tokens for airdrop to OG + let releaseCount + releaseCount = 100 + await communityMint721(this, this.alice.address, releaseCount) + releaseCount = 200 + await communityMint721(this, this.bob.address, releaseCount) + releaseCount = 1895 + await communityMint721(this, this.carol.address, releaseCount) + releaseCount = 5 + await communityMint721(this, this.dev.address, releaseCount) + await this.mini721.setOffsetValue(2200) + + // Public Variables + expect(await this.mini721.maxMiniTokens()).to.equal(10000) + expect(await this.mini721.mintPrice()).to.equal( + ethers.utils.parseEther('0.1') + ) // 0.1 ETH + expect(await this.mini721.maxPerMint()).to.equal(10) + expect(await this.mini721.provenanceHash()).to.equal('') + expect(await this.mini721.startIndex()).to.not.equal(0) + console.log(`startIndex: ${await this.mini721.startIndex()}`) + expect(await this.mini721.offsetValue()).to.equal(2200) + expect(await this.mini721.metadataFrozen()).to.equal(false) + expect(await this.mini721.provenanceFrozen()).to.equal(false) + expect(await this.mini721.saleIsActive()).to.equal(false) + expect(await this.mini721.saleStarted()).to.equal(false) + expect(await this.mini721.temporaryTokenUri()).to.equal( + 'ipfs://TemporaryTokenUriPlaceHolder/' + ) + + // Public Functions Mini721 + expect(await this.mini721.tokenURI(2198)).to.equal( + 'ipfs://TemporaryTokenUriPlaceHolder/' + ) + + // TODO review supportsInterface logic + expect(await this.mini721.supportsInterface('0x2a55205a')).to.equal(true) + expect(await this.mini721.exists(2198)).to.equal(true) + expect(await this.mini721.contractURI()).to.equal('') + // Token should hold no eth initially + // expect(await ethers.provider.getBalance(this.mini721.address)).to.equal( + // ethers.utils.parseEther("0") + // ); + // Alices is the owner and there should be nothing to withdraw + // expect(await ethers.provider.getBalance(this.alice.address)).to.equal( + // ethers.utils.parseEther("9999.985685653318801876") + // ); + await this.mini721.withdraw(ethers.utils.parseEther('0'), false) + // expect(await ethers.provider.getBalance(this.alice.address)).to.equal( + // ethers.utils.parseEther("9999.985653474264747487") + // ); + // expect(await this.mini721.tokensOfOwner(this.alice.address)).to.deep.equal( + // [] + // ); + expect(await this.mini721.tokensOfOwner(this.dev.address)).to.deep.equal([ + BigNumber.from(2195), + BigNumber.from(2196), + BigNumber.from(2197), + BigNumber.from(2198), + BigNumber.from(2199) + ]) + + // Public Functions HRC721 + expect(await this.mini721.totalSupply()).to.equal(2200) + expect(await this.mini721.name()).to.equal('MiniWallet NFT') + expect(await this.mini721.symbol()).to.equal('Mini721') + + // Public Functions HRC721 (Tokens) + expect(await this.mini721.tokenByIndex(2198)).to.equal(2198) + expect(await this.mini721.ownerOf(2198)).to.equal(this.dev.address) + expect(await this.mini721.tokenURI(2198)).to.equal( + 'ipfs://TemporaryTokenUriPlaceHolder/' + ) + expect(await this.mini721.getApproved(2198)).to.equal(ADDRESS_ZERO) + // Public Functions HRC721 (Users) + expect(await this.mini721.balanceOf(this.dev.address)).to.equal(5) + expect( + await this.mini721.isApprovedForAll(this.alice.address, this.bob.address) + ).to.equal(false) + // Public Functions HRC721 (User and Tokens) + expect( + await this.mini721.tokenOfOwnerByIndex(this.dev.address, 3) + ).to.equal(2198) + + // Public Functions RoyaltiesV2 + expect(await this.mini721.getRaribleV2Royalties(0)).to.deep.equal([]) + }) + }) + + // User Mints (1,5,10) and negative use cases + describe('Mini721UserMint', function (this) { + it('Mini721 tokens should be minted by Users', async function () { + // do airdrop first + let releaseCount + releaseCount = 100 + await communityMint721(this, this.alice.address, releaseCount) + releaseCount = 200 + await communityMint721(this, this.bob.address, releaseCount) + releaseCount = 1895 + await communityMint721(this, this.carol.address, releaseCount) + releaseCount = 5 + await communityMint721(this, this.dev.address, releaseCount) + await this.mini721.setOffsetValue(2200) + // set state variables + await this.mini721.toggleSaleState() + // Set the random start Index + await this.mini721.setTemporaryTokenUri( + 'ipfs://TemporaryTokenUriPlaceHolder/' + ) + await this.mini721.setStartIndex() + expect(await this.mini721.saleIsActive()).to.equal(true) + expect(await this.mini721.totalSupply()).to.equal(2200) + expect(await this.mini721.exists(0)).to.equal(true) + // mint tokens + releaseCount = 1 + await userMint721(this, this.alice, releaseCount) + + expect(await this.mini721.totalSupply()).to.equal(2201) + releaseCount = 5 + await userMint721(this, this.bob, releaseCount) + // const mintPrice = await this.mini721.mintPrice(); + // await this.mini721.connect(this.bob).mintMini(releaseCount, { + // value: mintPrice.mul(releaseCount), + // }); + + expect(await this.mini721.totalSupply()).to.equal(2206) + releaseCount = 10 + await userMint721(this, this.carol, releaseCount) + + releaseCount = 1 + expect(await this.mini721.totalSupply()).to.equal(2216) + expect(await this.mini721.maxMiniTokens()).to.equal(10000) + await userMint721(this, this.alice, releaseCount) + + // turn sale off and check user can no longer mint + await this.mini721.toggleSaleState() + expect(await this.mini721.saleIsActive()).to.equal(false) + await expect( + this.mini721.connect(this.alice).mintMini(releaseCount, { + value: ethers.utils.parseEther('0.1').mul(releaseCount) + }) + ).to.be.revertedWith('Mini721: Sale is not active') + + // Public Variables + expect(await this.mini721.maxMiniTokens()).to.equal(10000) + expect(await this.mini721.mintPrice()).to.equal( + ethers.utils.parseEther('0.1') + ) // 0.1 ETH + expect(await this.mini721.maxPerMint()).to.equal(10) + expect(await this.mini721.provenanceHash()).to.equal('') + expect(await this.mini721.startIndex()).to.not.equal(0) + console.log(`startIndex: ${await this.mini721.startIndex()}`) + expect(await this.mini721.offsetValue()).to.equal(2200) + expect(await this.mini721.metadataFrozen()).to.equal(false) + expect(await this.mini721.provenanceFrozen()).to.equal(false) + expect(await this.mini721.saleIsActive()).to.equal(false) + expect(await this.mini721.saleStarted()).to.equal(true) + expect(await this.mini721.temporaryTokenUri()).to.equal( + 'ipfs://TemporaryTokenUriPlaceHolder/' + ) + + // Public Functions Mini721 + expect(await this.mini721.tokenURI(12)).to.equal( + 'ipfs://TemporaryTokenUriPlaceHolder/' + ) + + // TODO review supportsInterface logic + expect(await this.mini721.supportsInterface('0x2a55205a')).to.equal(true) + expect(await this.mini721.exists(12)).to.equal(true) + expect(await this.mini721.contractURI()).to.equal('') + // Token should hold 1.7 ETH from the 17 mints + // expect(await ethers.provider.getBalance(this.mini721.address)).to.equal( + // ethers.utils.parseEther("1.7") + // ); + // Alice is the owner and there should be 1.7 ETH to withdraw + // expect(await ethers.provider.getBalance(this.alice.address)).to.equal( + // ethers.utils.parseEther("9999.784673934091280850") + // ); + await expect( + this.mini721.withdraw(ethers.utils.parseEther('1.8'), false) + ).to.be.revertedWith('') + await this.mini721.withdraw(ethers.utils.parseEther('0.7'), false) + // expect(await ethers.provider.getBalance(this.alice.address)).to.equal( + // ethers.utils.parseEther("10000.493126781913573560") + // ); + await this.mini721.setRevenueAccount(this.bob.address) + await this.mini721 + .connect(this.bob) + .withdraw(ethers.utils.parseEther('1.0'), true) + // expect(await ethers.provider.getBalance(this.bob.address)).to.equal( + // ethers.utils.parseEther("10000.484604394681221011") + // ); + // expect(await this.mini721.tokensOfOwner(this.carol.address)).to.deep.equal( + // [ + // BigNumber.from(6), + // BigNumber.from(7), + // BigNumber.from(8), + // BigNumber.from(9), + // BigNumber.from(10), + // BigNumber.from(11), + // BigNumber.from(12), + // BigNumber.from(13), + // BigNumber.from(14), + // BigNumber.from(15), + // ] + // ); + + // Public Functions HRC721 + expect(await this.mini721.totalSupply()).to.equal(2217) + expect(await this.mini721.name()).to.equal('MiniWallet NFT') + expect(await this.mini721.symbol()).to.equal('Mini721') + + // Public Functions HRC721 (Tokens) + expect(await this.mini721.tokenByIndex(2212)).to.equal(2212) + expect(await this.mini721.ownerOf(2212)).to.equal(this.carol.address) + expect(await this.mini721.tokenURI(2212)).to.equal( + 'ipfs://TemporaryTokenUriPlaceHolder/' + ) + expect(await this.mini721.getApproved(12)).to.equal(ADDRESS_ZERO) + // Public Functions HRC721 (Users) + expect(await this.mini721.balanceOf(this.carol.address)).to.equal(1905) + expect( + await this.mini721.isApprovedForAll( + this.alice.address, + this.carol.address + ) + ).to.equal(false) + // Public Functions HRC721 (User and Tokens) + expect( + await this.mini721.tokenOfOwnerByIndex(this.carol.address, 3) + ).to.equal(303) + + // Public Functions RoyaltiesV2 + expect(await this.mini721.getRaribleV2Royalties(0)).to.deep.equal([]) + }) + }) + + // Minting Negative Use Cases + describe('Mini721MintNegativeUseCases', function (this) { + it('Mini721 minting negative use Cases', async function () { + // do airdrop first + let releaseCount + releaseCount = 100 + await communityMint721(this, this.alice.address, releaseCount) + releaseCount = 200 + await communityMint721(this, this.bob.address, releaseCount) + releaseCount = 1895 + await communityMint721(this, this.carol.address, releaseCount) + releaseCount = 5 + await communityMint721(this, this.dev.address, releaseCount) + await this.mini721.setOffsetValue(2200) + await this.mini721.toggleSaleState() + // Test minting negative use cases + const mintPrice = await this.mini721.mintPrice() + const lowMintPrice = mintPrice.div(2) + const maxPerMint = await this.mini721.maxPerMint() + const highPerMint = maxPerMint.add(1) + const maxMiniTokens = await this.mini721.maxMiniTokens() + const highMiniTokens = maxMiniTokens.add(1) + // Test minting too many Mini tokens + await expect( + this.mini721.mintMini(highMiniTokens, { + value: mintPrice.mul(highMiniTokens) + }) + ).to.be.revertedWith('Mini721: Purchase would exceed cap') + // Test minting too many in a mint + await expect( + this.mini721.mintMini(highPerMint, { + value: mintPrice.mul(highPerMint) + }) + ).to.be.revertedWith('Mini721: Amount exceeds max per mint') + // Test mint with insufficient amount + await expect( + this.mini721.mintMini(1, { + value: lowMintPrice + }) + ).to.be.revertedWith('Mini721: Ether value sent is not correct') + // Test Mint with excess amount gets refunded + // expect(await ethers.provider.getBalance(this.bob.address)).to.equal( + // ethers.utils.parseEther("10000") + // ); + // mint 3 tokens parsing 1 eth + await this.mini721.connect(this.bob).mintMini(3, { + value: ethers.utils.parseEther('1') + }) + // bobs balance should be less 0.3 eth + gas + // expect(await ethers.provider.getBalance(this.bob.address)).to.equal( + // ethers.utils.parseEther("9999.699901322127631927") + // ); + + // Test Mini Community minting negative use cases + releaseCount = 1 + // Test minting to many Mini tokens + await expect( + this.mini721.mintForCommunity(this.alice.address, highMiniTokens) + ).to.be.revertedWith('Mini721: Minting would exceed cap') + // Test minting to zero address + await expect( + this.mini721.mintForCommunity(ADDRESS_ZERO, releaseCount) + ).to.be.revertedWith('Mini721: Cannot mint to zero address.') + }) + }) + + // NFT Sale Closed + describe('Mini721CloseCollection', function (this) { + it('Mini721 Close Collection', async function () { + // set state variables + + // Collection closed is set at time of last token minted (i.e. 10,000th) + // Currently using Provenance Hash from Bored Apes https://ipfs.io/ipfs/Qme57kZ2VuVzcj5sC3tVHFgyyEgBTmAnyTK45YVNxKf6hi + // https://boredapeyachtclub.com/#/provenance + // https://medium.com/coinmonks/the-elegance-of-the-nft-provenance-hash-solution-823b39f99473 + + // Mint 2200 tokens for airdrop to OG + let releaseCount + releaseCount = 100 + await communityMint721(this, this.alice.address, releaseCount) + releaseCount = 200 + await communityMint721(this, this.bob.address, releaseCount) + releaseCount = 1895 + await communityMint721(this, this.carol.address, releaseCount) + releaseCount = 5 + await communityMint721(this, this.dev.address, releaseCount) + await this.mini721.setOffsetValue(2200) + + // Do User Minting + await this.mini721.toggleSaleState() + releaseCount = 1 + await userMint721(this, this.alice, releaseCount) + releaseCount = 5 + await userMint721(this, this.bob, releaseCount) + releaseCount = 10 + await userMint721(this, this.carol, releaseCount) + releaseCount = 1 + await userMint721(this, this.alice, releaseCount) + + // turn sale off and check user can no longer mint + await this.mini721.toggleSaleState() + + // setProvenenceHash() + await this.mini721.setProvenanceHash( + 'cc354b3fcacee8844dcc9861004da081f71df9567775b3f3a43412752752c0bf' + ) + + // setBaseUri (Metadata) + const baseUri = 'ipfs://Qmf8WkAVZtkBwngG4mTrPk23vDd6z8dZW2UshV9ywWGyB9/' + await this.mini721.setBaseUri(baseUri) + + // setContractUri + await this.mini721.setContractUri( + 'ipfs://Qmf8WkAVZtkBwngG4mTrPk23vDd6z8dZW2UshV9ywWGyB9/contract.json' + ) + + // Set Contract URI + expect(await this.mini721.provenanceHash()).to.equal( + 'cc354b3fcacee8844dcc9861004da081f71df9567775b3f3a43412752752c0bf' + ) + + // Removce the TemporaryTokenUri + await this.mini721.setTemporaryTokenUri('') + + // Set the random start Index + await this.mini721.setStartIndex() + + // freezeProvenance() + await this.mini721.freezeProvenance() + await expect(this.mini721.setProvenanceHash('badHash')).to.be.revertedWith( + 'Mini721: Provenance is frozen' + ) + expect(await this.mini721.provenanceHash()).to.equal( + 'cc354b3fcacee8844dcc9861004da081f71df9567775b3f3a43412752752c0bf' + ) + + // freezeMetaData() + await this.mini721.freezeMetadata() + await expect(this.mini721.setBaseUri('badURI')).to.be.revertedWith( + 'Mini721: Metadata is frozen' + ) + + // Public Variables + expect(await this.mini721.maxMiniTokens()).to.equal(10000) + expect(await this.mini721.mintPrice()).to.equal( + ethers.utils.parseEther('0.1') + ) // 0.1 ETH + expect(await this.mini721.maxPerMint()).to.equal(10) + expect(await this.mini721.provenanceHash()).to.equal( + 'cc354b3fcacee8844dcc9861004da081f71df9567775b3f3a43412752752c0bf' + ) + expect(await this.mini721.startIndex()).to.not.equal(0) + console.log(`startIndex: ${await this.mini721.startIndex()}`) + expect(await this.mini721.offsetValue()).to.equal(2200) + expect(await this.mini721.metadataFrozen()).to.equal(true) + expect(await this.mini721.provenanceFrozen()).to.equal(true) + expect(await this.mini721.saleIsActive()).to.equal(false) + expect(await this.mini721.saleStarted()).to.equal(true) + expect(await this.mini721.temporaryTokenUri()).to.equal('') + + // Public Functions Mini721 + const offsetValue = parseInt(await this.mini721.offsetValue()) + const startIndex = parseInt(await this.mini721.startIndex()) + const maxMiniTokens = parseInt(await this.mini721.maxMiniTokens()) + let tokenId = 12 + let tokenUriSuffix = tokenId + if (tokenId > offsetValue) { + tokenUriSuffix = + ((tokenId + startIndex - offsetValue) % (maxMiniTokens - offsetValue)) + + offsetValue + } + let tokenUri = baseUri + tokenUriSuffix + expect(await this.mini721.tokenURI(12)).to.equal(tokenUri) + console.log(`tokenURI[12]: ${tokenUri}`) + + // TODO review supportsInterface logic + expect(await this.mini721.supportsInterface('0x2a55205a')).to.equal(true) + expect(await this.mini721.exists(12)).to.equal(true) + expect(await this.mini721.contractURI()).to.equal( + 'ipfs://Qmf8WkAVZtkBwngG4mTrPk23vDd6z8dZW2UshV9ywWGyB9/contract.json' + ) + // Token should hold 1.7 ETH from the 17 mints + expect(await ethers.provider.getBalance(this.mini721.address)).to.equal( + ethers.utils.parseEther('1.7') + ) + // Alice is the owner and there should be 1.7 ETH to withdraw + // expect(await ethers.provider.getBalance(this.alice.address)).to.equal( + // ethers.utils.parseEther("9999.784320346204536024") + // ); + await expect( + this.mini721.withdraw(ethers.utils.parseEther('1.8'), false) + ).to.be.revertedWith('') + await this.mini721.withdraw(ethers.utils.parseEther('1.7'), false) + // expect(await ethers.provider.getBalance(this.alice.address)).to.equal( + // ethers.utils.parseEther("10001.484254922290212431") + // ); + expect(await this.mini721.tokensOfOwner(this.dev.address)).to.deep.equal([ + BigNumber.from(2195), + BigNumber.from(2196), + BigNumber.from(2197), + BigNumber.from(2198), + BigNumber.from(2199) + ]) + + // Public Functions HRC721 + expect(await this.mini721.totalSupply()).to.equal(2217) + expect(await this.mini721.name()).to.equal('MiniWallet NFT') + expect(await this.mini721.symbol()).to.equal('Mini721') + + // Public Functions HRC721 (Tokens) + tokenId = 2212 + tokenUriSuffix = tokenId + if (tokenId > offsetValue) { + tokenUriSuffix = + ((tokenId + startIndex - offsetValue) % + (maxMiniTokens - offsetValue)) + + offsetValue + } + tokenUri = baseUri + tokenUriSuffix + expect(await this.mini721.tokenByIndex(tokenId)).to.equal(tokenId) + expect(await this.mini721.ownerOf(tokenId)).to.equal(this.carol.address) + expect(await this.mini721.tokenURI(tokenId)).to.equal(tokenUri) + console.log(`tokenURI[2212]: ${tokenUri}`) + expect(await this.mini721.getApproved(tokenId)).to.equal(ADDRESS_ZERO) + // Public Functions HRC721 (Users) + expect(await this.mini721.balanceOf(this.carol.address)).to.equal(1905) + expect( + await this.mini721.isApprovedForAll( + this.alice.address, + this.carol.address + ) + ).to.equal(false) + // Public Functions HRC721 (User and Tokens) + expect( + await this.mini721.tokenOfOwnerByIndex(this.carol.address, 3) + ).to.equal(303) + + // Public Functions RoyaltiesV2 + expect(await this.mini721.getRaribleV2Royalties(0)).to.deep.equal([]) + }) + }) + + // NFT (Transfer Ownership and Approval Testing + describe('Mini721TransferOwnership', function (this) { + it('Mini721 Transfer Ownership', async function () { + // Mint 2200 tokens for airdrop to OG + let releaseCount + releaseCount = 100 + await communityMint721(this, this.alice.address, releaseCount) + releaseCount = 200 + await communityMint721(this, this.bob.address, releaseCount) + releaseCount = 1895 + await communityMint721(this, this.carol.address, releaseCount) + releaseCount = 5 + await communityMint721(this, this.dev.address, releaseCount) + await this.mini721.setOffsetValue(2200) + // Do User Minting + await this.mini721.toggleSaleState() + releaseCount = 1 + await userMint721(this, this.alice, releaseCount) + releaseCount = 5 + await userMint721(this, this.bob, releaseCount) + releaseCount = 10 + await userMint721(this, this.carol, releaseCount) + releaseCount = 1 + await userMint721(this, this.alice, releaseCount) + // Airdrop + releaseCount = 20 + await communityMint721(this, this.bob.address, releaseCount) + // setProvenenceHash() + await this.mini721.setProvenanceHash( + 'cc354b3fcacee8844dcc9861004da081f71df9567775b3f3a43412752752c0bf' + ) + // setBaseUri (Metadata) + await this.mini721.setBaseUri( + 'ipfs://Qmf8WkAVZtkBwngG4mTrPk23vDd6z8dZW2UshV9ywWGyB9/' + ) + // Transfer from Carol to Bob and back + expect(await this.mini721.ownerOf(2209)).to.equal(this.carol.address) + expect(await this.mini721.getApproved(2209)).to.equal(ADDRESS_ZERO) + await this.mini721 + .connect(this.carol) + .transferFrom(this.carol.address, this.bob.address, 2209) + expect(await this.mini721.ownerOf(2209)).to.equal(this.bob.address) + await this.mini721 + .connect(this.bob) + .transferFrom(this.bob.address, this.carol.address, 2209) + expect(await this.mini721.ownerOf(2209)).to.equal(this.carol.address) + + // Carol gives approval to Bob and he transfers the token + await this.mini721.connect(this.carol).approve(this.bob.address, 2209) + expect(await this.mini721.getApproved(2209)).to.equal(this.bob.address) + expect(await this.mini721.ownerOf(2209)).to.equal(this.carol.address) + await this.mini721 + .connect(this.bob) + .transferFrom(this.carol.address, this.bob.address, 2209) + expect(await this.mini721.ownerOf(2209)).to.equal(this.bob.address) + expect(await this.mini721.getApproved(2209)).to.equal(ADDRESS_ZERO) + await this.mini721.connect(this.bob).approve(this.carol.address, 2209) + expect(await this.mini721.ownerOf(2209)).to.equal(this.bob.address) + expect(await this.mini721.getApproved(2209)).to.equal(this.carol.address) + await this.mini721 + .connect(this.carol) + .transferFrom(this.bob.address, this.carol.address, 2209) + expect(await this.mini721.ownerOf(2209)).to.equal(this.carol.address) + expect(await this.mini721.getApproved(2209)).to.equal(ADDRESS_ZERO) + + // Carol gives approval to Bob for all tokens and he does the transfer + await this.mini721 + .connect(this.carol) + .setApprovalForAll(this.bob.address, true) + expect(await this.mini721.getApproved(2209)).to.equal(ADDRESS_ZERO) + expect(await this.mini721.ownerOf(2209)).to.equal(this.carol.address) + expect( + await this.mini721.isApprovedForAll(this.carol.address, this.bob.address) + ).to.equal(true) + await this.mini721 + .connect(this.bob) + .transferFrom(this.carol.address, this.bob.address, 2209) + expect(await this.mini721.ownerOf(2209)).to.equal(this.bob.address) + expect(await this.mini721.getApproved(2209)).to.equal(ADDRESS_ZERO) + expect( + await this.mini721.isApprovedForAll(this.carol.address, this.bob.address) + ).to.equal(true) + await this.mini721 + .connect(this.bob) + .setApprovalForAll(this.carol.address, true) + expect(await this.mini721.ownerOf(2209)).to.equal(this.bob.address) + expect(await this.mini721.getApproved(2209)).to.equal(ADDRESS_ZERO) + expect( + await this.mini721.isApprovedForAll(this.bob.address, this.carol.address) + ).to.equal(true) + await this.mini721 + .connect(this.carol) + .transferFrom(this.bob.address, this.carol.address, 2209) + expect(await this.mini721.ownerOf(2209)).to.equal(this.carol.address) + expect(await this.mini721.getApproved(2209)).to.equal(ADDRESS_ZERO) + expect( + await this.mini721.isApprovedForAll(this.bob.address, this.carol.address) + ).to.equal(true) + }) + }) + + // NFT Updating of Metadata Testing + describe('Mini721UpdateMetadataOfIndividualTokens', function (this) { + it('Mini721 Update Metadata Of Individual Tokens', async function () { + // setOffsetValue(13) + // Mint 2200 tokens for airdrop to OG + let releaseCount + releaseCount = 100 + await communityMint721(this, this.alice.address, releaseCount) + releaseCount = 200 + await communityMint721(this, this.bob.address, releaseCount) + releaseCount = 1895 + await communityMint721(this, this.carol.address, releaseCount) + releaseCount = 5 + await communityMint721(this, this.dev.address, releaseCount) + await this.mini721.setOffsetValue(2200) + // do user and community mint testing + await this.mini721.setOffsetValue(2200) + await this.mini721.toggleSaleState() + releaseCount = 1 + await userMint721(this, this.alice, releaseCount) + releaseCount = 5 + await userMint721(this, this.bob, releaseCount) + + // Set the random start Index + await this.mini721.setStartIndex() + expect(await this.mini721.startIndex()).to.not.equal(0) + console.log(`startIndex: ${await this.mini721.startIndex()}`) + // setBaseUri (Metadata) + const baseUri = 'ipfs://Qmf8WkAVZtkBwngG4mTrPk23vDd6z8dZW2UshV9ywWGyB9/' + await this.mini721.setBaseUri(baseUri) + + // Check the token + const offsetValue = parseInt(await this.mini721.offsetValue()) + const startIndex = parseInt(await this.mini721.startIndex()) + const maxMiniTokens = parseInt(await this.mini721.maxMiniTokens()) + const tokenId = 3 + let tokenUriSuffix = tokenId + if (tokenId > offsetValue) { + tokenUriSuffix = + ((tokenId + startIndex - offsetValue) % + (maxMiniTokens - offsetValue)) + + offsetValue + } + const tokenUri = baseUri + tokenUriSuffix + expect(await this.mini721.tokenURI(tokenId)).to.equal(tokenUri) + + // Update the tokens metadata + expect(await this.mini721.metadataFrozen()).to.equal(false) + const updatedBaseUri = 'ipfs://bestupdateduri/' + const updatedTokenUri = updatedBaseUri + tokenUriSuffix + await this.mini721.setUri(3, updatedTokenUri) + expect(await this.mini721.tokenURI(tokenId)).to.equal(updatedTokenUri) + // Freeze metadata and then Updating the tokens metadata should fail + await this.mini721.freezeMetadata() + expect(await this.mini721.metadataFrozen()).to.equal(true) + await expect( + this.mini721.setUri(tokenId, updatedTokenUri) + ).to.be.revertedWith('') + }) + }) + + // NFT Burning Testing + describe('Mini721Burn', function (this) { + it('Mini721 Test Burning of Tokens', async function () { + // Mint 2200 tokens for airdrop to OG + let releaseCount + releaseCount = 100 + await communityMint721(this, this.alice.address, releaseCount) + releaseCount = 200 + await communityMint721(this, this.bob.address, releaseCount) + releaseCount = 1895 + await communityMint721(this, this.carol.address, releaseCount) + releaseCount = 5 + await communityMint721(this, this.dev.address, releaseCount) + await this.mini721.setOffsetValue(2200) + // Do User Minting + await this.mini721.toggleSaleState() + releaseCount = 1 + await userMint721(this, this.alice, releaseCount) + releaseCount = 5 + await userMint721(this, this.bob, releaseCount) + releaseCount = 10 + await userMint721(this, this.carol, releaseCount) + releaseCount = 1 + await userMint721(this, this.alice, releaseCount) + // Airdrop + releaseCount = 20 + await communityMint721(this, this.bob.address, releaseCount) + // setProvenenceHash() + await this.mini721.setProvenanceHash( + 'cc354b3fcacee8844dcc9861004da081f71df9567775b3f3a43412752752c0bf' + ) + // setBaseUri (Metadata) + await this.mini721.setBaseUri( + 'ipfs://Qmf8WkAVZtkBwngG4mTrPk23vDd6z8dZW2UshV9ywWGyB9/' + ) + + // Check current State + expect(await this.mini721.totalSupply()).to.equal(2237) + expect(await this.mini721.ownerOf(2209)).to.equal(this.carol.address) + expect(await this.mini721.balanceOf(this.carol.address)).to.equal(1905) + expect( + await this.mini721.tokenOfOwnerByIndex(this.carol.address, 3) + ).to.equal(303) + // expect(await this.mini721.tokensOfOwner(this.carol.address)).to.deep.equal( + // [ + // BigNumber.from(6), + // BigNumber.from(7), + // BigNumber.from(8), + // BigNumber.from(9), + // BigNumber.from(10), + // BigNumber.from(11), + // BigNumber.from(12), + // BigNumber.from(13), + // BigNumber.from(14), + // BigNumber.from(15), + // ] + // ); + // Burn a token + await this.mini721.burn(9) + // Check current State + expect(await this.mini721.totalSupply()).to.equal(2236) + // await expect(this.mini721.ownerOf(2237)).to.be.revertedWith( + // "OwnerQueryForNonexistentToken()" + // ); + expect(await this.mini721.balanceOf(this.carol.address)).to.equal(1905) + expect( + await this.mini721.tokenOfOwnerByIndex(this.carol.address, 3) + ).to.equal(303) + // expect(await this.mini721.tokensOfOwner(this.carol.address)).to.deep.equal( + // [ + // BigNumber.from(6), + // BigNumber.from(7), + // BigNumber.from(8), + // BigNumber.from(10), + // BigNumber.from(11), + // BigNumber.from(12), + // BigNumber.from(13), + // BigNumber.from(14), + // BigNumber.from(15), + // ] + // ); + + // Burn some tokens + await this.mini721.batchBurn([7, 12, 14]) + // Check current State + expect(await this.mini721.totalSupply()).to.equal(2233) + // await expect(this.mini721.ownerOf(2212)).to.be.revertedWith( + // "OwnerQueryForNonexistentToken()" + // ); + expect(await this.mini721.balanceOf(this.carol.address)).to.equal(1905) + expect( + await this.mini721.tokenOfOwnerByIndex(this.carol.address, 3) + ).to.equal(303) + // expect(await this.mini721.tokensOfOwner(this.carol.address)).to.deep.equal( + // [ + // BigNumber.from(6), + // BigNumber.from(8), + // BigNumber.from(10), + // BigNumber.from(11), + // BigNumber.from(13), + // BigNumber.from(15), + // ] + // ); + + // freezeMetaData() means burning no longer allowed + // await this.mini721.freezeMetadata(); + // await expect(this.mini721.burn(8)).to.be.revertedWith( + // "Mini721: Metadata is frozen" + // ); + // await expect(this.mini721.batchBurn([10, 11, 13])).to.be.revertedWith( + // "Mini721: Metadata is frozen" + // ); + }) + }) + + // NFT Royalty Testing + describe('Mini721Royalties', function (this) { + it('Mini721 Test Royalties', async function () { + // safeTransferFrom(this.carol.address, this.bob.address, 13) + // safeTransferFrom(this.carol.address, this.bob.address, 13, "bytes") + // getRaribleV2Royalties(uint256) + }) + }) + + // NFT Event Testing + describe('Mini721TestEvents', function (this) { + it('Mini721 Test all events are emitted correctly', async function () { + // safeTransferFrom(this.carol.address, this.bob.address, 13) + // safeTransferFrom(this.carol.address, this.bob.address, 13, "bytes") + // getRaribleV2Royalties(uint256) + }) + }) + + // Test remaining functions including royalties and granitng approval + describe('MiniRemainingFunctions', function (this) { + it('Mini721 remaining functions including royaltiess and approvals', async function () { + // Remaining Update Functions + // mintMini(1) + // freezeMetaData() + // freezeProvenance() + // toggleSaleState() + // setOffsetValue(13) + // setMaxPerMint + // setMintPrice(ethers.utils.parseEther("0.2")) + // setBaseUri + // receive + // HRC721 Update Functions + // approve(this.carol.address,13) + // setApprovalForAll(this.carol.address, true) + // safeTransferFrom(this.carol.address, this.bob.address, 13) + // safeTransferFrom(this.carol.address, this.bob.address, 13, "bytes") + + // setTemporaryTokenUri(string) + // setStartIndex() + // setUri(uint256,string) + + }) + }) + + // NFT (Transfer Ownership) + describe('Mini721NFTSale', function (this) { + it('Mini721 tokens should be sold by users', async function () { + // Mint 2200 tokens for airdrop to OG + let releaseCount + releaseCount = 100 + await communityMint721(this, this.alice.address, releaseCount) + releaseCount = 200 + await communityMint721(this, this.bob.address, releaseCount) + releaseCount = 1895 + await communityMint721(this, this.carol.address, releaseCount) + releaseCount = 5 + await communityMint721(this, this.dev.address, releaseCount) + await this.mini721.setOffsetValue(2200) + await this.mini721.toggleSaleState() + releaseCount = 10 + await userMint721(this, this.bob, releaseCount) + expect(await this.mini721.ownerOf(2201)).to.equal(this.bob.address) + await this.mini721 + .connect(this.bob) + ['safeTransferFrom(address,address,uint256)']( + this.bob.address, + this.carol.address, + 2201 + ) + expect(await this.mini721.ownerOf(2201)).to.equal(this.carol.address) + await this.mini721 + .connect(this.carol) + .transferFrom(this.carol.address, this.alice.address, 2201) + expect(await this.mini721.ownerOf(1)).to.equal(this.alice.address) + }) + }) + // 7. Metadata Update (fixing outdated metadata) + // MetaData + describe('checkMetaData', function (this) { + it('Mini721 Metadata can be updated until frozen', async function () { + // ipfs://QmPcY4yVQu4J2z3ztHWziWkoUEugpzdfftbGH8xD49DvRx/ + await this.mini721.setBaseUri( + 'ipfs://Qmf8WkAVZtkBwngG4mTrPk23vDd6z8dZW2UshV9ywWGyB9/' + ) + // TODO Check the baseURI is used in tokenURI + await this.mini721.freezeMetadata() + await expect(this.mini721.setBaseUri('badURI')).to.be.revertedWith( + 'Mini721: Metadata is frozen' + ) + }) + }) + + // TODO Additional Tests (tested separately) + // A. Auctions + // B. Airdrops +}) diff --git a/miniwallet/test/utilities/Mini1155Util.ts b/miniwallet/test/utilities/Mini1155Util.ts new file mode 100644 index 0000000..58a7206 --- /dev/null +++ b/miniwallet/test/utilities/Mini1155Util.ts @@ -0,0 +1,73 @@ +// import { expect } from 'chai' +import { ethers, network } from 'hardhat' + +// import { userMint1155, safeTransferFrom1155 } from './utilities' + +export async function mini1155configure ({ + mini1155, + config = { + revenueAccount: '0xa636a88102a7821b7e42292a4A3920A25a5d49b5', + maxPerMint: 10, + mintPrice: ethers.utils.parseEther('0.042'), + exchangeRatio: 0, // will be 20 after sale + rareProbabilityPercentage: 1, + // standard token configuration + s: { + tokenId: 1, + maxSupply: 770, + personalCap: 10 + }, + // rare token configuration + r: { + tokenId: 2, + maxSupply: 7, // will be 84 after Sale + personalCap: 1 + } + } +}) { + // set the revenue account + await mini1155.setRevenueAccount(config.revenueAccount) + // configure standard Token + await mini1155.setStandardTokenId(config.s.tokenId) + await mini1155.setMaxSupply(config.s.tokenId, config.s.maxSupply) + await mini1155.setMaxPersonalCap(config.s.tokenId, config.s.personalCap) + // await mini1155.setUri(standardTokenId, standardURI) + // configure rare Token + await mini1155.setRareTokenId(config.r.tokenId) + await mini1155.setMaxSupply(config.r.tokenId, config.r.maxSupply) + await mini1155.setMaxPersonalCap(config.r.tokenId, config.r.personalCap) + // await mini1155.setUri(rareTokenId, rareURI) + await mini1155.setRareProbabilityPercentage(config.rareProbabilityPercentage) + // configure exchange rate + await mini1155.setExchangeRatio(config.exchangeRatio) +} + +export async function mini1155Mint (mini1155, minter, numTokens) { + const tx = await mini1155 + .connect(minter)['mint(uint256)'](numTokens) + return tx +} + +export async function mini1155OwnerMint (mini1155, minter, tokenId, numTokens) { + const tx = await mini1155.connect(minter)['mint(address,uint256,uint256,bytes)'](minter, tokenId, numTokens, '0x') + return tx +} + +export async function mini1155SafeTransferFrom ( + mini1155, + sender, + receiver, + tokenId, + numTokens +) { + const tx = await mini1155 + .connect(sender) + .safeTransferFrom( + sender.address, + receiver.address, + tokenId, + numTokens, + '0x' + ) + return tx +} diff --git a/miniwallet/test/utilities/Mini721Util.ts b/miniwallet/test/utilities/Mini721Util.ts new file mode 100644 index 0000000..7658b94 --- /dev/null +++ b/miniwallet/test/utilities/Mini721Util.ts @@ -0,0 +1,52 @@ +import { ethers } from 'hardhat' +const { BigNumber } = require('ethers') +const { ethers: { constants: { MaxUint256 } } } = require('ethers') + +export const BASE_TEN = 10 +export const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000' + +export function encodeParameters (types, values) { + const abi = new ethers.utils.AbiCoder() + return abi.encode(types, values) +}; + +export async function prepare721 (thisObject, contracts) { + for (const i in contracts) { + const contract = contracts[i] + thisObject[contract] = await ethers.getContractFactory(contract) + } + thisObject.signers = await ethers.getSigners() + thisObject.alice = thisObject.signers[0] + thisObject.bob = thisObject.signers[1] + thisObject.carol = thisObject.signers[2] + thisObject.dev = thisObject.signers[3] + thisObject.alicePrivateKey = + '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' + thisObject.bobPrivateKey = + '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d' + thisObject.carolPrivateKey = + '0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a' + thisObject.Mini721 = await ethers.getContractFactory('Mini721') +} + +export async function deploy721 (thisObject, contracts) { + for (const i in contracts) { + const contract = contracts[i] + thisObject[contract[0]] = await contract[1].deploy(...(contract[2] || [])) + await thisObject[contract[0]].deployed() + } +} + +export async function userMint721 (thisObject, minter, numTokens) { + const mintPrice = await thisObject.mini721.mintPrice() + // console.log(`mintPrice: ${JSON.stringify(mintPrice)}`) + const tx = await thisObject.mini721.connect(minter).mintMini(numTokens, { + value: mintPrice.mul(numTokens) + }) + return tx +} + +export async function communityMint721 (thisObject, owner, numTokens) { + const tx = await thisObject.mini721.mintForCommunity(owner, numTokens) + return tx +} diff --git a/miniwallet/test/utilities/index.ts b/miniwallet/test/utilities/index.ts index 69820f6..8705f50 100644 --- a/miniwallet/test/utilities/index.ts +++ b/miniwallet/test/utilities/index.ts @@ -68,3 +68,6 @@ export async function checkContractBalance ( export function getBigNumber (amount: string, decimals = 18) { return BigNumber.from(amount).mul(BigNumber.from(BASE_TEN).pow(decimals)) } + +export * from './Mini721Util' +export * from './Mini1155Util'