// File: TokenHub.sol



// SPDX-License-Identifier: -- DG --

pragma solidity =0.8.21;

import "./Interfaces.sol";
import "./TransferHelper.sol";
import "./AccessController.sol";
import "./EIP712MetaTransaction.sol";

contract TokenHub is
    AccessController,
    TransferHelper,
    EIP712MetaTransaction
{    
    uint256 public forwardFrame;
    address public forwardAddress;

    receive()
        external
        payable
    {
        emit ReceiveNative(
            msg.value
        );
    }

    mapping(address => bool) public supportedTokens;
    mapping(address => uint256) public forwardFrames;

    event Forward(
        address indexed depositorAddress,
        address indexed paymentTokenAddress,
        uint256 indexed paymentTokenAmount
    );

    event ForwardNative(
        address indexed depositorAddress,
        uint256 indexed paymentTokenAmount
    );

    event ReceiveNative(
        uint256 indexed nativeAmount
    );

    constructor(
        address _defaultToken,
        uint256 _defaultFrame,
        address _defaultAddress
    )
        EIP712Base(
            "TokenHub",
            "v3.0"
        )
    {
        forwardFrame = _defaultFrame;
        forwardAddress = _defaultAddress;        
        supportedTokens[_defaultToken] = true;
    }

    function forwardNative()
        external
        payable 
    {
        address _depositorAddress = msg.sender;

        require(
            canDepositAgain(_depositorAddress),
            "TokenHub: DEPOSIT_COOLDOWN"
        );

        forwardFrames[_depositorAddress] = block.number;

        payable(forwardAddress).transfer(
            msg.value
        );

        emit ForwardNative(
            msg.sender,
            msg.value
        );        
    }

    function forwardTokens(
        address _paymentToken,
        uint256 _paymentTokenAmount
    )
        external
    {
        address _depositorAddress = msg.sender;

        require(
            canDepositAgain(_depositorAddress),
            "TokenHub: DEPOSIT_COOLDOWN"
        );

        forwardFrames[_depositorAddress] = block.number;

        require(
            supportedTokens[_paymentToken],
            "TokenHub: UNSUPPORTED_TOKEN"
        );

        safeTransferFrom(
            _paymentToken,
            _depositorAddress,
            forwardAddress,
            _paymentTokenAmount
        );

        emit Forward(
            msg.sender,
            _paymentToken,
            _paymentTokenAmount
        );
    }

    function forwardTokensByWorker(
        address _depositorAddress,
        address _paymentTokenAddress,
        uint256 _paymentTokenAmount
    )
        external
        onlyWorker
    {
        require(
            canDepositAgain(_depositorAddress),
            "TokenHub: DEPOSIT_COOLDOWN"
        );

        forwardFrames[_depositorAddress] = block.number;

        require(
            supportedTokens[_paymentTokenAddress],
            "TokenHub: UNSUPPORTED_TOKEN"
        );

        safeTransferFrom(
            _paymentTokenAddress,
            _depositorAddress,
            forwardAddress,
            _paymentTokenAmount
        );

        emit Forward(
            _depositorAddress,
            _paymentTokenAddress,
            _paymentTokenAmount
        );
    }

    function changeForwardFrame(
        uint256 _newDepositFrame
    )
        external
        onlyCEO
    {
        forwardFrame = _newDepositFrame;
    }

    function changeForwardAddress(
        address _newForwardAddress
    )
        external
        onlyCEO
    {
        forwardAddress = _newForwardAddress;
    }

    function changeSupportedToken(
        address _tokenAddress,
        bool _supportStatus
    )
        external 
        onlyCEO
    {
        supportedTokens[_tokenAddress] = _supportStatus;
    }

    function canDepositAgain(
        address _depositorAddress
    )
        public
        view
        returns (bool)
    {
        return block.number - forwardFrames[_depositorAddress] >= forwardFrame;
    }
}
// File: EIP712MetaTransaction.sol



// SPDX-License-Identifier: -- vitally.eth --

pragma solidity =0.8.21;

import "./EIP712Base.sol";

