text: const fs = require('fs')
  const Web3 = require('web3')
  const { toBN, toWei, fromWei, toChecksumAddress } = require('web3-utils')
  const MerkleTree = require('fixed-merkle-tree')
  const Redis = require('ioredis')
  const { GasPriceOracle } = require('gas-price-oracle')
  const { Utils, Controller } = require('tornado-anonymity-mining')
  
  const swapABI = require('../abis/swap.abi.json')
  const miningABI = require('../abis/mining.abi.json')
  const tornadoABI = require('../abis/tornadoABI.json')
  const tornadoProxyABI = require('../abis/tornadoProxyABI.json')
  const { queue } = require('./queue')
  const { poseidonHash2, getInstance, fromDecimals, sleep } = require('./utils')
  const { jobType, status } = require('./constants')
  const {
    torn,
    netId,
    redisUrl,
    gasLimits,
    instances,
    privateKey,
    httpRpcUrl,
    oracleRpcUrl,
    baseFeeReserve,
    miningServiceFee,
    tornadoServiceFee,
    tornadoGoerliProxy,
  } = require('./config')
  const ENSResolver = require('./resolver')
  const resolver = new ENSResolver()
  const { TxManager } = require('tx-manager')
  
  let web3
  let currentTx
  let currentJob
  let tree
  let txManager
  let controller
  let swap
  let minerContract
  const redis = new Redis(redisUrl)
  const redisSubscribe = new Redis(redisUrl)
  const gasPriceOracle = new GasPriceOracle({ defaultRpc: oracleRpcUrl })
  
  async function fetchTree() {
    const elements = await redis.get('tree:elements')
    const convert = (_, val) => (typeof val === 'string' ? toBN(val) : val)
    tree = MerkleTree.deserialize(JSON.parse(elements, convert), poseidonHash2)
  
    if (currentTx && currentJob && ['MINING_REWARD', 'MINING_WITHDRAW'].includes(currentJob.data.type)) {
      const { proof, args } = currentJob.data
      if (toBN(args.account.inputRoot).eq(toBN(tree.root()))) {
        console.log('Account root is up to date. Skipping Root Update operation...')
        return
      } else {
        console.log('Account root is outdated. Starting Root Update operation...')
      }
  
      const update = await controller.treeUpdate(args.account.outputCommitment, tree)
  
      const minerAddress = await resolver.resolve(torn.miningV2.address)
      const instance = new web3.eth.Contract(miningABI, minerAddress)
      const data =
        currentJob.data.type === 'MINING_REWARD'
          ? instance.methods.reward(proof, args, update.proof, update.args).encodeABI()
          : instance.methods.withdraw(proof, args, update.proof, update.args).encodeABI()
      await currentTx.replace({
        to: minerAddress,
        data,
      })
      console.log('replaced pending tx')
    }
  }
  
  async function start() {
    try {
      web3 = new Web3(httpRpcUrl)
      const { CONFIRMATIONS, MAX_GAS_PRICE } = process.env
      txManager = new TxManager({
        privateKey,
        rpcUrl: httpRpcUrl,
        config: {
          CONFIRMATIONS,
          MAX_GAS_PRICE,
          THROW_ON_REVERT: false,
          BASE_FEE_RESERVE_PERCENTAGE: baseFeeReserve,
        },
      })
      swap = new web3.eth.Contract(swapABI, await resolver.resolve(torn.rewardSwap.address))
      minerContract = new web3.eth.Contract(miningABI, await resolver.resolve(torn.miningV2.address))
      redisSubscribe.subscribe('treeUpdate', fetchTree)
      await fetchTree()
      const provingKeys = {
        treeUpdateCircuit: require('../keys/TreeUpdate.json'),
        treeUpdateProvingKey: fs.readFileSync('./keys/TreeUpdate_proving_key.bin').buffer,
      }
      controller = new Controller({ provingKeys })
      await controller.init()
      queue.process(processJob)
      console.log('Worker started')
    } catch (e) {
      console.error('error on start worker', e.message)
    }
  }
  
  function checkFee({ data }) {
    if (data.type === jobType.TORNADO_WITHDRAW) {
      return checkTornadoFee(data)
    }
    return checkMiningFee(data)
  }
  
  async function getGasPrice() {
    const block = await web3.eth.getBlock('latest')
  
    if (block && block.baseFeePerGas) {
      const baseFeePerGas = toBN(block.baseFeePerGas)
      return baseFeePerGas
    }
  
    const { fast } = await gasPriceOracle.gasPrices()
    const gasPrice = toBN(toWei(fast.toString(), 'gwei'))
    return gasPrice
  }
  
  async function checkTornadoFee({ args, contract }) {
    const { currency, amount } = getInstance(contract)
    const { decimals } = instances[`netId${netId}`][currency]
    const [fee, refund] = [args[4], args[5]].map(toBN)
    const gasPrice = await getGasPrice()
  
    const ethPrice = await redis.hget('prices', currency)
    const expense = gasPrice.mul(toBN(gasLimits[jobType.TORNADO_WITHDRAW]))
  
    const feePercent = toBN(fromDecimals(amount, decimals))
      .mul(toBN(parseInt(tornadoServiceFee * 1e10)))
      .div(toBN(1e10 * 100))
  
    let desiredFee
    switch (currency) {
      case 'eth': {
        desiredFee = expense.add(feePercent)
        break
      }
      default: {
        desiredFee = expense
          .add(refund)
          .mul(toBN(10 ** decimals))
          .div(toBN(ethPrice))
        desiredFee = desiredFee.add(feePercent)
        break
      }
    }
    console.log(
      'sent fee, desired fee, feePercent',
      fromWei(fee.toString()),
      fromWei(desiredFee.toString()),
      fromWei(feePercent.toString()),
    )
    if (fee.lt(desiredFee)) {
      throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.')
    }
  }
  
  async function checkMiningFee({ args }) {
    const gasPrice = await getGasPrice()
    const ethPrice = await redis.hget('prices', 'torn')
    const isMiningReward = currentJob.data.type === jobType.MINING_REWARD
    const providedFee = isMiningReward ? toBN(args.fee) : toBN(args.extData.fee)
  
    const expense = gasPrice.mul(toBN(gasLimits[currentJob.data.type]))
    const expenseInTorn = expense.mul(toBN(1e18)).div(toBN(ethPrice))
    // todo make aggregator for ethPrices and rewardSwap data
    const balance = await swap.methods.tornVirtualBalance().call()
    const poolWeight = await swap.methods.poolWeight().call()
    const expenseInPoints = Utils.reverseTornadoFormula({ balance, tokens: expenseInTorn, poolWeight })
    /* eslint-disable */
    const serviceFeePercent = isMiningReward
      ? toBN(0)
      : toBN(args.amount)
          .sub(providedFee) // args.amount includes fee
          .mul(toBN(parseInt(miningServiceFee * 1e10)))
          .div(toBN(1e10 * 100))
    /* eslint-enable */
    const desiredFee = expenseInPoints.add(serviceFeePercent) // in points
    console.log(
      'user provided fee, desired fee, serviceFeePercent',
      providedFee.toString(),
      desiredFee.toString(),
      serviceFeePercent.toString(),
    )
    if (toBN(providedFee).lt(desiredFee)) {
      throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.')
    }
  }
  
  async function getProxyContract() {
    let proxyAddress
  
    if (netId === 5) {
      proxyAddress = tornadoGoerliProxy
    } else {
      proxyAddress = await resolver.resolve(torn.tornadoProxy.address)
    }
  
    const contract = new web3.eth.Contract(tornadoProxyABI, proxyAddress)
  
    return {
      contract,
      isOldProxy: checkOldProxy(proxyAddress),
    }
  }
  
  function checkOldProxy(address) {
    const OLD_PROXY = '0x905b63Fff465B9fFBF41DeA908CEb12478ec7601'
    return toChecksumAddress(address) === toChecksumAddress(OLD_PROXY)
  }
  
  async function getTxObject({ data }) {
    if (data.type === jobType.TORNADO_WITHDRAW) {
      let { contract, isOldProxy } = await getProxyContract()
  
      let calldata = contract.methods.withdraw(data.contract, data.proof, ...data.args).encodeABI()
  
      if (isOldProxy && getInstance(data.contract).currency !== 'eth') {
        contract = new web3.eth.Contract(tornadoABI, data.contract)
        calldata = contract.methods.withdraw(data.proof, ...data.args).encodeABI()
      }
  
      return {
        value: data.args[5],
        to: contract._address,
        data: calldata,
        gasLimit: gasLimits['WITHDRAW_WITH_EXTRA'],
      }
    } else {
      const method = data.type === jobType.MINING_REWARD ? 'reward' : 'withdraw'
      const calldata = minerContract.methods[method](data.proof, data.args).encodeABI()
      return {
        to: minerContract._address,
        data: calldata,
        gasLimit: gasLimits[data.type],
      }
    }
  }
  
  async function isOutdatedTreeRevert(receipt, currentTx) {
    try {
      await web3.eth.call(currentTx.tx, receipt.blockNumber)
      console.log('Simulated call successful')
      return false
    } catch (e) {
      console.log('Decoded revert reason:', e.message)
      return (
        e.message.indexOf('Outdated account merkle root') !== -1 ||
        e.message.indexOf('Outdated tree update merkle root') !== -1
      )
    }
  }
  
  async function processJob(job) {
    try {
      if (!jobType[job.data.type]) {
        throw new Error(`Unknown job type: ${job.data.type}`)
      }
      currentJob = job
      await updateStatus(status.ACCEPTED)
      console.log(`Start processing a new ${job.data.type} job #${job.id}`)
      await submitTx(job)
    } catch (e) {
      console.error('processJob', e.message)
      await updateStatus(status.FAILED)
      throw e
    }
  }
  
  async function submitTx(job, retry = 0) {
    await checkFee(job)
    currentTx = await txManager.createTx(await getTxObject(job))
  
    if (job.data.type !== jobType.TORNADO_WITHDRAW) {
      await fetchTree()
    }
  
    try {
      const receipt = await currentTx
        .send()
        .on('transactionHash', txHash => {
          updateTxHash(txHash)
          updateStatus(status.SENT)
        })
        .on('mined', receipt => {
          console.log('Mined in block', receipt.blockNumber)
          updateStatus(status.MINED)
        })
        .on('confirmations', updateConfirmations)
  
      if (receipt.status === 1) {
        await updateStatus(status.CONFIRMED)
      } else {
        if (job.data.type !== jobType.TORNADO_WITHDRAW && (await isOutdatedTreeRevert(receipt, currentTx))) {
          if (retry < 3) {
            await updateStatus(status.RESUBMITTED)
            await submitTx(job, retry + 1)
          } else {
            throw new Error('Tree update retry limit exceeded')
          }
        } else {
          throw new Error('Submitted transaction failed')
        }
      }
    } catch (e) {
      // todo this could result in duplicated error logs
      // todo handle a case where account tree is still not up to date (wait and retry)?
      if (
        job.data.type !== jobType.TORNADO_WITHDRAW &&
        (e.message.indexOf('Outdated account merkle root') !== -1 ||
          e.message.indexOf('Outdated tree update merkle root') !== -1)
      ) {
        if (retry < 5) {
          await sleep(3000)
          console.log('Tree is still not up to date, resubmitting')
          await submitTx(job, retry + 1)
        } else {
          throw new Error('Tree update retry limit exceeded')
        }
      } else {
        throw new Error(`Revert by smart contract ${e.message}`)
      }
    }
  }
  
  async function updateTxHash(txHash) {
    console.log(`A new successfully sent tx ${txHash}`)
    currentJob.data.txHash = txHash
    await currentJob.update(currentJob.data)
  }
  
  async function updateConfirmations(confirmations) {
    console.log(`Confirmations count ${confirmations}`)
    currentJob.data.confirmations = confirmations
    await currentJob.update(currentJob.data)
  }
  
  async function updateStatus(status) {
    console.log(`Job status updated ${status}`)
    currentJob.data.status = status
    await currentJob.update(currentJob.data)
  }
  
  start()
  