// Original license: SPDX_License_Identifier: MIT
pragma solidity ^0.8.9;
library TransferHelper {
    /// @notice Transfers tokens from the targeted address to the given destination
    /// @notice Errors with 'STF' if transfer fails
    /// @param token The contract address of the token to be transferred
    /// @param from The originating address from which the tokens will be transferred
    /// @param to The destination address of the transfer
    /// @param value The amount to be transferred
    function safeTransferFrom(
        address token,
        address from,
        address to,
        uint256 value
    ) internal {
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(
                IERC20.transferFrom.selector,
                from,
                to,
                value
            )
        );
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            "STF"
        );
    }

    /// @notice Transfers tokens from msg.sender to a recipient
    /// @dev Errors with ST if transfer fails
    /// @param token The contract address of the token which will be transferred
    /// @param to The recipient of the transfer
    /// @param value The value of the transfer
    function safeTransfer(address token, address to, uint256 value) internal {
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(IERC20.transfer.selector, to, value)
        );
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            "ST"
        );
    }

    /// @notice Approves the stipulated contract to spend the given allowance in the given token
    /// @dev Errors with 'SA' if transfer fails
    /// @param token The contract address of the token to be approved
    /// @param to The target of the approval
    /// @param value The amount of the given token the target will be allowed to spend
    function safeApprove(address token, address to, uint256 value) internal {
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(IERC20.approve.selector, to, value)
        );
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            "SA"
        );
    }

    /// @notice Transfers ETH to the recipient address
    /// @dev Fails with `STE`
    /// @param to The destination of the transfer
    /// @param value The value to be transferred
    function safeTransferETH(address to, uint256 value) internal {
        (bool success, ) = to.call{value: value}(new bytes(0));
        require(success, "STE");
    }
}

interface IJJRewardV1 {
    struct User {
        uint256 claimed;
        uint64 latestTimestamp;
    }

    function userClaim(address) external view returns (User memory);
}

