contract Escrow_v1_0 {

    using SafeMath for uint256;

    enum Status {FUNDED, RELEASED}

    enum TransactionType {ETHER, TOKEN}

    event Executed(
        bytes32 indexed scriptHash,
        address[] destinations,
        uint256[] amounts
    );

    event FundAdded(
        bytes32 indexed scriptHash,
        address indexed from,
        uint256 valueAdded
    );

    event Funded(
        bytes32 indexed scriptHash,
        address indexed from,
        uint256 value
    );

    struct Transaction {
        uint256 value;
        uint256 lastModified; //time txn was last modified (in seconds)
        Status status;
        TransactionType transactionType;
        uint8 threshold;
        uint32 timeoutHours;
        address buyer;
        address seller;
        address tokenAddress; //token address in case of token transfer
        address moderator;
        mapping(address => bool) isOwner; //to keep track of owners.
        mapping(address => bool) voted; //to keep track of who all voted
        mapping(address => bool) beneficiaries; //beneficiaries of execution
    }

    mapping(bytes32 => Transaction) public transactions;

    uint256 public transactionCount = 0;

    //Contains mapping between each party and all of their transactions
    mapping(address => bytes32[]) private partyVsTransaction;

    modifier transactionExists(bytes32 scriptHash) {
        require(
            transactions[scriptHash].value != 0, "Transaction does not exist"
        );
        _;
    }

    modifier transactionDoesNotExist(bytes32 scriptHash) {
        require(transactions[scriptHash].value == 0, "Transaction exists");
        _;
    }

    modifier inFundedState(bytes32 scriptHash) {
        require(
            transactions[scriptHash].status == Status.FUNDED,
            "Transaction is not in FUNDED state"
        );
        _;
    }

    modifier nonZeroAddress(address addressToCheck) {
        require(addressToCheck != address(0), "Zero address passed");
        _;
    }

    modifier checkTransactionType(
        bytes32 scriptHash,
        TransactionType transactionType
    )
    {
        require(
            transactions[scriptHash].transactionType == transactionType,
            "Transaction type does not match"
        );
        _;
    }

    modifier onlyBuyer(bytes32 scriptHash) {
        require(
            msg.sender == transactions[scriptHash].buyer,
            "The initiator of the transaction is not buyer"
        );
        _;
    }

    /**
    * @dev Add new transaction in the contract
    * @param buyer The buyer of the transaction
    * @param seller The seller of the listing associated with the transaction
    * @param moderator Moderator for this transaction
    * @param threshold Minimum number of signatures required to released funds
    * @param timeoutHours Hours after which seller can release funds into his
    * favour by signing transaction unilaterally
    * @param scriptHash keccak256 hash of the redeem script
    * @param uniqueId bytes20 unique id for the transaction, generated by
    * ETH wallet
    * Redeem Script format will be following
    * <uniqueId: 20><threshold:1><timeoutHours:4><buyer:20><seller:20>
    * <moderator:20><multisigAddress:20>
    * Pass amount of the ETH to be put in escrow
    */
    function addTransaction(
        address buyer,
        address seller,
        address moderator,
        uint8 threshold,
        uint32 timeoutHours,
        bytes32 scriptHash,
        bytes20 uniqueId
    )
        external
        payable
        transactionDoesNotExist(scriptHash)
        nonZeroAddress(buyer)
        nonZeroAddress(seller)
    {
        _addTransaction(
            buyer,
            seller,
            moderator,
            threshold,
            timeoutHours,
            scriptHash,
            msg.value,
            uniqueId,
            TransactionType.ETHER,
            address(0)
        );

        emit Funded(scriptHash, msg.sender, msg.value);

    }

    /**
    * @dev Add new transaction in the contract
    * @param buyer The buyer of the transaction
    * @param seller The seller of the listing associated with the transaction
    * @param moderator Moderator for this transaction
    * @param threshold Minimum number of signatures required to released funds
    * @param timeoutHours Hours after which seller can release funds into his
    * favour by signing transaction unilaterally
    * @param scriptHash keccak256 hash of the redeem script
    * @param value Amount of tokens to be put in escrow
    * @param uniqueId bytes20 unique id for the transaction, generated by
    * ETH wallet
    * @param tokenAddress Address of the token to be used
    * Redeem Script format will be following:
    * <uniqueId: 20><threshold:1><timeoutHours:4><buyer:20><seller:20>
    * <moderator:20><multisigAddress:20><tokenAddress:20>
    * Approve escrow contract to spend amount of token on sender's behalf
    */
    function addTokenTransaction(
        address buyer,
        address seller,
        address moderator,
        uint8 threshold,
        uint32 timeoutHours,
        bytes32 scriptHash,
        uint256 value,
        bytes20 uniqueId,
        address tokenAddress
    )
        external
        transactionDoesNotExist(scriptHash)
        nonZeroAddress(buyer)
        nonZeroAddress(seller)
        nonZeroAddress(tokenAddress)
    {

        _addTransaction(
            buyer,
            seller,
            moderator,
            threshold,
            timeoutHours,
            scriptHash,
            value,
            uniqueId,
            TransactionType.TOKEN,
            tokenAddress
        );

        ITokenContract token = ITokenContract(tokenAddress);

        require(
            token.transferFrom(msg.sender, address(this), value),
            "Token transfer failed, maybe you did not approve escrow contract to spend on behalf of sender"
        );
        emit Funded(scriptHash, msg.sender, value);
    }

    /**
    * @dev This method will check whether given address was a beneficiary of
    * transaction execution or not
    * @param scriptHash script hash of the transaction
    * @param beneficiary Beneficiary address to be checked
    */
    function checkBeneficiary(
        bytes32 scriptHash,
        address beneficiary
    )
        external
        view
        returns (bool)
    {
        return transactions[scriptHash].beneficiaries[beneficiary];
    }

    /**
    * @dev This method will check whether given party has voted or not
    * @param scriptHash script hash of the transaction
    * @param party Address of the party whose vote has to be checked
    * @return bool vote
    */
    function checkVote(
        bytes32 scriptHash,
        address party
    )
        external
        view
        returns (bool)
    {
        return transactions[scriptHash].voted[party];
    }

    /**
    * @dev Allows buyer of the transaction to add more funds(ether) in the
    * transaction.
    * This will help to cater scenarios wherein initially buyer missed to
    * fund transaction as required
    * @param scriptHash script hash of the transaction
    * Only buyer of the transaction can invoke this method
    */
    function addFundsToTransaction(
        bytes32 scriptHash
    )
        external
        payable
        transactionExists(scriptHash)
        inFundedState(scriptHash)
        checkTransactionType(scriptHash, TransactionType.ETHER)
        onlyBuyer(scriptHash)

    {

        require(msg.value > 0, "Value must be greater than zero.");

        transactions[scriptHash].value = transactions[scriptHash].value
            .add(msg.value);

        emit FundAdded(scriptHash, msg.sender, msg.value);
    }

    /**
    * @dev Allows buyer of the transaction to add more funds(Tokens) in the
    * transaction.
    * This will help to cater scenarios wherein initially buyer missed to
    * fund transaction as required
    * @param scriptHash script hash of the transaction
    * Only buyer of the transaction can invoke this method
    */
    function addTokensToTransaction(
        bytes32 scriptHash,
        uint256 value
    )
        external
        transactionExists(scriptHash)
        inFundedState(scriptHash)
        checkTransactionType(scriptHash, TransactionType.TOKEN)
        onlyBuyer(scriptHash)
    {

        require(value > 0, "Value must be greater than zero.");

        ITokenContract token = ITokenContract(
            transactions[scriptHash].tokenAddress
        );

        require(
            token.transferFrom(msg.sender, address(this), value),
            "Token transfer failed, maybe you did not approve the escrow contract to spend on behalf of the buyer"
        );

        transactions[scriptHash].value = transactions[scriptHash].value
            .add(value);

        emit FundAdded(scriptHash, msg.sender, value);
    }

    /**
    *@dev Returns all transaction ids for a party
    *@param partyAddress Address of the party
    */
    function getAllTransactionsForParty(
        address partyAddress
    )
        external
        view
        returns (bytes32[])
    {
        return partyVsTransaction[partyAddress];
    }

    /**
    *@dev This method will be used to release funds associated with
    * the transaction
    * Please see specs https://github.com/OpenBazaar/smart-contracts/blob/master/contracts/escrow/EscrowSpec.md
    *@param sigV Array containing V component of all the signatures
    *@param sigR Array containing R component of all the signatures
    *@param signS Array containing S component of all the signatures
    *@param scriptHash script hash of the transaction
    *@param destinations List of addresses who will receive funds
    *@param amounts amount released to each destination
    */
    function execute(
        uint8[] sigV,
        bytes32[] sigR,
        bytes32[] sigS,
        bytes32 scriptHash,
        address[] destinations,
        uint256[] amounts
    )
        external
        transactionExists(scriptHash)
        inFundedState(scriptHash)
    {

        require(
            destinations.length > 0,
            "Number of destinations must be greater than 0"
        );
        require(
            destinations.length == amounts.length,
            "Number of destinations must match number of values sent"
        );

        _verifyTransaction(
            sigV,
            sigR,
            sigS,
            scriptHash,
            destinations,
            amounts
        );

        transactions[scriptHash].status = Status.RELEASED;
        //Last modified timestamp modified, which will be used by rewards
        transactions[scriptHash].lastModified = block.timestamp;
        require(
            _transferFunds(scriptHash, destinations, amounts) == transactions[scriptHash].value,
            "Total value to be released must be equal to the transaction escrow value"
        );

        emit Executed(scriptHash, destinations, amounts);
    }


    /**
    *@dev Method for calculating script hash. Calculation will depend upon
    * the type of transaction
    * ETHER Type transaction-:
    * Script Hash- keccak256(uniqueId, threshold, timeoutHours, buyer, seller,
    * moderator, multiSigContractAddress)
    * TOKEN Type transaction
    * Script Hash- keccak256(uniqueId, threshold, timeoutHours, buyer, seller,
    * moderator, multiSigContractAddress, tokenAddress)
    * Client can use this method to verify whether it has calculated correct
    * script hash or not
    */
    function calculateRedeemScriptHash(
        bytes20 uniqueId,
        uint8 threshold,
        uint32 timeoutHours,
        address buyer,
        address seller,
        address moderator,
        address tokenAddress
    )
        public
        view
        returns (bytes32)
    {
        if (tokenAddress == address(0)) {
            return keccak256(
                abi.encodePacked(
                    uniqueId,
                    threshold,
                    timeoutHours,
                    buyer,
                    seller,
                    moderator,
                    address(this)
                )
            );
        } else {
            return keccak256(
                abi.encodePacked(
                    uniqueId,
                    threshold,
                    timeoutHours,
                    buyer,
                    seller,
                    moderator,
                    address(this),
                    tokenAddress
                )
            );
        }
    }

    /**
    * @dev This methods checks validity of transaction
    * 1. Verify Signatures
    * 2. Check if minimum number of signatures has been acquired
    * 3. If above condition is false, check if timelock is expired or if the
    *    execution is signed by seller
    */
    function _verifyTransaction(
        uint8[] sigV,
        bytes32[] sigR,
        bytes32[] sigS,
        bytes32 scriptHash,
        address[] destinations,
        uint256[] amounts
    )
        private
    {
        _verifySignatures(
            sigV,
            sigR,
            sigS,
            scriptHash,
            destinations,
            amounts
        );

        bool timeLockExpired = _isTimeLockExpired(
            transactions[scriptHash].timeoutHours,
            transactions[scriptHash].lastModified
        );

        //if the minimum number of signatures are not gathered and either
        //timelock has not expired or transaction was not signed by seller
        //then revert
        if (sigV.length < transactions[scriptHash].threshold) {
            if (!timeLockExpired) {
                revert("Min number of sigs not present and timelock not expired");
            }
            else if (!transactions[scriptHash].voted[transactions[scriptHash].seller]) {
                revert("Min number of sigs not present and seller did not sign");
            }
        }
    }

    /**
    *@dev Private method to transfer funds to the destination addresses on
    * the basis of transaction type
    */
    function _transferFunds(
        bytes32 scriptHash,
        address[]destinations,
        uint256[]amounts
    )
        private
        returns (uint256)
    {
        Transaction storage t = transactions[scriptHash];

        uint256 valueTransferred = 0;

        if (t.transactionType == TransactionType.ETHER) {
            for (uint256 i = 0; i < destinations.length; i++) {

                require(
                    destinations[i] != address(0),
                    "zero address is not allowed as destination address"
                );

                require(
                    t.isOwner[destinations[i]],
                    "Destination address is not one of the owners"
                );

                require(
                    amounts[i] > 0,
                    "Amount to be sent should be greater than 0"
                );

                valueTransferred = valueTransferred.add(amounts[i]);

                //add receiver as beneficiary
                t.beneficiaries[destinations[i]] = true;
                destinations[i].transfer(amounts[i]);
            }

        } else if (t.transactionType == TransactionType.TOKEN) {

            ITokenContract token = ITokenContract(t.tokenAddress);

            for (uint256 j = 0; j<destinations.length; j++) {

                require(
                    destinations[j] != address(0),
                    "zero address is not allowed as destination address"
                );

                require(
                    t.isOwner[destinations[j]],
                    "Destination address is not one of the owners"
                );

                require(
                    amounts[j] > 0,
                    "Amount to be sent should be greater than 0"
                );

                valueTransferred = valueTransferred.add(amounts[j]);

                //add receiver as beneficiary
                t.beneficiaries[destinations[j]] = true;

                require(
                    token.transfer(destinations[j], amounts[j]),
                    "Token transfer failed."
                );
            }
        }
        return valueTransferred;
    }


    /**
    *@dev Checks whether the signatures are valid or not and marks signers as
    * having "voted".
    */
    function _verifySignatures(
        uint8[] sigV,
        bytes32[] sigR,
        bytes32[] sigS,
        bytes32 scriptHash,
        address[] destinations,
        uint256[]amounts
    )
        private
    {
        require(sigR.length == sigS.length, "R,S length mismatch");
        require(sigR.length == sigV.length, "R,V length mismatch");

        // Follows ERC191 signature scheme: https://github.com/ethereum/EIPs/issues/191
        bytes32 txHash = keccak256(
            abi.encodePacked(
                "\x19Ethereum Signed Message:\n32",
                keccak256(
                    abi.encodePacked(
                        byte(0x19),
                        byte(0),
                        address(this),
                        destinations,
                        amounts,
                        scriptHash
                    )
                )
            )
        );

        for (uint i = 0; i < sigR.length; i++) {

            address recovered = ecrecover(
                txHash,
                sigV[i],
                sigR[i],
                sigS[i]
            );

            require(
                transactions[scriptHash].isOwner[recovered],
                "Invalid signature"
            );
            require(
                !transactions[scriptHash].voted[recovered],
                "Same signature sent twice"
            );
            transactions[scriptHash].voted[recovered] = true;
        }
    }

    function _isTimeLockExpired(
        uint32 timeoutHours,
        uint256 lastModified
    )
        private
        view
        returns (bool)
    {
        uint256 timeSince = now.sub(lastModified);
        return (
            timeoutHours == 0 ? false : timeSince > uint256(timeoutHours).mul(3600)
        );
    }

    /**
    * Private method to add transaction to reduce code redundancy
    */
    function _addTransaction(
        address buyer,
        address seller,
        address moderator,
        uint8 threshold,
        uint32 timeoutHours,
        bytes32 scriptHash,
        uint256 value,
        bytes20 uniqueId,
        TransactionType transactionType,
        address tokenAddress
    )
        private
    {
        require(buyer != seller, "Buyer and seller are same");

        //value passed should be greater than 0
        require(value > 0, "Value passed is 0");

        // For now allowing 0 moderator to support 1-2 multisig wallet
        require(threshold > 0, "Threshold must be greater than 0");
        require(threshold <= 3, "Threshold must not be greater than 3");

        //if threshold is 1 then moderator can be any address
        //otherwise moderator should be a valid address
        require(
            threshold == 1 || moderator != address(0),
            "Either threshold should be 1 or valid moderator address should be passed"
        );

        require(
            scriptHash == calculateRedeemScriptHash(
                uniqueId,
                threshold,
                timeoutHours,
                buyer,
                seller,
                moderator,
                tokenAddress
            ),
            "Calculated script hash does not match passed script hash."
        );

        transactions[scriptHash] = Transaction({
            buyer: buyer,
            seller: seller,
            moderator: moderator,
            value: value,
            status: Status.FUNDED,
            lastModified: block.timestamp,
            threshold: threshold,
            timeoutHours: timeoutHours,
            transactionType:transactionType,
            tokenAddress:tokenAddress
        });

        transactions[scriptHash].isOwner[seller] = true;
        transactions[scriptHash].isOwner[buyer] = true;

        //check if buyer or seller are not passed as moderator
        require(
            !transactions[scriptHash].isOwner[moderator],
            "Either buyer or seller is passed as moderator"
        );

        //set moderator as one of the owners only if threshold is greater than
        // 1 otherwise only buyer and seller should be able to release funds
        if (threshold > 1) {
            transactions[scriptHash].isOwner[moderator] = true;
        }


        transactionCount++;

        partyVsTransaction[buyer].push(scriptHash);
        partyVsTransaction[seller].push(scriptHash);
    }
}
/**
 * @dev Supports ERC20 tokens
 * The escrow smart contract for the OpenBazaar trades in Ethereum
 * The smart contract is designed keeping in mind the current wallet interface
 * of OB-core:
 * https://github.com/OpenBazaar/wallet-interface/blob/master/wallet.go
 * Current wallet interface strictly adheres to UTXO(bitcoin) model
 * Please read below mentioned link for detailed specs
 * https://github.com/OpenBazaar/smart-contracts/blob/master/contracts/escrow/EscrowSpec.md
 */
function calculateRedeemScriptHash(
        bytes20 uniqueId,
        uint8 threshold,
        uint32 timeoutHours,
        address buyer,
        address seller,
        address moderator,
        address tokenAddress
    )
        public
        view
        returns (bytes32)
    {
        if (tokenAddress == address(0)) {
            return keccak256(
                abi.encodePacked(
                    uniqueId,
                    threshold,
                    timeoutHours,
                    buyer,
                    seller,
                    moderator,
                    address(this)
                )
            );
        } else {
            return keccak256(
                abi.encodePacked(
                    uniqueId,
                    threshold,
                    timeoutHours,
                    buyer,
                    seller,
                    moderator,
                    address(this),
                    tokenAddress
                )
            );
        }
    }
/**
 *@dev Method for calculating script hash. Calculation will depend upon
 * the type of transaction
 * ETHER Type transaction-:
 * Script Hash- keccak256(uniqueId, threshold, timeoutHours, buyer, seller,
 * moderator, multiSigContractAddress)
 * TOKEN Type transaction
 * Script Hash- keccak256(uniqueId, threshold, timeoutHours, buyer, seller,
 * moderator, multiSigContractAddress, tokenAddress)
 * Client can use this method to verify whether it has calculated correct
 * script hash or not
 */