/* eslint-disable camelcase */
/* eslint-disable no-console, import/order */
import Web3 from 'web3'

import networkConfig from '@/networkConfig'
import { cachedEventsLength, eventsType } from '@/constants'

import MulticallABI from '@/abis/Multicall.json'
import InstanceABI from '@/abis/Instance.abi.json'
import TornadoProxyABI from '@/abis/TornadoProxy.abi.json'

import { ACTION, ACTION_GAS } from '@/constants/variables'
import { graph, treesInterface, EventsFactory } from '@/services'

import {
  randomBN,
  parseNote,
  toFixedHex,
  saveAsFile,
  isEmptyArray,
  decimalPlaces,
  parseHexNote,
  checkCommitments,
  buffPedersenHash
} from '@/utils'

import { buildGroth16, download, getTornadoKeys } from './snark'

let groth16

const websnarkUtils = require('websnark/src/utils')
const { toWei, numberToHex, toBN, isAddress } = require('web3-utils')

const getStatisticStore = (acc, { tokens }) => {
  Object.entries(tokens).forEach(([currency, { instanceAddress }]) => {
    acc[currency] = Object.assign({}, acc[currency])

    Object.keys(instanceAddress).forEach((amount) => {
      if (!acc[currency][amount]) {
        acc[currency][amount] = {
          latestDeposits: [],
          nextDepositIndex: null,
          anonymitySet: null
        }
      }
    })
  })
  return acc
}

const defaultStatistics = Object.values(networkConfig).reduce(getStatisticStore, {})

const state = () => {
  return {
    note: null,
    commitment: null,
    prefix: null,
    errors: [],
    notes: {},
    statistic: defaultStatistics,
    ip: {},
    selectedInstance: { currency: 'eth', amount: 0.1 },
    selectedStatistic: { currency: 'eth', amount: 0.1 },
    withdrawType: 'relayer',
    ethToReceive: '20000000000000000',
    defaultEthToReceive: '20000000000000000',
    withdrawNote: ''
  }
}

const mutations = {
  SAVE_DEPOSIT(state, { note, commitment, prefix }) {
    state.note = note
    state.commitment = commitment
    state.prefix = prefix
  },
  SAVE_PROOF(state, { proof, args, note }) {
    this._vm.$set(state.notes, note, { proof, args })
  },
  REMOVE_PROOF(state, { note }) {
    this._vm.$delete(state.notes, note)
  },
  SAVE_ERROR(state, message) {
    state.errors.push(message)
  },
  REMOVE_ERRORS(state) {
    this._vm.$set(state, 'errors', [])
  },
  SAVE_LAST_INDEX(state, { nextDepositIndex, anonymitySet, currency, amount }) {
    const currentState = state.statistic[currency][amount]
    this._vm.$set(state.statistic[currency], `${amount}`, { ...currentState, nextDepositIndex, anonymitySet })
  },
  SAVE_LAST_EVENTS(state, { latestDeposits, currency, amount }) {
    const currentState = state.statistic[currency][amount]
    this._vm.$set(state.statistic[currency], `${amount}`, { ...currentState, latestDeposits })
  },
  SET_SELECTED_INSTANCE(state, selectedInstance) {
    state.selectedInstance = selectedInstance
  },
  SET_SELECTED_STATISTIC(state, selectedStatistic) {
    state.selectedStatistic = selectedStatistic
  },
  SET_WITHDRAW_TYPE(state, { withdrawType }) {
    this._vm.$set(state, 'withdrawType', withdrawType)
  },
  SAVE_ETH_TO_RECEIVE(state, { ethToReceive }) {
    this._vm.$set(state, 'ethToReceive', ethToReceive)
  },
  SAVE_DEFAULT_ETH_TO_RECEIVE(state, { ethToReceive }) {
    this._vm.$set(state, 'defaultEthToReceive', ethToReceive)
  },
  SET_WITHDRAW_NOTE(state, withdrawNote) {
    state.withdrawNote = withdrawNote
  }
}