contract JJRewardV2 is
    Initializable,
    PausableUpgradeable,
    OwnableUpgradeable,
    UUPSUpgradeable
{
    using TransferHelper for IERC20Upgradeable;

    struct User {
        uint256 claimed;
        uint64 latestTimestamp;
    }
    bool inClaim;

    mapping(address => bytes32) public claimTokenRoot;
    mapping(address => bool) public operators;
    mapping(address => mapping(address => User)) public userTokenClaim;
    address public jjReward;

    event Received(address Token, address Sender, uint256 Value);
    event SetOperator(address Operator, bool Flag);
    event ClaimedSuccess(address Token, address Receiver, uint256 Amount);
    event UpdateClaimRoot(address Token, address Sender, uint256 Timestamp);
    event UpdateJJRewardv1(address target);

    modifier onlyOperator() {
        require(operators[msg.sender], "onlyOperatorCall");
        _;
    }

    modifier claiming() {
        require(!inClaim, "inClaiming");
        inClaim = true;
        _;
        inClaim = false;
    }

    struct ClaimRewards {
        address token;
        uint256 amount;
        bytes32[] proof;
    }

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        initialize();
        _disableInitializers();
    }

    function initialize() public initializer {
        __Pausable_init();
        __Ownable_init();
        __UUPSUpgradeable_init();
        inClaim = false;
        operators[_msgSender()] = true;
        emit SetOperator(_msgSender(), true);
    }

    receive() external payable {
        emit Received(address(0), msg.sender, msg.value);
    }

    function claim(
        address _token,
        uint256 _amount,
        bytes32[] memory _proof
    ) external claiming whenNotPaused {
        require(tx.origin == _msgSender(), "OnlyOrigin");
        require(claimTokenRoot[_token] != bytes32(0), "NotInit");
        require(
            checkoutEligibility(_token, _msgSender(), _amount, _proof),
            "VerifyFailed!"
        );
        User memory user = userTokenClaim[_msgSender()][_token];
        // @Note: deal with v1 ; multichain no neet this part
        if (_token == address(0) && jjReward != address(0)) {
            IJJRewardV1.User memory v1user = IJJRewardV1(jjReward).userClaim(
                _msgSender()
            );
            if (v1user.claimed > 0 && user.claimed == 0) {
                user.claimed = v1user.claimed;
            }
        }
        require(_amount >= user.claimed, "NoReward");
        uint256 pendingClaim = _amount - user.claimed;
        user.claimed = _amount;
        user.latestTimestamp = uint64(block.timestamp);
        userTokenClaim[_msgSender()][_token] = user;

        if (_token == address(0)) {
            TransferHelper.safeTransferETH(_msgSender(), pendingClaim);
        } else {
            TransferHelper.safeTransfer(_token, _msgSender(), pendingClaim);
        }

        emit ClaimedSuccess(_token, _msgSender(), pendingClaim);
    }

    function claimMultishop(
        ClaimRewards[] memory claimRewards
    ) external claiming whenNotPaused {
        require(tx.origin == _msgSender(), "OnlyOrigin");
        for (uint i = 0; i < claimRewards.length; i++) {
            require(
                claimTokenRoot[claimRewards[i].token] != bytes32(0),
                "NotInit"
            );
            require(
                checkoutEligibility(
                    claimRewards[i].token,
                    _msgSender(),
                    claimRewards[i].amount,
                    claimRewards[i].proof
                ),
                "VerifyFailed!"
            );
            User memory user = userTokenClaim[_msgSender()][
                claimRewards[i].token
            ];
            // @Note: deal with v1 ; multichain no neet this part
            if (claimRewards[i].token == address(0) && jjReward != address(0)) {
                IJJRewardV1.User memory v1user = IJJRewardV1(jjReward)
                    .userClaim(_msgSender());
                if (v1user.claimed > 0 && user.claimed == 0) {
                    user.claimed = v1user.claimed;
                }
            }

            require(claimRewards[i].amount >= user.claimed, "NoReward");
            uint256 pendingClaim = claimRewards[i].amount - user.claimed;
            user.claimed = claimRewards[i].amount;
            user.latestTimestamp = uint64(block.timestamp);
            userTokenClaim[_msgSender()][claimRewards[i].token] = user;

            if (claimRewards[i].token == address(0)) {
                TransferHelper.safeTransferETH(_msgSender(), pendingClaim);
            } else {
                TransferHelper.safeTransfer(
                    claimRewards[i].token,
                    _msgSender(),
                    pendingClaim
                );
            }
            emit ClaimedSuccess(
                claimRewards[i].token,
                _msgSender(),
                pendingClaim
            );
        }
    }

    function pause() public onlyOwner {
        _pause();
    }

    function unpause() public onlyOwner {
        _unpause();
    }

    function _authorizeUpgrade(
        address newImplementation
    ) internal override onlyOwner {}

    function setOperator(address operator, bool flag) public onlyOwner {
        operators[operator] = flag;
        emit SetOperator(operator, flag);
    }

    function setClaimRoot(
        address token,
        bytes32 root_hash
    ) public onlyOperator {
        claimTokenRoot[token] = root_hash;
        emit UpdateClaimRoot(token, _msgSender(), block.timestamp);
    }

    function setRewardv1(address target) external onlyOwner {
        jjReward = target;
        emit UpdateJJRewardv1(target);
    }

    function withdrawPayments(address token_) external onlyOwner {
        if (token_ == address(0x0)) {
            (bool sent, ) = msg.sender.call{value: address(this).balance}(
                new bytes(0)
            );
            require(sent, "WithdrawFail");
            return;
        }
        IERC20Upgradeable token = IERC20Upgradeable(token_);
        uint256 amount = token.balanceOf(address(this));
        token.transfer(msg.sender, amount);
    }

    function getUserClaimed(
        address target,
        address token
    ) public view returns (User memory) {
        User memory user = userTokenClaim[target][token];
        if (
            token == address(0) &&
            user.latestTimestamp == 0 &&
            jjReward != address(0)
        ) {
            IJJRewardV1.User memory v1user = IJJRewardV1(jjReward).userClaim(
                target
            );
            if (v1user.claimed > 0 && user.claimed == 0) {
                user.claimed += v1user.claimed;
                user.latestTimestamp = v1user.latestTimestamp;
            }
        }
        return user;
    }

    function checkoutEligibility(
        address token,
        address account,
        uint256 amount,
        bytes32[] memory proof
    ) public view returns (bool) {
        return
            MerkleProofUpgradeable.verify(
                proof,
                claimTokenRoot[token],
                _getKey(account, amount)
            );
    }

    function _getKey(
        address owner_,
        uint256 amount
    ) internal pure returns (bytes32) {
        bytes memory n = abi.encodePacked(_trs(owner_), "-", _uint2str(amount));
        bytes32 q = keccak256(n);
        return q;
    }

    function _uint2str(
        uint256 _i
    ) internal pure returns (bytes 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 bstr;
    }

    function _trs(address a) internal pure returns (string memory) {
        return _toString(abi.encodePacked(a));
    }

    function _toString(
        bytes memory data
    ) internal pure returns (string memory) {
        bytes memory alphabet = "0123456789abcdef";
        bytes memory str = new bytes(2 + data.length * 2);
        str[0] = "0";
        str[1] = "x";
        for (uint256 i = 0; i < data.length; i++) {
            str[2 + i * 2] = alphabet[uint256(uint8(data[i] >> 4))];
            str[3 + i * 2] = alphabet[uint256(uint8(data[i] & 0x0f))];
        }
        return string(str);
    }
}
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"previousAdmin","type":"address"},{"indexed":false,"internalType":"address","name":"newAdmin","type":"address"}],"name":"AdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"beacon","type":"address"}],"name":"BeaconUpgraded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"Token","type":"address"},{"indexed":false,"internalType":"address","name":"Receiver","type":"address"},{"indexed":false,"internalType":"uint256","name":"Amount","type":"uint256"}],"name":"ClaimedSuccess","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint8","name":"version","type":"uint8"}],"name":"Initialized","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":false,"internalType":"address","name":"account","type":"address"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"Token","type":"address"},{"indexed":false,"internalType":"address","name":"Sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"Value","type":"uint256"}],"name":"Received","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"Operator","type":"address"},{"indexed":false,"internalType":"bool","name":"Flag","type":"bool"}],"name":"SetOperator","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Unpaused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"Token","type":"address"},{"indexed":false,"internalType":"address","name":"Sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"Timestamp","type":"uint256"}],"name":"UpdateClaimRoot","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"target","type":"address"}],"name":"UpdateJJRewardv1","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"implementation","type":"address"}],"name":"Upgraded","type":"event"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"address","name":"account","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"bytes32[]","name":"proof","type":"bytes32[]"}],"name":"checkoutEligibility","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"},{"internalType":"bytes32[]","name":"_proof","type":"bytes32[]"}],"name":"claim","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"bytes32[]","name":"proof","type":"bytes32[]"}],"internalType":"struct JJRewardV2.ClaimRewards[]","name":"claimRewards","type":"tuple[]"}],"name":"claimMultishop","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"claimTokenRoot","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"},{"internalType":"address","name":"token","type":"address"}],"name":"getUserClaimed","outputs":[{"components":[{"internalType":"uint256","name":"claimed","type":"uint256"},{"internalType":"uint64","name":"latestTimestamp","type":"uint64"}],"internalType":"struct JJRewardV2.User","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"jjReward","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"operators","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"proxiableUUID","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"bytes32","name":"root_hash","type":"bytes32"}],"name":"setClaimRoot","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"flag","type":"bool"}],"name":"setOperator","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"}],"name":"setRewardv1","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newImplementation","type":"address"}],"name":"upgradeTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newImplementation","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"upgradeToAndCall","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"userTokenClaim","outputs":[{"internalType":"uint256","name":"claimed","type":"uint256"},{"internalType":"uint64","name":"latestTimestamp","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token_","type":"address"}],"name":"withdrawPayments","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]