pragma solidity ^0.8.26;
pragma experimental ABIEncoderV2;
import "./Interfaces/IERC20.sol";
import "./Interfaces/IElasticERC20.sol";
import "./Libraries/structs.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV2V3Interface.sol";
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";


// Upgradeable
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol";

interface IVerifier {
    function verify(bytes calldata, bytes32[] calldata) external view returns (bool);
    function getVerificationKeyHash() external pure returns (bytes32);
}

interface IOracle {
    function getCost(
        uint256 _amount,
        address _chainlinkFeed,
        address _xftPool
    ) external view returns (uint256);

    function chainlinkPrice(address _chainlinkFeed) external view returns (uint256);
}

interface IWETH9 is IERC20{ 
    function deposit() external payable;
    function withdraw(uint256 _amount) external;
}

interface IMomiji {
    function publish(bytes calldata _proof, Batch calldata _batch) external;
}

contract Momiji is Initializable, UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeable, EIP712Upgradeable {

    address constant _weth9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    uint256 constant MAX_FIELD_VALUE = 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000000;
    uint256 constant FIELD_MODULUS = MAX_FIELD_VALUE + 1;
    bytes32 constant ZERO_VALUE = 0x016a430aa58685aba1311244a973a3bc358859da86784be51094368e8fb6f720;
    bytes32 constant ZERO_UTXO_ROOT = 0x11d25ff6aa8a431fbce8e8d9a87a2d7986adf38e724fbe47f15752d0931f14d8;
    bytes32 constant INITIAL_STATE = 0x06f93f503e77fcdcacfe622e66adc639b63e8c0083f5cab5d71d461aa4562c92;
    bytes32 constant COINBASE_PAYMENT = 0x0000000000000000000000000000000000000000000000000000000000000003;
    bytes32 constant BROADCASTER_PAYMENT = 0x0000000000000000000000000000000000000000000000000000000000000002;
    bytes32 constant PROVER_PAYMENT = 0x0000000000000000000000000000000000000000000000000000000000000001;
    bytes32 constant NO_PAYMENT = 0x0000000000000000000000000000000000000000000000000000000000000000;

    IWETH9 constant weth9 = IWETH9(_weth9);
    IVerifier public verifier;
    IElasticERC20 public token;
    IOracle public oracle;
    ISwapRouter public swapRouter;

    bytes32[] public validRoots;
    bytes32 public txKeyHash;
    bytes32 public txWrapperKeyHash;
    bytes32 public recursiveKeyHash;
    uint256 public STATE_DEPTH;
    uint256 public MAX_ITEMS;
    uint256 public MAX_UTXOS;
    bytes32 public merkleRoot;
    bytes32 public histRoot;
    
    // Training Wheels
    uint256 public dailyCap;
    uint256 public dailyMint;
    uint256 public lastCapReset;
    uint256 public burnPercentageSwap;

    event EncryptedUTXOBroadcast(bytes32 indexed _oldRoot, bytes32 indexed _utxoId, EncryptedUTXO _encryptedUTXO);
    event TransactionBroadcast(TransactionWithProof _tx, bytes32 indexed _merkleRoot, bytes32 indexed _txId);
    event TransactionPublish(Transaction _tx, bytes32 indexed _merkleRoot, bytes32 indexed _txId);
    event BatchPublish(uint256 indexed _batchNumber, bytes32 _oldRoot, bytes32 indexed _newRoot, bytes32 indexed _oracle, bytes32[20] _historicPath);
    event BroadcastAddress(bytes32 indexed _merkleRoot, string indexed _address); // _address is a libp2p address

    // Training Wheels
    event DailyCapChanged(uint256 indexed _oldCap, uint256 indexed _newCap);
    event BurnPercentageChanged(uint256 indexed _oldPercentage, uint256 indexed _newPercentage);
    
    mapping(bytes32 => bool) public nullifierHashes;
    mapping(bytes32 => bytes32) public utxoPrevRoots;
    mapping(bytes32 => bytes32) public relay;

    uint256 public batchNumber;
    address public xftPool;

    address public xft;
    uint24 public fee;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }
    function initialize(
        IVerifier _verifier, 
        address _token, 
        bytes32 _txKeyHash, 
        bytes32 _txWrapperKeyHash, 
        bytes32 _recursiveKeyHash, 
        address _xftPool
    ) public initializer {
        __Pausable_init();
        __Ownable_init(msg.sender);
        __UUPSUpgradeable_init();
        verifier = _verifier;
        token = IElasticERC20(_token);
        xft = _token;
        txKeyHash = _txKeyHash;
        txWrapperKeyHash = _txWrapperKeyHash;
        recursiveKeyHash = _recursiveKeyHash;
        xftPool = _xftPool;

        swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
        STATE_DEPTH = 20;
        MAX_ITEMS = 16;
        MAX_UTXOS = 16;
        merkleRoot = ZERO_VALUE;
        validRoots.push(merkleRoot);
        histRoot = INITIAL_STATE;
        fee = 3000;
        burnPercentageSwap = 5;
        dailyCap = 1;
        __EIP712_init("Momiji", "1");
    }

    
    function _authorizeUpgrade(address newImplementation)
        internal
        onlyOwner
        override
    {}

    modifier nonreentrant() {
        assembly {
            if tload(0) { revert(0, 0) }
            tstore(0, 1)
        }
        _;
        assembly {
            tstore(0, 0)
        }
    }

    receive() external payable {
        if (msg.sender != _weth9) {
            msg.sender.call{value: msg.value}("");
        }
    }

    fallback() external payable {
        if (msg.sender != _weth9) {
            msg.sender.call{value: msg.value}("");
        }
    }
    
    // Training Wheels
    function updateCircuits(bytes32[] calldata _circuits) public onlyOwner {
        txKeyHash = _circuits[0];
        txWrapperKeyHash = _circuits[1];
        recursiveKeyHash = _circuits[2];
        verifier = IVerifier(_toAddress(_circuits[3]));
    }

    // Training Wheels
    function emergencyPause() public onlyOwner {
        _pause();
    }

    // Training Wheels
    function emergencyUnpause() public onlyOwner {
        _unpause();
    }

    // Training Wheels
    function changeCap(uint256 _cap) public onlyOwner {
        require(_cap < 1000, "momiji.changeCap: Cannot exceed 1000 bps");
        emit DailyCapChanged(dailyCap, _cap);
        dailyCap = _cap;
    }

    // Training Wheels
    function changeBurnPercentage(uint256 _percentage) public onlyOwner {
        require(_percentage < 100, "momiji.changeBurnPercentage: Cannot exceed 100%");
        emit BurnPercentageChanged(burnPercentageSwap, _percentage);
        burnPercentageSwap = _percentage;
    }

    // Training Wheels
    function changePools(address[] calldata _addrs) public onlyOwner {
        swapRouter = ISwapRouter(_addrs[0]);
        xftPool = _addrs[1];
        xft = _addrs[2];
        token = IElasticERC20(_addrs[2]);
    }


    function getSpentNullifiers(bytes32[] calldata _nullifierHashes) external view returns (bool[] memory spent) {
        uint256 nullifierHashesLength = _nullifierHashes.length;
        spent = new bool[](nullifierHashesLength);
        for (uint256 i; i < nullifierHashesLength; i++) {
            if (isSpent(_nullifierHashes[i])) {
                spent[i] = true;
            }
        }
    }

    function getValidRoots() public view returns (bytes32[] memory) {
        return validRoots;
    }

    function getValidRootAtIndex(uint256 _index) public view returns (bytes32) {
        return validRoots[_index];
    }

    function getValidRootsPaginated(uint256 _start, uint256 _end) public view returns (bytes32[] memory) {
            require(_start < _end, "Invalid range");
            require(_end < validRoots.length, "Invalid end index");

            bytes32[] memory paginatedRoots = new bytes32[](_end - _start);
            uint256 paginatedIndex = 0;
            
            for (uint256 i = _start; i <= _end; i++) {
                paginatedRoots[paginatedIndex] = validRoots[i];
                paginatedIndex++;
            }

            return paginatedRoots;
    }

    function isSpent(bytes32 _nullifierHash) public view returns (bool) {
        return nullifierHashes[_nullifierHash];
    }

    function ecRecover(bytes32 _hash, bytes calldata _signature) public pure returns (address) {
        return ECDSA.recover(_hash, _signature);
    }

    function _toAddress(bytes32 _address) internal pure returns (address) {
        return address(uint160(uint256(_address)));
    }

    function _fromAddress(address _address) internal pure returns (bytes32) {
        return bytes32(uint256(uint160(_address)));
    }

    function hashTypedDataV4(Deposit calldata _deposit) public view returns (bytes32) {
        return _hashTypedDataV4(keccak256(abi.encode(
                keccak256("DepositHash(bytes32 pi_hash)"),
                _deposit.pi_hash
            )));
    }

    function broadcastTransaction(TransactionWithProof calldata _tx, bytes32 _recipient) public nonreentrant {
        bytes32 _signatureHash;
        if (_tx.transaction.amount > 0) {
            _signatureHash = _hashTypedDataV4(keccak256(abi.encode(
                keccak256("DepositHash(bytes32 pi_hash)"),
                _tx.transaction.deposit.pi_hash
            )));
        } else {
            _signatureHash = 0;
        }
        bytes32 _piHash = hashCircuitInputsForTx(_tx.transaction, _signatureHash);
        require(relay[_piHash] == 0, "momiji.broadcast: already broadcasted");
        for (uint256 i = 0; i < 16; i++) {
            if (_tx.transaction.recipients[i] == BROADCASTER_PAYMENT)
                relay[_piHash] = _recipient;
        }
        emit TransactionBroadcast(_tx, merkleRoot, _piHash);
    }

    function broadcastAddress(string calldata _address) public nonreentrant {
        emit BroadcastAddress(merkleRoot, _address);
    }

    function _swapForETH(uint256 _swapAmount, uint160 _priceLimit) internal returns (uint256 _amountOut) {
        token.approve(address(swapRouter), _swapAmount);
        _amountOut = swapRouter.exactInputSingle(
            ISwapRouter.ExactInputSingleParams({
                tokenIn: xft,
                tokenOut: _weth9,
                fee: fee,
                recipient: address(this),
                deadline: block.timestamp,
                amountIn: _swapAmount,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: _priceLimit
            })
        );
        weth9.withdraw(_amountOut);
        return _amountOut;
    }

    function verifyProof(
        bytes calldata _proof,
        Batch calldata _batch,
        bytes32 _accumulator
    ) public view returns (bool) {
        bytes32[] memory publicInputs = new bytes32[](17);
        publicInputs[0] = prepareBatchPublicInputs(_batch, _accumulator);
        for (uint256 i = 0; i < 16; i++) publicInputs[i + 1] = _batch.aggregation_object[i];
        return verifier.verify(_proof, publicInputs);
    }

    function _publishDeposit(
        Transaction calldata _tx,
        bytes32 _signatureHash
    ) internal {

        address depositor = ecRecover(_signatureHash, _tx.deposit.signature);

        require(token.balanceOf(depositor) >= uint256(_tx.amount), "momiji._publishDeposit: insufficient balance");
        token.burn(depositor, uint256(_tx.amount));
    }

    function _handleUTXOs(Transaction calldata _tx) internal {
        
        for (uint256 j = 0; j < 16; j++) {
            if (_tx.nullifier_hashes[j] != ZERO_VALUE) {
                require(!nullifierHashes[_tx.nullifier_hashes[j]], 'momiji._publishWithdraw: nullifier spent');
                nullifierHashes[_tx.nullifier_hashes[j]] = true;
            }

            if (_tx.commitments[j] != ZERO_VALUE) {
                require(utxoPrevRoots[_tx.commitments[j]] == 0, 'momiji._publishWithdraw: utxo exists');
                utxoPrevRoots[_tx.commitments[j]] = merkleRoot;
                
                emit EncryptedUTXOBroadcast(
                    merkleRoot,
                    _tx.uids[j],
                    _tx.encrypted_utxo[j]
                );
            }
            
            if (_tx.commitments_in[j] != ZERO_VALUE) require(utxoPrevRoots[_tx.commitments_in[j]] != 0, "momiji.publish: commitment doesn't exist");
        
        }

    }

    function _createPayments(
        Transaction calldata _tx,
        Payment[] memory _payments,
        bytes32 _piHash,
        uint256 _txIndex
    ) internal {

        for (uint256 j = 0; j < MAX_UTXOS; j++) {
            bytes32 _recipient = ZERO_VALUE;

            if (_tx.recipients[j] != ZERO_VALUE) {

                if (_tx.recipients[j] == BROADCASTER_PAYMENT) {
                    _recipient = relay[_piHash];
                } else {
                    _recipient = _tx.recipients[j];
                }
                
                uint256 _swapAmount = uint256(_tx.swap_amounts[j]);
                uint256 _withdrawalAmount = uint256(_tx.withdrawals[j]) - _swapAmount;

                _swapAmount = _swapAmount * (100 - burnPercentageSwap) / 100;

                uint160 _priceLimit = (_tx.price_limit == ZERO_VALUE) ? 0 : uint160(uint256(_tx.price_limit));

                _payments[(_txIndex * MAX_UTXOS) + j] = Payment(_recipient, _withdrawalAmount, _swapAmount, _priceLimit);
            }
        }
    }

    function _handlePayments(Payment[] memory _payments, uint256 _txCount) internal {

        uint256 _paymentsCount = 1;
        uint256 _xftForSwap;
        uint160 _priceLimit;

        bytes32[] memory _recipients = new bytes32[](_txCount * MAX_UTXOS + 1);
        Payment memory _payment;

        for (uint256 i = 0; i < _txCount; i++) {
            for (uint256 j = 0; j < MAX_UTXOS; j++) {
                _payment = _payments[i * MAX_UTXOS + j];

                if (_payment.recipient != ZERO_VALUE && _payment.recipient != NO_PAYMENT) {

                    if (_payment.recipient == COINBASE_PAYMENT) _payment.recipient = _fromAddress(block.coinbase);
                    if (_payment.recipient == PROVER_PAYMENT) _payment.recipient = _fromAddress(msg.sender);

                    if (_priceLimit < _payment.price_limit) _priceLimit = _payment.price_limit;

                    uint256 _recipientIndex;
                    bytes32 _thisRecipient = _payment.recipient;

                    assembly {
                        _recipientIndex := tload(_thisRecipient)
                    }
                    
                    if (_recipientIndex == 0) {
                        _recipients[_paymentsCount] = _payment.recipient;
                        _recipientIndex = i * MAX_UTXOS + j;
                        assembly {
                            tstore(_thisRecipient, _recipientIndex)
                        }
                        _paymentsCount++;
                    } else {
                        if (_payment.withdrawalAmount > 0) _payments[_recipientIndex].withdrawalAmount += _payment.withdrawalAmount;
                        if (_payment.swapAmount > 0) _payments[_recipientIndex].swapAmount += _payment.swapAmount;
                        _payments[i * MAX_UTXOS + j] = Payment(NO_PAYMENT, 0, 0, 0);
                    }

                    _xftForSwap += _payment.swapAmount;
                    
                }

            }
        }

        // Training Wheels
        uint256 _now = block.timestamp;
        if (_now - lastCapReset > 86400) {
            lastCapReset = _now;
            dailyMint = 0;
        }

        uint256 _amountOut = 0;
        if (_xftForSwap > 0) {

            // Training Wheels
            require(_xftForSwap + dailyMint <= (dailyCap * token.totalSupply() / 1000), "momiji._handlePayments: daily cap exceeded");
            dailyMint += _xftForSwap;

            token.mint(address(this), _xftForSwap);
            _amountOut = _swapForETH(_xftForSwap, _priceLimit);
            for (uint256 i = 1; i < _paymentsCount; i++) {
                if (_recipients[i] != NO_PAYMENT) {
                    uint256 _thisPaymentIndex;
                    bytes32 _thisRecipient = _recipients[i];
                    assembly {
                        _thisPaymentIndex := tload(_thisRecipient)
                    }
                    _payment = _payments[uint256(_thisPaymentIndex)];
                    uint256 _amountOutShare;
                    if (_payment.swapAmount > 0) {
                        _amountOutShare = _payment.swapAmount * _amountOut / _xftForSwap;
                        payable(_toAddress(_recipients[i])).transfer(_amountOutShare);
                    }
                    
                    uint256 _xftAmountOut = _payment.withdrawalAmount;
                    if (_xftAmountOut > 0) {

                        // Training Wheels
                        require(_xftAmountOut + dailyMint <= (dailyCap * token.totalSupply() / 1000), "momiji._handlePayments: daily cap exceeded");
                        dailyMint += _xftAmountOut;

                        token.mint(_toAddress(_recipients[i]), _xftAmountOut);
                    }
                }
            }

        }
    }

    function _getSignatureHash(Transaction calldata _transaction) public view returns (bytes32 _signatureHash) {
        if (_transaction.amount > 0) {
            _signatureHash = _hashTypedDataV4(keccak256(abi.encode(
                keccak256("DepositHash(bytes32 pi_hash)"),
                hashCircuitInputsForTxWithoutDeposit(_transaction)
            )));
        } else {
            _signatureHash = 0;
        }
        return _signatureHash;
    }

    function _accumulatePublicInputs(
        bytes32 _previousAccumulator,
        bytes32 _publicInputsHash
    ) public view returns (bytes32) {
        bytes32[] memory _hash = new bytes32[](4);
        _hash[0] = _previousAccumulator;
        _hash[1] = _publicInputsHash;
        _hash[2] = txKeyHash;
        _hash[3] = (_previousAccumulator == ZERO_VALUE) ? txWrapperKeyHash : recursiveKeyHash;
        return _hashAndMod(_hash);
    }

    function _updateState(Batch memory _batch) internal {
        validRoots.push(_batch.new_root);
        merkleRoot = _batch.new_root;
        histRoot = _batch.new_hist_root;
        batchNumber++;
    }

    // This should always revert before the verify step
    function simulatePublish(Batch calldata _batch) public returns (bytes memory returnData) {
        (bool success, returnData) = address(this).call(abi.encodeWithSelector(IMomiji.publish.selector, "0x", _batch));
    }

    function publish(bytes calldata _proof, Batch calldata _batch) public nonreentrant whenNotPaused {

        require(batchNumber < 2 ** STATE_DEPTH, "momiji.publish: state depth reached");
        require(_batch.tx_key_hash == txKeyHash, 'momiji.publish: invalid tx keyHash');
        require(_batch.recursive_key_hash == recursiveKeyHash, 'momiji.publish: invalid recursive keyHash');
        require(_batch.old_hist_root == histRoot, 'momiji.publish: invalid historic root');

        uint256 _txCount = _batch.transactions.length;

        bytes32 _accumulator = ZERO_VALUE;

        Payment[] memory _payments = new Payment[](MAX_ITEMS * MAX_UTXOS);

        for (uint256 i = 0; i < _txCount; i++) {

            require(_batch.transactions[i].utxo_root != ZERO_VALUE, 'momiji.publish: tx must not be empty');
            require(_batch.transactions[i].utxo_root != ZERO_UTXO_ROOT, 'momiji.publish: tx utxos must not be empty');
            require(uint256(_batch.transactions[i].timestamp) % 60 == 0, 'momiji.publish: timestamp must be divisible by 60');
            require(_batch.transactions[i].timestamp < bytes32(block.timestamp), 'momiji.publish: tx not yet valid');
            require(uint256(_batch.transactions[i].deadline) % 60 == 0, 'momiji.publish: deadline must be divisible by 60');
            require(_batch.transactions[i].deadline > bytes32(block.timestamp), 'momiji.publish: tx has expired');
            
            bytes32 _signatureHash = _getSignatureHash(_batch.transactions[i]);
            bytes32 _publicInputsHash = hashCircuitInputsForTx(_batch.transactions[i], _signatureHash);
            _accumulator = _accumulatePublicInputs(_accumulator, _publicInputsHash);
            if (_signatureHash > 0) _publishDeposit(_batch.transactions[i], _signatureHash);
            _handleUTXOs(_batch.transactions[i]);
            _createPayments(_batch.transactions[i], _payments, _publicInputsHash, i);

            emit TransactionPublish(_batch.transactions[i], merkleRoot, _publicInputsHash);
        }

        _handlePayments(_payments, _txCount);

        _updateState(_batch);

        emit BatchPublish(batchNumber, merkleRoot, _batch.new_root, _batch.oracle, _batch.historic_path);

        require(_proof.length > 0, 'momiji.publish: no proof');
        require(verifyProof(_proof, _batch, _accumulator), 'momiji.publish: invalid proof');
    }

    function prepareBatchPublicInputs(
        Batch calldata _batch,
        bytes32 _accumulator
    ) public view returns (bytes32) {
        bytes32[] memory _hash = new bytes32[](26);
        _hash[0] = _batch.new_root;
        _hash[1] = _batch.old_hist_root;
        _hash[2] = _batch.new_hist_root;
        _hash[3] = _accumulator;
        _hash[4] = _batch.tx_key_hash;
        _hash[5] = _batch.recursive_key_hash;
        for (uint256 i = 0; i < 20; i++) {
            _hash[6 + i] = _batch.historic_path[i];
        }
        return _hashAndMod(_hash);
    }

    function hashCircuitInputsForTx(Transaction calldata _tx, bytes32 _signatureHash) public view returns (bytes32) {
        bytes32[] memory _hashInputs = new bytes32[](53);
        _hashInputs[0] = _tx.current_root;
        _hashInputs[1] = _tx.utxo_root;
        _hashInputs[2] = _tx.amount;
        _hashInputs[3] = hashContractOnlyInputsForTx(_tx, _signatureHash);
        for (uint256 i = 0; i < 16; i++) {
            _hashInputs[4] = bytes32(uint256(_hashInputs[4]) + uint256(_tx.withdrawals[i])); 
            _hashInputs[5 + i] = _tx.commitments_in[i];
            _hashInputs[21 + i] = _tx.commitments[i];
            _hashInputs[37 + i] = _tx.nullifier_hashes[i];
        }
        return _hashAndMod(_hashInputs);
    }

    function hashContractOnlyInputsForTx(Transaction calldata _tx, bytes32 _signatureHash) public view returns (bytes32) {
        bytes32[] memory _hashInputs = new bytes32[](100);
        _hashInputs[0] = _tx.timestamp;
        _hashInputs[1] = _tx.deadline;
        _hashInputs[2] = bytes32(uint256(_signatureHash) % FIELD_MODULUS);
        _hashInputs[3] = _tx.price_limit;
        for (uint256 i = 0; i < 16; i++) {
            _hashInputs[4 + i] = _tx.recipients[i];
            _hashInputs[20 + i] = _tx.swap_amounts[i];
            _hashInputs[36 + i] = _tx.uids[i];
            _hashInputs[52 + (i * 3)] = _tx.encrypted_utxo[i].secret;
            _hashInputs[53 + (i * 3)] = _tx.encrypted_utxo[i].amount;
            _hashInputs[54 + (i * 3)] = _tx.encrypted_utxo[i].data;
        }
        return _hashAndMod(_hashInputs);
    }

    function hashCircuitInputsForTxWithoutDeposit(Transaction calldata _tx) public view returns (bytes32) {
        bytes32[] memory _hashInputs = new bytes32[](53);
        _hashInputs[0] = _tx.current_root;
        _hashInputs[1] = _tx.utxo_root;
        _hashInputs[2] = _tx.amount;
        _hashInputs[3] = hashContractOnlyInputsForTxWithoutDeposit(_tx);
        for (uint256 i = 0; i < 16; i++) {
            _hashInputs[4] = bytes32(uint256(_hashInputs[4]) + uint256(_tx.withdrawals[i])); 
            _hashInputs[5 + i] = _tx.commitments_in[i];
            _hashInputs[21 + i] = _tx.commitments[i];
            _hashInputs[37 + i] = _tx.nullifier_hashes[i];
        }
        return _hashAndMod(_hashInputs);
    }

    function hashContractOnlyInputsForTxWithoutDeposit(Transaction calldata _tx) public view returns (bytes32) {
        bytes32[] memory _hashInputs = new bytes32[](99);
        _hashInputs[0] = _tx.timestamp;
        _hashInputs[1] = _tx.deadline;
        _hashInputs[2] = _tx.price_limit;
        for (uint256 i = 0; i < 16; i++) {
            _hashInputs[3 + i] = _tx.recipients[i];
            _hashInputs[19 + i] = _tx.swap_amounts[i];
            _hashInputs[35 + i] = _tx.uids[i];
            _hashInputs[51 + (i * 3)] = _tx.encrypted_utxo[i].secret;
            _hashInputs[52 + (i * 3)] = _tx.encrypted_utxo[i].amount;
            _hashInputs[53 + (i * 3)] = _tx.encrypted_utxo[i].data;
        }
        return _hashAndMod(_hashInputs);
    }

    function _hashAndMod(bytes32[] memory _hashes) internal view returns (bytes32) {
        bytes32 _hash = keccak256(abi.encodePacked(_hashes));
        return bytes32(uint256(_hash) % FIELD_MODULUS);
    }

}