const getters = {
  eventsInterface: (state, getters, rootState, rootGetters) => {
    const netId = rootGetters['metamask/netId']
    const { url } = rootState.settings[`netId${netId}`].rpc
    return new EventsFactory(url)
  },
  instanceContract: (state, getters, rootState) => ({ currency, amount, netId }) => {
    const config = networkConfig[`netId${netId}`]
    const { url } = rootState.settings[`netId${netId}`].rpc
    const address = config.tokens[currency].instanceAddress[amount]
    const web3 = new Web3(url)
    return new web3.eth.Contract(InstanceABI, address)
  },
  multicallContract: (state, getters, rootState) => ({ netId }) => {
    const config = networkConfig[`netId${netId}`]
    const { url } = rootState.settings[`netId${netId}`].rpc
    const web3 = new Web3(url)
    return new web3.eth.Contract(MulticallABI, config.multicall)
  },
  tornadoProxyContract: (state, getters, rootState) => ({ netId }) => {
    const {
      'tornado-proxy.contract.tornadocash.eth': tornadoProxy,
      'tornado-router.contract.tornadocash.eth': tornadoRouter,
      'tornado-proxy-light.contract.tornadocash.eth': tornadoProxyLight
    } = networkConfig[`netId${netId}`]

    const proxyContract = tornadoRouter || tornadoProxy || tornadoProxyLight
    const { url } = rootState.settings[`netId${netId}`].rpc
    const web3 = new Web3(url)
    return new web3.eth.Contract(TornadoProxyABI, proxyContract)
  },
  currentContract: (state, getters) => (params) => {
    return getters.tornadoProxyContract(params)
  },
  withdrawGas: (state, getters) => {
    let action = ACTION.WITHDRAW_WITH_EXTRA

    if (getters.hasEnabledLightProxy) {
      action = ACTION.WITHDRAW
    }

    if (getters.isOptimismConnected) {
      action = ACTION.OP_WITHDRAW
    }

    if (getters.isArbitrumConnected) {
      action = ACTION.ARB_WITHDRAW
    }

    return ACTION_GAS[action]
  },
  networkFee: (state, getters, rootState, rootGetters) => {
    const gasPrice = rootGetters['gasPrices/fastGasPrice']

    const networkFee = toBN(gasPrice).mul(toBN(getters.withdrawGas))

    if (getters.isOptimismConnected) {
      const l1Fee = rootGetters['gasPrices/l1Fee']
      return networkFee.add(toBN(l1Fee))
    }

    return networkFee
  },
  relayerFee: (state, getters, rootState, rootGetters) => {
    const { currency, amount } = rootState.application.selectedStatistic
    const { decimals } = rootGetters['metamask/networkConfig'].tokens[currency]
    const nativeCurrency = rootGetters['metamask/nativeCurrency']
    const total = toBN(rootGetters['token/fromDecimals'](amount.toString()))
    const fee = rootState.relayer.selectedRelayer.tornadoServiceFee
    const decimalsPoint = decimalPlaces(fee)
    const roundDecimal = 10 ** decimalsPoint
    const aroundFee = toBN(parseInt(fee * roundDecimal, 10))
    const tornadoServiceFee = total.mul(aroundFee).div(toBN(roundDecimal * 100))
    const ethFee = getters.networkFee
    switch (currency) {
      case nativeCurrency: {
        return ethFee.add(tornadoServiceFee)
      }
      default: {
        const tokenFee = ethFee.mul(toBN(10 ** decimals)).div(toBN(rootState.price.prices[currency]))
        return tokenFee.add(tornadoServiceFee)
      }
    }
  },
  ethToReceiveInToken: (state, getters, rootState, rootGetters) => {
    const { currency } = rootState.application.selectedStatistic
    const { decimals } = rootGetters['metamask/networkConfig'].tokens[currency]
    const price = rootState.price.prices[currency]
    const ethToReceive = toBN(state.ethToReceive)
    return ethToReceive.mul(toBN(10 ** decimals)).div(toBN(price))
  },
  isNotEnoughTokens: (state, getters, rootState, rootGetters) => {
    const { amount, currency } = rootState.application.selectedStatistic
    let total = toBN(rootGetters['token/fromDecimals'](amount.toString()))

    if (state.withdrawType === 'relayer') {
      const relayerFee = getters.relayerFee
      const nativeCurrency = rootGetters['metamask/nativeCurrency']

      if (currency === nativeCurrency) {
        total = total.sub(relayerFee)
      } else {
        const ethToReceiveInToken = getters.ethToReceiveInToken
        total = total.sub(relayerFee).sub(ethToReceiveInToken)
      }
    }

    return total.isNeg()
  },
  maxEthToReceive: (state, getters, rootState, rootGetters) => {
    const { currency, amount } = rootState.application.selectedStatistic
    const { decimals } = rootGetters['metamask/networkConfig'].tokens[currency]
    const total = toBN(rootGetters['token/fromDecimals'](amount.toString()))
    const price = rootState.price.prices[currency]
    const relayerFee = getters.relayerFee
    return total
      .sub(relayerFee)
      .mul(toBN(price))
      .div(toBN(10 ** decimals))
  },
  selectedCurrency: (state, getters, rootState, rootGetters) => {
    const tokens = rootGetters['metamask/networkConfig'].tokens
    return tokens[state.selectedInstance.currency].symbol
  },
  selectedStatisticCurrency: (state, getters, rootState, rootGetters) => {
    const tokens = rootGetters['metamask/networkConfig'].tokens
    return tokens[state.selectedStatistic.currency].symbol
  },
  lastEventIndex: (state) => ({ currency, amount }) => {
    return state.statistic[currency][amount].anonymitySet
  },
  latestDeposits: (state) => {
    const { currency, amount } = state.selectedStatistic
    return state.statistic[currency][amount].latestDeposits
  },
  hasEnabledLightProxy: (state, getters, rootState, rootGetters) => {
    return Boolean(rootGetters['metamask/networkConfig']['tornado-proxy-light.contract.tornadocash.eth'])
  },
  isOptimismConnected: (state, getters, rootState, rootGetters) => {
    const netId = rootGetters['metamask/netId']
    return Number(netId) === 10
  },
  isArbitrumConnected: (state, getters, rootState, rootGetters) => {
    const netId = rootGetters['metamask/netId']
    return Number(netId) === 42161
  }
}