abstract contract EIP712MetaTransaction is EIP712Base {

    bytes32 private constant META_TRANSACTION_TYPEHASH = keccak256(
        bytes(
            "MetaTransaction(uint256 nonce,address from,bytes functionSignature)"
        )
    );

    event MetaTransactionExecuted(
        address userAddress,
        address payable relayerAddress,
        bytes functionSignature
    );

    mapping(address => uint256) internal nonces;

    struct MetaTransaction {
		uint256 nonce;
		address from;
        bytes functionSignature;
	}

    function executeMetaTransaction(
        address _userAddress,
        bytes memory _functionSignature,
        bytes32 _sigR,
        bytes32 _sigS,
        uint8 _sigV
    )
        public
        payable
        returns(bytes memory)
    {
        MetaTransaction memory metaTx = MetaTransaction(
            {
                nonce: nonces[_userAddress],
                from: _userAddress,
                functionSignature: _functionSignature
            }
        );

        require(
            verify(
                _userAddress,
                metaTx,
                _sigR,
                _sigS,
                _sigV
            ), "EIP712MetaTransaction: INVALID_SIGNATURE"
        );

	    nonces[_userAddress] =
	    nonces[_userAddress] + 1;

        (bool success, bytes memory returnData) = address(this).call(
            abi.encodePacked(
                _functionSignature,
                _userAddress
            )
        );

        require(
            success,
            "EIP712MetaTransaction: INVALID_CALL"
        );

        emit MetaTransactionExecuted(
            _userAddress,
            payable(msg.sender),
            _functionSignature
        );

        return returnData;
    }

    function hashMetaTransaction(
        MetaTransaction memory _metaTx
    )
        internal
        pure
        returns (bytes32)
    {
		return keccak256(
		    abi.encode(
                META_TRANSACTION_TYPEHASH,
                _metaTx.nonce,
                _metaTx.from,
                keccak256(_metaTx.functionSignature)
            )
        );
	}

    function verify(
        address _user,
        MetaTransaction memory _metaTx,
        bytes32 _sigR,
        bytes32 _sigS,
        uint8 _sigV
    )
        internal
        view
        returns (bool)
    {
        address signer = ecrecover(
            toTypedMessageHash(
                hashMetaTransaction(_metaTx)
            ),
            _sigV,
            _sigR,
            _sigS
        );

        require(
            signer != address(0x0),
            "EIP712MetaTransaction: INVALID_SIGNATURE"
        );

		return signer == _user;
	}

    function msgSender()
        internal
        view
        returns(address sender)
    {
        if (msg.sender == address(this)) {

            bytes memory array = msg.data;
            uint256 index = msg.data.length;

            assembly {
                // Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those.
                sender := and(mload(add(array, index)), 0xffffffffffffffffffffffffffffffffffffffff)
            }
        } else {
            sender = msg.sender;
        }

        return sender;
    }

    function getNonce(
        address _user
    )
        external
        view
        returns(uint256 nonce)
    {
        nonce = nonces[_user];
    }
}

// File: AccessController.sol



// SPDX-License-Identifier: ---DG----

pragma solidity =0.8.21;

contract AccessController {

    address public ceoAddress;
    mapping (address => bool) public isWorker;

    event CEOSet(
        address newCEO
    );

    event WorkerAdded(
        address newWorker
    );

    event WorkerRemoved(
        address existingWorker
    );

    constructor() {

        address creator = msg.sender;
        ceoAddress = creator;
        isWorker[creator] = true;

        emit CEOSet(
            creator
        );

        emit WorkerAdded(
            creator
        );
    }

    modifier onlyCEO() {
        require(
            msg.sender == ceoAddress,
            "AccessControl: CEO_DENIED"
        );
        _;
    }

    modifier onlyWorker() {
        require(
            isWorker[msg.sender] == true,
            "AccessControl: WORKER_DENIED"
        );
        _;
    }

    modifier nonZeroAddress(
        address checkingAddress
    ) {
        require(
            checkingAddress != address(0x0),
            "AccessControl: INVALID_ADDRESS"
        );
        _;
    }

    function setCEO(
        address _newCEO
    )
        external
        nonZeroAddress(_newCEO)
        onlyCEO
    {
        ceoAddress = _newCEO;

        emit CEOSet(
            ceoAddress
        );
    }

    function addWorker(
        address _newWorker
    )
        external
        onlyCEO
    {
        _addWorker(
            _newWorker
        );
    }

    function addWorkerBulk(
        address[] calldata _newWorkers
    )
        external
        onlyCEO
    {
        for (uint8 index = 0; index < _newWorkers.length; index++) {
            _addWorker(_newWorkers[index]);
        }
    }

    function _addWorker(
        address _newWorker
    )
        internal
        nonZeroAddress(_newWorker)
    {
        require(
            isWorker[_newWorker] == false,
            'AccessControl: worker already exist'
        );

        isWorker[_newWorker] = true;

        emit WorkerAdded(
            _newWorker
        );
    }

    function removeWorker(
        address _existingWorker
    )
        external
        onlyCEO
    {
        _removeWorker(
            _existingWorker
        );
    }

    function removeWorkerBulk(
        address[] calldata _workerArray
    )
        external
        onlyCEO
    {
        for (uint8 index = 0; index < _workerArray.length; index++) {
            _removeWorker(_workerArray[index]);
        }
    }

    function _removeWorker(
        address _existingWorker
    )
        internal
        nonZeroAddress(_existingWorker)
    {
        require(
            isWorker[_existingWorker] == true,
            "AccessControl: worker not detected"
        );

        isWorker[_existingWorker] = false;

        emit WorkerRemoved(
            _existingWorker
        );
    }
}

