pragma solidity ^0.8.4;







/****************************************************************
* OPTIX by The Blinkless: The Official Currency of New Cornea
* "Soft" stake your favorite NFTs
* code by @digitalkemical
*****************************************************************/

contract Optix is ERC20, ERC20Burnable, Ownable {
    //define a stake structure
    struct Stake{
        address contractAddress;
        address ownerAddress;
        uint startTime;
        uint tokenId;
    }

    //define a collection structure
    struct Collection{
        address contractAddress;
        uint hourlyReward;
    }

    //define variables
    mapping( address => mapping(uint => Stake ) ) public openStakes; //mapping of all open stakes by collection address
    mapping( address => mapping( address => uint[] ) ) public myActiveCollections; //mapping of wallet address to all active collections
    mapping( address => bool ) public hasMigrated; //bool tracker of who has migrated from v1
    Collection[] public collections; //array of NFT collections that can be staked
    address[] public freeCollections; //array of contract addresses for collections with no fee
    address v1ContractAddress = 0xcEE33d20845038Df71B88041B28c3654CF05ae2f; //address to v1 Optix contract (for migration)
    address payoutWallet = 0xeD2faa60373eC70E57B39152aeE5Ce4ed7C333c7; //wallet for payouts
    uint migrationMode = 1; //turn on/off migrations
    uint public thirdPartyFee = 0.001 ether; //fee to charge for 3rd party stakes

    //run on deploy
    constructor() ERC20("Optix", "OPTIX") {}

    /**
    * Owner can mint more tokens
    */
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    /**
    * Migrate balance from v1 contract
    */
    function migrateFromv1() public{
        require(migrationMode == 1, "Migration period has closed.");
        require(hasMigrated[msg.sender] == false, "You have already migrated.");

         uint oldBalance = IERC20(v1ContractAddress).balanceOf(msg.sender);
       
        hasMigrated[msg.sender] = true;
        _mint(msg.sender, oldBalance + 100000 ether);
        
        
    }


    /**
    * Add a collection to the staking options
    */
    function addCollection(address _contractAddress, uint _hourlyReward) public onlyOwner{
        collections.push(
            Collection(
                _contractAddress,
                _hourlyReward
            )
            );
    }

    /**
    * Update the third party fee
    */
    function updateThirdPartyFee(uint _fee) public onlyOwner{
        thirdPartyFee = _fee;
    }

    /**
    * Update the migration mode
    */
    function updateMigrationMode(uint _migrationMode) public onlyOwner{
        migrationMode = _migrationMode;
    }

    /**
    * Add a collection to free collection list
    */
    function addToFreeCollections(address _contractAddress) public onlyOwner{
        freeCollections.push(_contractAddress);

    }

    /**
    * Update the v1 contract address
    */
    function updateV1Contract(address _v1ContractAddress) public onlyOwner{
        v1ContractAddress = _v1ContractAddress;
    }

    /**
    * Update the payout wallet address
    */
    function updatePayoutWallet(address _payoutWallet) public onlyOwner{
        payoutWallet = _payoutWallet;
    }

    /**
    * Remove a collection from the free collection list
    */
    function removeFreeCollection(address _contractAddress) public onlyOwner{
        uint i = 0;
        while(i < freeCollections.length){
            if(freeCollections[i] == _contractAddress){
                freeCollections[i] = freeCollections[freeCollections.length-1];
                freeCollections.pop();
            }
            i++;
        }
    }

    /**
    * Remove a collection from the staking options
    */
    function removeCollection(address _contractAddress) public onlyOwner{
        uint i = 0;
        while(i < collections.length){
            if(collections[i].contractAddress == _contractAddress){
                collections[i] = collections[collections.length-1];
                collections.pop();
            }
            i++;
        }
    }

    /**
    * Get all available collections
    */
    function getAllCollections() public view returns(Collection[] memory _collections){
        return collections;
    }

    /**
    * Get all free collections
    */
    function getAllFreeCollections() public view returns(address[] memory _collections){
        return freeCollections;
    }

    /**
    * Get a collection
    */
    function getCollection(address _contractAddress) public view returns(Collection memory _collections){
        uint i = 0;
        while(i < collections.length){
            if(collections[i].contractAddress == _contractAddress){
                return collections[i];
            }
            i++;
        }
    }

    /**
    * Open a new soft stake (tokens are never locked)
    */
    function openStake(address _contractAddress, uint[] memory _tokenIds) public payable {
        //check if collection is approved
        require(collectionIsApproved(_contractAddress),"This collection has not been approved.");

        bool isFree = false;
        uint i = 0;
        while(i < freeCollections.length){
            if(freeCollections[i] == _contractAddress){
                isFree = true;
            }
            i++;
        }

        //charge a withdrawal fee for 3rd party collections 
        if(!isFree){
            require(msg.value >= thirdPartyFee * _tokenIds.length, "Insufficient funds to open 3rd-party stake.");
        }

        uint counter = 0;
        while(counter < _tokenIds.length){
            uint _tokenId = _tokenIds[counter];
            //ensure sender is owner of token and collection is approved
            
            require(IERC721(_contractAddress).ownerOf(_tokenId) == msg.sender,"Could not verify ownership!");

            //if trying to open a stake previously owned, update the stake owner
            if(checkForStake(_contractAddress,_tokenId) && openStakes[_contractAddress][_tokenId].ownerAddress != msg.sender){
                updateOwnership( _contractAddress, _tokenId );
            }
            //make sure stake doesn't already exist
            if(!checkForStake(_contractAddress,_tokenId)){
             

                //create a new stake
                openStakes[_contractAddress][_tokenId]=
                    Stake(
                        _contractAddress,
                        msg.sender,
                        block.timestamp,
                        _tokenId
                    )
                ;
                    
                //add collection to active list
                addToActiveList(_contractAddress, _tokenId);
            }

            counter++;
        }
        
    }

    /**
    * Add an active collection to a wallet
    */
    function addToActiveList(address _contractAddress, uint _tokenId) internal {
        uint i = 0;
        bool exists = false;
        while(i < myActiveCollections[msg.sender][_contractAddress].length){
            if(myActiveCollections[msg.sender][_contractAddress][i] == _tokenId){
                exists = true;
            }
            i++;
        }
        //if it doesnt already exist, add it
        if(!exists){
            myActiveCollections[msg.sender][_contractAddress].push(_tokenId);
        }
    }

    /**
    * Get the active list for the wallet by collection contract address
    */
    function getActiveList(address _contractAddress) external view returns(uint[] memory _activeList){
        //get list of active collections for sender
        return myActiveCollections[msg.sender][_contractAddress];
        
    }

    /**
    * Verify that a collection being staked has been approved
    */
    function collectionIsApproved(address _contractAddress) public view returns(bool _approved){
        uint i = 0;
        while(i < collections.length){
            if(collections[i].contractAddress == _contractAddress){
                return true;
            }
            i++;
        }

        return false;
    }

    /**
    * Check if a stake exists already
    */
    function checkForStake(address _contractAddress, uint _tokenId) public view returns(bool _exists){
 
            if(openStakes[_contractAddress][_tokenId].startTime > 0){
                return true;
            }

        return false;
    }

    /**
    * Get a stake
    */
    function getStake(address _contractAddress, uint _tokenId) public view returns(Stake memory _exists){
        return openStakes[_contractAddress][_tokenId];
    }

    /**
    * Calculate stake reward for a token
    */
    function calculateStakeReward(address _contractAddress, uint _tokenId) public view returns(uint _totalReward){
        //get the stake
        Stake memory closingStake = getStake( _contractAddress, _tokenId );
        //get collection data
        Collection memory stakedCollection = getCollection(_contractAddress);
        //calc hours in between start and now
        uint hoursDiff = (block.timestamp - closingStake.startTime) / 60 / 60;
        //calc total reward
        uint totalReward = hoursDiff * stakedCollection.hourlyReward;

        return totalReward;
    }

    /**
    * Close a stake and claim reward
    */
    function closeStake(address _contractAddress, uint[] memory _tokenIds) public payable returns(uint _totalReward){
        
        uint totalReward = 0;
        
        uint counter = 0;
        while(counter < _tokenIds.length){
            uint _tokenId = _tokenIds[counter];

                bool isFree = false;
                uint i2 = 0;
                while(i2 < freeCollections.length){
                    if(freeCollections[i2] == _contractAddress){
                        isFree = true;
                    }
                    i2++;
                }

                //charge a withdrawal fee for 3rd party collections - Blinkless NFTs will be zero
                if(!isFree){
                    require(msg.value >= thirdPartyFee * _tokenIds.length, "Insufficient funds to open 3rd-party stake.");
                }

                if(checkForStake(_contractAddress,_tokenId) && IERC721(_contractAddress).ownerOf(_tokenId) == msg.sender){

                        //calculate end of stake reward
                        totalReward += calculateStakeReward(_contractAddress, _tokenId);

                        //stake has been identified, remove stake
                        delete(openStakes[_contractAddress][_tokenId]);

                        //remove from active list
                        uint i = 0;
                        while(i < myActiveCollections[msg.sender][_contractAddress].length){
                            if(myActiveCollections[msg.sender][_contractAddress][i] == _tokenId){
                                myActiveCollections[msg.sender][_contractAddress][i] = myActiveCollections[msg.sender][_contractAddress][myActiveCollections[msg.sender][_contractAddress].length-1];
                                myActiveCollections[msg.sender][_contractAddress].pop();
                            }
                            i++;
                        }

                }

                counter++;
        }

        //award tokens
        if(totalReward > 0){
            _mint(msg.sender, totalReward);
        }

        return totalReward;
      

    }

    /**
    * Claim rewards from multiple stakes at once without closing 
    */
    function claimWithoutClosing(address _contractAddress, uint[] memory _tokenIds) public returns(uint _totalReward){
        
        uint totalReward = 0;

            uint counter = 0;
            while(counter < _tokenIds.length){
                uint _tokenId = _tokenIds[counter];

                    if(checkForStake(_contractAddress,_tokenId) && IERC721(_contractAddress).ownerOf(_tokenId) == msg.sender){

                            //calculate end of stake reward
                            totalReward += calculateStakeReward(_contractAddress, _tokenId);

                            //stake has been identified, update the timestamp
                            openStakes[_contractAddress][_tokenId].startTime = block.timestamp;

                    }

                counter++;
            }

           

        //award tokens
        if(totalReward > 0){
            _mint(msg.sender, totalReward);
        }

        return totalReward;
      

    }


    /**
    * Claim rewards from all stakes without closing
    */
    function claimAllWithoutClosing() public returns(uint _totalReward){
        
        uint totalReward = 0;
        uint collectionCounter = 0;
        while(collectionCounter < collections.length){
            address _contractAddress = collections[collectionCounter].contractAddress;
            uint[] memory _tokenIds = myActiveCollections[msg.sender][_contractAddress];

            uint counter = 0;
            while(counter < _tokenIds.length){
                uint _tokenId = _tokenIds[counter];

                    if(checkForStake(_contractAddress,_tokenId) && IERC721(_contractAddress).ownerOf(_tokenId) == msg.sender){

                            //calculate end of stake reward
                            totalReward += calculateStakeReward(_contractAddress, _tokenId);

                            //stake has been identified, update the timestamp
                            openStakes[_contractAddress][_tokenId].startTime = block.timestamp;

                    }

                counter++;
            }

            collectionCounter++;

        }

        //award tokens
        if(totalReward > 0){
            _mint(msg.sender, totalReward);
        }

        return totalReward;
      

    }

    /**
    * Update the ownership of a stake to match the NFT owner
    */
    function updateOwnership(address _contractAddress, uint _tokenId) public{
        //get the NFT owner
        address tokenOwner = IERC721(_contractAddress).ownerOf(_tokenId);
        if(openStakes[_contractAddress][_tokenId].ownerAddress != address(0) &&
            openStakes[_contractAddress][_tokenId].ownerAddress != tokenOwner
        ){
            //stake exists, update owner
            uint i = 0;
            while(i < myActiveCollections[openStakes[_contractAddress][_tokenId].ownerAddress][_contractAddress].length){
                if(myActiveCollections[openStakes[_contractAddress][_tokenId].ownerAddress][_contractAddress][i] == _tokenId){
                    //delete active collection
                    myActiveCollections[openStakes[_contractAddress][_tokenId].ownerAddress][_contractAddress][i] = myActiveCollections[openStakes[_contractAddress][_tokenId].ownerAddress][_contractAddress][myActiveCollections[openStakes[_contractAddress][_tokenId].ownerAddress][_contractAddress].length-1];
                    myActiveCollections[openStakes[_contractAddress][_tokenId].ownerAddress][_contractAddress].pop();
                }
                i++;
            }
            //update stake ownership
            myActiveCollections[tokenOwner][_contractAddress].push(_tokenId);
            openStakes[_contractAddress][_tokenId].ownerAddress = tokenOwner;
        }
        
    }



    /*
    * Withdraw by owner
    */
    function withdraw() external onlyOwner {
        (bool success, ) = payable(payoutWallet).call{value: address(this).balance}("");
        require(success, "Transfer failed.");
    }


    /*
    * These are here to receive ETH sent to the contract address
    */
    receive() external payable {}

    fallback() external payable {}

}
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"stateMutability":"payable","type":"fallback"},{"inputs":[{"internalType":"address","name":"_contractAddress","type":"address"},{"internalType":"uint256","name":"_hourlyReward","type":"uint256"}],"name":"addCollection","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_contractAddress","type":"address"}],"name":"addToFreeCollections","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"burn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"burnFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_contractAddress","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"calculateStakeReward","outputs":[{"internalType":"uint256","name":"_totalReward","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_contractAddress","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"checkForStake","outputs":[{"internalType":"bool","name":"_exists","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"claimAllWithoutClosing","outputs":[{"internalType":"uint256","name":"_totalReward","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_contractAddress","type":"address"},{"internalType":"uint256[]","name":"_tokenIds","type":"uint256[]"}],"name":"claimWithoutClosing","outputs":[{"internalType":"uint256","name":"_totalReward","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_contractAddress","type":"address"},{"internalType":"uint256[]","name":"_tokenIds","type":"uint256[]"}],"name":"closeStake","outputs":[{"internalType":"uint256","name":"_totalReward","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"_contractAddress","type":"address"}],"name":"collectionIsApproved","outputs":[{"internalType":"bool","name":"_approved","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"collections","outputs":[{"internalType":"address","name":"contractAddress","type":"address"},{"internalType":"uint256","name":"hourlyReward","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"freeCollections","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_contractAddress","type":"address"}],"name":"getActiveList","outputs":[{"internalType":"uint256[]","name":"_activeList","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getAllCollections","outputs":[{"components":[{"internalType":"address","name":"contractAddress","type":"address"},{"internalType":"uint256","name":"hourlyReward","type":"uint256"}],"internalType":"struct Optix.Collection[]","name":"_collections","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getAllFreeCollections","outputs":[{"internalType":"address[]","name":"_collections","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_contractAddress","type":"address"}],"name":"getCollection","outputs":[{"components":[{"internalType":"address","name":"contractAddress","type":"address"},{"internalType":"uint256","name":"hourlyReward","type":"uint256"}],"internalType":"struct Optix.Collection","name":"_collections","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_contractAddress","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"getStake","outputs":[{"components":[{"internalType":"address","name":"contractAddress","type":"address"},{"internalType":"address","name":"ownerAddress","type":"address"},{"internalType":"uint256","name":"startTime","type":"uint256"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"internalType":"struct Optix.Stake","name":"_exists","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"hasMigrated","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"migrateFromv1","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"mint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"myActiveCollections","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_contractAddress","type":"address"},{"internalType":"uint256[]","name":"_tokenIds","type":"uint256[]"}],"name":"openStake","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"openStakes","outputs":[{"internalType":"address","name":"contractAddress","type":"address"},{"internalType":"address","name":"ownerAddress","type":"address"},{"internalType":"uint256","name":"startTime","type":"uint256"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_contractAddress","type":"address"}],"name":"removeCollection","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_contractAddress","type":"address"}],"name":"removeFreeCollection","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"thirdPartyFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_migrationMode","type":"uint256"}],"name":"updateMigrationMode","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_contractAddress","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"updateOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_payoutWallet","type":"address"}],"name":"updatePayoutWallet","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_fee","type":"uint256"}],"name":"updateThirdPartyFee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_v1ContractAddress","type":"address"}],"name":"updateV1Contract","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]