const actions = {
  setAndUpdateStatistic({ dispatch, commit }, { currency, amount }) {
    commit('SET_SELECTED_STATISTIC', { currency, amount })
    dispatch('updateSelectEvents')
  },
  async updateSelectEvents({ dispatch, commit, state, rootGetters, getters }) {
    const netId = rootGetters['metamask/netId']
    const { currency, amount } = state.selectedStatistic
    const { deployedBlock } = networkConfig[`netId${netId}`]

    const eventService = getters.eventsInterface.getService({ netId, amount, currency })

    const savedEvents = await eventService.getEvents(eventsType.DEPOSIT)
    const fromBlock = savedEvents?.lastBlock || deployedBlock

    const graphEvents = await eventService.getEventsFromGraph({ fromBlock, methodName: 'getStatistic' })

    let statistic = graphEvents?.events

    if (!statistic || !statistic.length) {
      const fresh = await eventService.getStatisticsRpc({ fromBlock, eventsCount: 10 })

      statistic = fresh || []
    }

    const { nextDepositIndex, anonymitySet } = await dispatch('getLastDepositIndex', {
      currency,
      amount,
      netId
    })

    statistic = statistic.sort((a, b) => a.leafIndex - b.leafIndex)

    const latestDeposits = []

    for (const event of statistic.slice(-10)) {
      latestDeposits.unshift({
        index: event.leafIndex,
        depositTime: this.$moment.unix(event.timestamp).fromNow()
      })
    }

    commit('SAVE_LAST_EVENTS', {
      amount,
      currency,
      latestDeposits
    })
    commit('SAVE_LAST_INDEX', {
      amount,
      currency,
      anonymitySet,
      nextDepositIndex
    })
  },
  async updateEvents({ getters, rootGetters }, payload) {
    try {
      const eventService = getters.eventsInterface.getService(payload)

      const freshEvents = await eventService.updateEvents(payload.type)

      return freshEvents
    } catch (err) {
      throw new Error(`Method updateEvents has error: ${err.message}`)
    }
  },
  async updateCurrentEvents({ dispatch, rootGetters }, { amount, currency, lastEvent, type, netId }) {
    let lastBlock = lastEvent
    const nativeCurrency = rootGetters['metamask/nativeCurrency']
    const { deployedBlock } = networkConfig[`netId${netId}`]

    if (currency === nativeCurrency && !lastEvent) {
      lastBlock = await this.$indexedDB.getFromIndex({
        indexName: 'name',
        storeName: `lastEvents_${netId}`,
        key: `${type}s_${currency}_${amount}`
      })
    }

    const params = {
      type,
      netId,
      amount,
      currency,
      fromBlock: lastBlock ? lastBlock.blockNumber + 1 : deployedBlock
    }

    const events = await dispatch('updateEvents', params)

    return events
  },
  async getLastDepositIndex({ getters }, params) {
    try {
      const contractInstance = getters.instanceContract(params)
      const nextDepositIndex = await contractInstance.methods.nextIndex().call()

      return {
        nextDepositIndex,
        anonymitySet: toBN(nextDepositIndex)
      }
    } catch (err) {
      throw new Error(`Method getLastDepositIndex has error: ${err.message}`)
    }
  },
  async loadEncryptedEvents(_, { netId }) {
    try {
      const module = await download({
        contentType: 'string',
        name: `events/encrypted_notes_${netId}.json.zip`
      })

      if (module) {
        const events = JSON.parse(module)

        return {
          events,
          lastBlock: events[events.length - 1].blockNumber
        }
      }
    } catch (err) {
      throw new Error(`Method loadCachedEvents has error: ${err.message}`)
    }
  },
  prepareDeposit({ getters, dispatch, commit, rootGetters }, { prefix }) {
    try {
      const [, currency, amount, netId] = prefix.split('-')
      const contractInstance = getters.instanceContract({ currency, amount, netId })

      const secret = randomBN(31)
      const nullifier = randomBN(31)

      const preimage = Buffer.concat([nullifier.toBuffer('le', 31), secret.toBuffer('le', 31)])

      const commitment = buffPedersenHash(preimage)
      const commitmentHex = toFixedHex(commitment)

      const note = `0x${preimage.toString('hex')}`

      const isEnabled = rootGetters['encryptedNote/isEnabledSaveFile']

      if (isEnabled) {
        setTimeout(() => {
          try {
            dispatch('saveFile', { prefix, note })
          } catch (err) {
            console.warn('NoteAccount backup as a file is not supported on this device', err)
          }
        }, 1000)
      }

      commit('SAVE_DEPOSIT', { note, commitment: commitmentHex, prefix })
      if (!contractInstance._address) {
        throw new Error(this.app.i18n.t('networkIsNotSupported'))
      }
    } catch (e) {
      console.error('prepareDeposit', e)
    }
  },
  saveFile(_, { prefix, note }) {
    try {
      const data = new Blob([`${prefix}-${note}`], { type: 'text/plain;charset=utf-8' })

      saveAsFile(data, `backup-${prefix}-${note.slice(0, 10)}.txt`)
    } catch (err) {
      console.error('saveFile', err.message)
    }
  },
  async getEncryptedEventsFromDb(_, { netId }) {
    try {
      if (this.$indexedDB.isBlocked) {
        return []
      }

      const cachedEvents = await this.$indexedDB.getAll({ storeName: `encrypted_events_${netId}` })

      return cachedEvents
    } catch (err) {
      console.warn(`Method getEventsFromDb has error: ${err.message}`)
    }
  },
  async getEncryptedNotes({ rootState, rootGetters, dispatch, getters }) {
    try {
      const { netId } = rootState.metamask
      const rpc = rootGetters['settings/currentRpc']
      let { ENCRYPTED_NOTES_BLOCK: deployedBlock } = networkConfig[`netId${netId}`].constants

      const contractInstance = getters.tornadoProxyContract({ netId })

      let cachedEvents = await dispatch('getEncryptedEventsFromDb', { netId })

      const networksWithCache = [1, 5]

      const LENGTH_CACHE =
        Number(netId) === 1
          ? cachedEventsLength.mainnet.ENCRYPTED_NOTES
          : cachedEventsLength.goerli.ENCRYPTED_NOTES

      if (
        ((isEmptyArray(cachedEvents) || !cachedEvents) && networksWithCache.includes(netId)) ||
        (cachedEvents.length < LENGTH_CACHE && networksWithCache.includes(netId))
      ) {
        ;({ events: cachedEvents } = await dispatch('loadEncryptedEvents', { netId }))
      }

      const hasCache = Boolean(cachedEvents && cachedEvents.length)

      if (hasCache) {
        const [lastEvent] = cachedEvents.sort((a, b) => a.blockNumber - b.blockNumber).slice(-1)

        deployedBlock = lastEvent.blockNumber + 1
      }

      const web3 = this.$provider.getWeb3(rpc.url)
      const currentBlockNumber = await web3.eth.getBlockNumber()

      let events = []

      const { events: graphEvents, lastSyncBlock } = await graph.getAllEncryptedNotes({
        netId,
        fromBlock: deployedBlock
      })

      if (!isEmptyArray(graphEvents)) {
        deployedBlock = lastSyncBlock
      }

      let NUMBER_PARTS = hasCache ? 2 : 10

      if (Number(netId) === 56) {
        NUMBER_PARTS = parseInt((currentBlockNumber - deployedBlock) / 4950) || 1
      }

      const part = parseInt((currentBlockNumber - deployedBlock) / NUMBER_PARTS)

      let fromBlock = deployedBlock
      let toBlock = deployedBlock + part

      if (toBlock >= currentBlockNumber || toBlock === deployedBlock) {
        toBlock = 'latest'
        NUMBER_PARTS = 1
      }

      for (let i = 0; i < NUMBER_PARTS; i++) {
        const partOfEvents = await contractInstance.getPastEvents('EncryptedNote', {
          toBlock,
          fromBlock
        })
        if (partOfEvents) {
          events = events.concat(partOfEvents)
        }
        fromBlock = toBlock
        toBlock += part
      }

      if (events && events.length) {
        events = events
          .filter((i) => i.returnValues.encryptedNote)
          .map((e) => ({
            txHash: e.transactionHash,
            transactionHash: e.transactionHash,
            blockNumber: Number(e.blockNumber),
            encryptedNote: e.returnValues.encryptedNote
          }))
      }

      const allEvents = [].concat(cachedEvents, graphEvents, events)

      await dispatch('saveEncryptedEventsToDB', { events: allEvents, netId })

      return allEvents
    } catch (err) {
      console.log('getEncryptedNotes', err)
    }
  },
  async saveEncryptedEventsToDB(_, { events, netId }) {
    if (!events || !events.length || this.$indexedDB.isBlocked) {
      return
    }

    await this.$indexedDB.createMultipleTransactions({
      data: events,
      storeName: `encrypted_events_${netId}`
    })
  },
  async sendDeposit({ state, rootState, getters, rootGetters, dispatch, commit }, { isEncrypted, gasPrice }) {
    try {
      const { commitment, note, prefix } = state
      // eslint-disable-next-line prefer-const
      let [, currency, amount, netId] = prefix.split('-')
      const config = networkConfig[`netId${netId}`]
      const contractInstance = getters.tornadoProxyContract({ netId })

      if (!state.commitment) {
        throw new Error(this.app.i18n.t('failToGenerateNote'))
      }

      const { nextDepositIndex: index } = await dispatch('getLastDepositIndex', { netId, currency, amount })

      const { ethAccount } = rootState.metamask
      const nativeCurrency = rootGetters['metamask/nativeCurrency']
      const isNative = currency === nativeCurrency

      const value = isNative ? toWei(amount, 'ether') : '0'
      const instance = config.tokens[currency].instanceAddress[amount]

      let params = [instance, commitment, []]

      if (isEncrypted) {
        const encryptedNote = await dispatch(
          'encryptedNote/getEncryptedNote',
          { data: `${instance}-${note}` },
          { root: true }
        )

        params = [instance, commitment, encryptedNote]
      }

      const data = contractInstance.methods.deposit(...params).encodeABI()
      const gas = await contractInstance.methods.deposit(...params).estimateGas({ from: ethAccount, value })

      const callParams = {
        method: 'eth_sendTransaction',
        params: {
          gasPrice,
          to: contractInstance._address,
          gas: numberToHex(gas + 50000),
          value: numberToHex(value),
          data
        },
        watcherParams: {
          title: { path: 'depositing', amount, currency },
          successTitle: {
            path: 'depositedValue',
            amount,
            currency
          },
          storeType: isEncrypted ? 'encryptedTxs' : 'txs'
        },
        isAwait: false
      }

      const txHash = await dispatch('metamask/sendTransaction', callParams, { root: true })

      // there may be a race condition, you need to request an index and a timestamp of the deposit after tx is mined
      const timestamp = Math.round(new Date().getTime() / 1000)

      const { nullifierHex, commitmentHex } = parseHexNote(state.note)
      const storeType = isEncrypted ? 'encryptedTxs' : 'txs'

      const accounts = rootGetters['encryptedNote/accounts']

      const tx = {
        txHash,
        type: 'Deposit',
        note,
        amount,
        storeType,
        prefix,
        netId,
        timestamp,
        index,
        nullifierHex,
        commitmentHex,
        currency
      }
      console.log('tx', tx)

      if (isEncrypted) {
        tx.note = params[2]
        tx.owner = isAddress(accounts.encrypt) ? accounts.encrypt : ''
        tx.backupAccount = isAddress(accounts.backup) ? accounts.backup : ''
      }

      commit('txHashKeeper/SAVE_TX_HASH', tx, { root: true })
    } catch (e) {
      console.error('sendDeposit', e)
      return false
    }
  },
  async checkSpentEventFromNullifier({ getters, dispatch }, parsedNote) {
    try {
      const isSpent = await dispatch('loadEvent', {
        note: parsedNote,
        eventName: 'nullifierHash',
        type: eventsType.WITHDRAWAL,
        methodName: 'getAllWithdrawals',
        eventToFind: parsedNote.nullifierHex
      })

      return Boolean(isSpent)
    } catch (err) {
      console.error(`Method checkSpentEventFromNullifier has error: ${err.message}`)
    }
  },
  async checkRoot({ getters }, { root, parsedNote }) {
    const contractInstance = getters.instanceContract(parsedNote)

    const isKnownRoot = await contractInstance.methods.isKnownRoot(root).call()

    if (!isKnownRoot) {
      throw new Error(this.app.i18n.t('invalidRoot'))
    }
  },
  async buildTree({ dispatch }, { currency, amount, netId, commitmentHex }) {
    const treeInstanceName = `${currency}_${amount}_${netId}`
    const params = { netId, amount, currency }

    const treeService = treesInterface.getService({
      ...params,
      commitment: commitmentHex,
      instanceName: treeInstanceName
    })

    const [cachedTree, eventsData] = await Promise.all([
      treeService.getTree(),
      dispatch('updateEvents', { ...params, type: eventsType.DEPOSIT })
    ])

    const commitments = eventsData.events.map((el) => el.commitment.toString(10))

    let tree = cachedTree
    if (tree) {
      const newLeaves = commitments.slice(tree.elements.length)
      tree.bulkInsert(newLeaves)
    } else {
      console.log('events', eventsData)
      checkCommitments(eventsData.events)
      tree = treeService.createTree({ events: commitments })
    }

    const root = toFixedHex(tree.root)
    await dispatch('checkRoot', { root, parsedNote: params })

    await treeService.saveTree({ tree })

    return { tree, root }
  },
  async createSnarkProof(
    { rootGetters, rootState, state, getters },
    { root, note, tree, recipient, leafIndex }
  ) {
    const { pathElements, pathIndices } = tree.path(leafIndex)
    console.log('pathElements, pathIndices', pathElements, pathIndices)

    const nativeCurrency = rootGetters['metamask/nativeCurrency']
    const withdrawType = state.withdrawType

    let relayer = BigInt(recipient)
    let fee = BigInt(0)
    let refund = BigInt(0)

    if (withdrawType === 'relayer') {
      let totalRelayerFee = getters.relayerFee
      relayer = BigInt(rootState.relayer.selectedRelayer.address)

      if (note.currency !== nativeCurrency) {
        refund = BigInt(state.ethToReceive.toString())
        totalRelayerFee = totalRelayerFee.add(getters.ethToReceiveInToken)
      }

      fee = BigInt(totalRelayerFee.toString())
    }

    const input = {
      // public
      fee,
      root,
      refund,
      relayer,
      recipient: BigInt(recipient),
      nullifierHash: note.nullifierHash,
      // private
      pathIndices,
      pathElements,
      secret: note.secret,
      nullifier: note.nullifier
    }

    const { circuit, provingKey } = await getTornadoKeys()

    if (!groth16) {
      groth16 = await buildGroth16()
    }

    console.log('Start generating SNARK proof', input)
    console.time('SNARK proof time')
    const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, provingKey)
    const { proof } = websnarkUtils.toSolidityInput(proofData)

    const args = [
      toFixedHex(input.root),
      toFixedHex(input.nullifierHash),
      toFixedHex(input.recipient, 20),
      toFixedHex(input.relayer, 20),
      toFixedHex(input.fee),
      toFixedHex(input.refund)
    ]
    return { args, proof }
  },
  async prepareWithdraw({ dispatch, getters, commit }, { note, recipient }) {
    commit('REMOVE_ERRORS')
    commit('REMOVE_PROOF', { note })
    try {
      const parsedNote = parseNote(note)

      const { tree, root } = await dispatch('buildTree', parsedNote)

      const isSpent = await dispatch('checkSpentEventFromNullifier', parsedNote)

      if (isSpent) {
        throw new Error(this.app.i18n.t('noteHasBeenSpent'))
      }

      const { proof, args } = await dispatch('createSnarkProof', {
        root,
        tree,
        recipient,
        note: parsedNote,
        leafIndex: tree.indexOf(parsedNote.commitmentHex)
      })
      console.timeEnd('SNARK proof time')
      commit('SAVE_PROOF', { proof, args, note })
    } catch (e) {
      console.error('prepareWithdraw', e)
      commit('SAVE_ERROR', e.message)
    }
  },
  async withdraw({ state, rootState, dispatch, getters }, { note }) {
    try {
      const [, currency, amount, netId] = note.split('-')
      const config = networkConfig[`netId${netId}`]
      const { proof, args } = state.notes[note]
      const { ethAccount } = rootState.metamask

      const contractInstance = getters.tornadoProxyContract({ netId })

      const instance = config.tokens[currency].instanceAddress[amount]
      const params = [instance, proof, ...args]

      const data = contractInstance.methods.withdraw(...params).encodeABI()
      const gas = await contractInstance.methods
        .withdraw(...params)
        .estimateGas({ from: ethAccount, value: args[5] })

      const callParams = {
        method: 'eth_sendTransaction',
        params: {
          data,
          value: args[5],
          to: contractInstance._address,
          gas: numberToHex(gas + 200000)
        },
        watcherParams: {
          title: { path: 'withdrawing', amount, currency },
          successTitle: {
            amount,
            currency,
            path: 'withdrawnValue'
          },
          onSuccess: (txHash) => {
            dispatch('txHashKeeper/updateDeposit', { amount, currency, netId, note, txHash }, { root: true })
          }
        },
        isAwait: false,
        isSaving: false
      }

      await dispatch('metamask/sendTransaction', callParams, { root: true })
    } catch (e) {
      console.error(e)
      throw new Error(e.message)
    }
  },
  loadAllNotesData({ dispatch, rootGetters }) {
    const { tokens } = rootGetters['metamask/networkConfig']

    for (const [currency, { instanceAddress }] of Object.entries(tokens)) {
      for (const amount in instanceAddress) {
        if (instanceAddress[amount]) {
          dispatch('updateLastIndex', { currency, amount })
        }
      }
    }
  },
  async updateLastIndex({ dispatch, commit, rootState }, { currency, amount }) {
    const netId = rootState.metamask.netId
    const { nextDepositIndex, anonymitySet } = await dispatch('getLastDepositIndex', {
      currency,
      netId,
      amount
    })

    commit('SAVE_LAST_INDEX', {
      amount,
      currency,
      anonymitySet,
      nextDepositIndex
    })
  },
  async loadEvent({ getters, rootGetters }, { note, type, eventName, eventToFind }) {
    try {
      const eventService = getters.eventsInterface.getService(note)

      const foundEvent = await eventService.findEvent({ eventName, eventToFind, type })

      return foundEvent
    } catch (err) {
      console.error(`Method loadEvent has error: ${err.message}`)
    }
  },
  async loadDepositEvent({ state, dispatch }, { withdrawNote }) {
    try {
      const note = parseNote(withdrawNote)

      const lastEvent = await dispatch('loadEvent', {
        note,
        eventName: 'commitment',
        type: eventsType.DEPOSIT,
        methodName: 'getAllDeposits',
        eventToFind: note.commitmentHex
      })

      if (lastEvent) {
        const { nextDepositIndex } = state.statistic[note.currency][note.amount]
        const depositsPast = nextDepositIndex - lastEvent.leafIndex - 1

        const isSpent = await dispatch('checkSpentEventFromNullifier', note)

        return {
          isSpent,
          depositsPast,
          timestamp: lastEvent.timestamp,
          leafIndex: lastEvent.leafIndex,
          txHash: lastEvent.transactionHash,
          depositBlock: lastEvent.blockNumber
        }
      }
    } catch (err) {
      console.error(`Method loadDepositEvent has error: ${err.message}`)
    }
  },
  async loadWithdrawalEvent({ dispatch }, { withdrawNote }) {
    try {
      const note = parseNote(withdrawNote)

      const lastEvent = await dispatch('loadEvent', {
        note,
        eventName: 'nullifierHash',
        type: eventsType.WITHDRAWAL,
        methodName: 'getAllWithdrawals',
        eventToFind: note.nullifierHex
      })

      if (lastEvent) {
        return {
          to: lastEvent.to,
          fee: lastEvent.fee,
          txHash: lastEvent.transactionHash,
          blockNumber: lastEvent.blockNumber
        }
      }
    } catch (err) {
      console.error(`Method loadWithdrawalEvent has error: ${err.message}`)
    }
  },
  async loadWithdrawalData({ commit, dispatch, rootGetters }, { withdrawNote }) {
    try {
      const toDecimals = rootGetters['token/toDecimals']

      const { currency, amount } = parseNote(withdrawNote)
      const { fee, txHash, blockNumber, to } = await dispatch('loadWithdrawalEvent', { withdrawNote })

      const decimals = rootGetters['metamask/networkConfig'].tokens[currency].decimals
      const withdrawalAmount = toBN(rootGetters['token/fromDecimals'](amount.toString(), decimals)).sub(
        toBN(fee)
      )

      return {
        to,
        txHash,
        withdrawalBlock: blockNumber,
        fee: toDecimals(fee, decimals, 4),
        amount: toDecimals(withdrawalAmount, decimals, 4)
      }
    } catch (e) {
      console.error(`Method loadWithdrawalData has error: ${e}`)
      commit('SAVE_ERROR', e.message)
    }
  },
  calculateEthToReceive({ commit, state, rootGetters }, { currency }) {
    const gasLimit = rootGetters['metamask/networkConfig'].tokens[currency].gasLimit
    const gasPrice = toBN(rootGetters['gasPrices/fastGasPrice'])

    const ethToReceive = gasPrice
      .mul(toBN(gasLimit))
      .mul(toBN(2))
      .toString()
    return ethToReceive
  },
  async setDefaultEthToReceive({ dispatch, commit }, { currency }) {
    const ethToReceive = await dispatch('calculateEthToReceive', { currency })
    commit('SAVE_ETH_TO_RECEIVE', { ethToReceive })
    commit('SAVE_DEFAULT_ETH_TO_RECEIVE', { ethToReceive })
  },
  setNativeCurrency({ commit }, { netId }) {
    const currency = networkConfig[`netId${netId}`].nativeCurrency
    const amounts = Object.keys(networkConfig[`netId${netId}`].tokens[currency].instanceAddress)
    const amount = Math.min(...amounts)

    commit('SET_SELECTED_INSTANCE', { currency, amount })
    commit('SET_SELECTED_STATISTIC', { currency, amount })
  },
  async aggregateMulticall({ rootGetters, getters }, { params }) {
    try {
      const netId = rootGetters['metamask/netId']
      const multicallContract = getters.multicallContract({ netId })

      const result = await multicallContract.methods.aggregate(params).call()

      return result.returnData
    } catch (err) {
      console.log('err', err.message)
    }
  }
}
export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}