// File: TransferHelper.sol



// SPDX-License-Identifier: ---DG----

pragma solidity =0.8.21;

contract TransferHelper {

    bytes4 private constant TRANSFER = bytes4(
        keccak256(
            bytes(
                "transfer(address,uint256)" // 0xa9059cbb
            )
        )
    );

    bytes4 private constant TRANSFER_FROM = bytes4(
        keccak256(
            bytes(
                "transferFrom(address,address,uint256)" // 0x23b872dd
            )
        )
    );

    function safeTransfer(
        address _token,
        address _to,
        uint256 _value
    )
        internal
    {
        (bool success, bytes memory data) = _token.call(
            abi.encodeWithSelector(
                TRANSFER, // 0xa9059cbb
                _to,
                _value
            )
        );

        require(
            success && (
                data.length == 0 || abi.decode(
                    data, (bool)
                )
            ),
            "TransferHelper: TRANSFER_FAILED"
        );
    }

    function safeTransferFrom(
        address _token,
        address _from,
        address _to,
        uint _value
    )
        internal
    {
        (bool success, bytes memory data) = _token.call(
            abi.encodeWithSelector(
                TRANSFER_FROM,
                _from,
                _to,
                _value
            )
        );

        require(
            success && (
                data.length == 0 || abi.decode(
                    data, (bool)
                )
            ),
            "TransferHelper: TRANSFER_FROM_FAILED"
        );
    }
}

// File: Interfaces.sol



// SPDX-License-Identifier: ---DG----

pragma solidity =0.8.21;

interface ERC721 {

    function ownerOf(
        uint256 _tokenId
    )
        external
        view
        returns (address);

    function transferFrom(
        address _from,
        address _to,
        uint256 _tokenId
    )
        external;
}

interface ERC20 {

    function approve(
        address spender,
        uint256 amount
    )
        external 
        returns (bool);
        
    function burn(
        uint256 _amount
    )
        external;
}

interface DGAccessories  {

    function issueTokens(
        address[] calldata _beneficiaries,
        uint256[] calldata _itemIds
    )
        external;

    function encodeTokenId(
        uint256 _itemId,
        uint256 _issuedId
    )
        external
        pure
        returns (uint256 id);

    function decodeTokenId(
        uint256 _tokenId
    )
        external
        pure
        returns (
            uint256 itemId,
            uint256 issuedId
        );

    function items(
        uint256 _id
    )
        external
        view
        returns (
            string memory rarity,
            uint256 maxSupply,
            uint256 totalSupply,
            uint256 price,
            address beneficiary,
            string memory metadata,
            string memory contentHash
        );

    function itemsCount()
        external
        view
        returns (uint256);
}

// File: EIP712Base.sol



// SPDX-License-Identifier: -- vitally.eth --

pragma solidity =0.8.21;

contract EIP712Base {

    struct EIP712Domain {
        string name;
        string version;
        uint256 chainId;
        address verifyingContract;
    }

    bytes32 internal constant EIP712_DOMAIN_TYPEHASH = keccak256(
        bytes(
            "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
        )
    );

    bytes32 internal domainSeperator;

    constructor(
        string memory _name,
        string memory _version
    ) {
        domainSeperator = keccak256(abi.encode(
			EIP712_DOMAIN_TYPEHASH,
			keccak256(bytes(_name)),
			keccak256(bytes(_version)),
			getChainID(),
			address(this)
		));
    }

    function getChainID()
        internal
        pure
        returns (uint256 id)
    {
		assembly {
			id := 1
		}
	}

    function getDomainSeperator()
        private
        view
        returns(bytes32)
    {
		return domainSeperator;
	}

    function toTypedMessageHash(
        bytes32 _messageHash
    )
        internal
        view
        returns(bytes32)
    {
        return keccak256(
            abi.encodePacked(
                "\x19\x01",
                getDomainSeperator(),
                _messageHash
            )
        );
    }
}