123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688 |
- /* eslint-disable no-console */
- import Web3 from 'web3'
- import BN from 'bignumber.js'
- import namehash from 'eth-ens-namehash'
- import { schema, relayerRegisterService } from '@/services'
- import { createChainIdState, parseNote, parseSemanticVersion } from '@/utils'
- import ENSABI from '@/abis/ENS.abi.json'
- import networkConfig from '@/networkConfig'
- const getAxios = () => {
- return import('axios')
- }
- const calculateScore = ({ stakeBalance, tornadoServiceFee }, minFee = 0.33, maxFee = 0.53) => {
- if (tornadoServiceFee < minFee) {
- tornadoServiceFee = minFee
- } else if (tornadoServiceFee >= maxFee) {
- return new BN(0)
- }
- const serviceFeeCoefficient = (tornadoServiceFee - minFee) ** 2
- const feeDiffCoefficient = 1 / (maxFee - minFee) ** 2
- const coefficientsMultiplier = 1 - feeDiffCoefficient * serviceFeeCoefficient
- return new BN(stakeBalance).multipliedBy(coefficientsMultiplier)
- }
- const getWeightRandom = (weightsScores, random) => {
- for (let i = 0; i < weightsScores.length; i++) {
- if (random.isLessThan(weightsScores[i])) {
- return i
- }
- random = random.minus(weightsScores[i])
- }
- return Math.floor(Math.random() * weightsScores.length)
- }
- const pickWeightedRandomRelayer = (items, netId) => {
- let minFee, maxFee
- if (netId !== 1) {
- minFee = 0.01
- maxFee = 0.3
- }
- const weightsScores = items.map((el) => calculateScore(el, minFee, maxFee))
- const totalWeight = weightsScores.reduce((acc, curr) => {
- return (acc = acc.plus(curr))
- }, new BN('0'))
- const random = totalWeight.multipliedBy(Math.random())
- const weightRandomIndex = getWeightRandom(weightsScores, random)
- return items[weightRandomIndex]
- }
- const initialJobsState = createChainIdState({
- tornado: {}
- })
- export const state = () => {
- return {
- prices: {
- dai: '6700000000000000'
- },
- selectedRelayer: {
- url: '',
- name: '',
- stakeBalance: 0,
- tornadoServiceFee: 0.05,
- address: null,
- ethPrices: {
- torn: '1'
- }
- },
- isLoadingRelayers: false,
- validRelayers: [],
- jobs: initialJobsState,
- jobWatchers: {}
- }
- }
- export const getters = {
- ethProvider: (state, getters, rootState) => {
- const { url } = rootState.settings.netId1.rpc
- return new Web3(url)
- },
- jobs: (state, getters, rootState, rootGetters) => (type) => {
- const netId = rootGetters['metamask/netId']
- const jobsToRender = Object.entries(state.jobs[`netId${netId}`][type])
- .reverse()
- .map(
- ([
- id,
- {
- action,
- relayerUrl,
- amount,
- currency,
- fee,
- timestamp,
- txHash,
- confirmations,
- status,
- failedReason
- }
- ]) => {
- return {
- id,
- action,
- relayerUrl,
- amount,
- currency,
- fee,
- timestamp,
- txHash,
- confirmations,
- status,
- failedReason
- }
- }
- )
- return jobsToRender
- }
- }
- export const mutations = {
- SET_SELECTED_RELAYER(state, payload) {
- this._vm.$set(state, 'selectedRelayer', payload)
- },
- SAVE_VALIDATED_RELAYERS(state, relayers) {
- state.validRelayers = relayers
- },
- SAVE_JOB(
- state,
- {
- id,
- netId,
- type,
- action,
- relayerUrl,
- amount,
- currency,
- fee,
- commitmentHex,
- timestamp,
- note,
- accountAfter,
- account
- }
- ) {
- this._vm.$set(state.jobs[`netId${netId}`][type], id, {
- action,
- relayerUrl,
- amount,
- currency,
- fee,
- commitmentHex,
- timestamp,
- note,
- accountAfter,
- account
- })
- },
- UPDATE_JOB(state, { id, netId, type, txHash, confirmations, status, failedReason }) {
- const job = state.jobs[`netId${netId}`][type][id]
- this._vm.$set(state.jobs[`netId${netId}`][type], id, {
- ...job,
- txHash,
- confirmations,
- status,
- failedReason
- })
- },
- DELETE_JOB(state, { id, netId, type }) {
- this._vm.$delete(state.jobs[`netId${netId}`][type], id)
- },
- ADD_JOB_WATCHER(state, { id, timerId }) {
- this._vm.$set(state.jobWatchers, id, {
- timerId
- })
- },
- DELETE_JOB_WATCHER(state, { id }) {
- this._vm.$delete(state.jobWatchers, id)
- },
- SET_IS_LOADING_RELAYERS(state, isLoadingRelayers) {
- state.isLoadingRelayers = isLoadingRelayers
- }
- }
- export const actions = {
- async askRelayerStatus(
- { rootState, dispatch, rootGetters },
- { hostname, relayerAddress, stakeBalance, ensName }
- ) {
- try {
- const axios = await getAxios()
- if (!hostname.endsWith('/')) {
- hostname += '/'
- }
- const url = `${window.location.protocol}//${hostname}`
- const response = await axios.get(`${url}status`, { timeout: 5000 }).catch(() => {
- throw new Error(this.app.i18n.t('canNotFetchStatusFromTheRelayer'))
- })
- if (Number(response.data.currentQueue) > 5) {
- throw new Error(this.app.i18n.t('withdrawalQueueIsOverloaded'))
- }
- const netId = Number(rootGetters['metamask/netId'])
- if (Number(response.data.netId) !== netId) {
- throw new Error(this.app.i18n.t('thisRelayerServesADifferentNetwork'))
- }
- const validate = schema.getRelayerValidateFunction(netId)
- // check rewardAccount === relayerAddress for TORN burn, custom relayer - exception
- if (netId === 1 && relayerAddress && response.data.rewardAccount !== relayerAddress) {
- throw new Error('The Relayer reward address must match registered address')
- }
- const isValid = validate(response.data)
- if (!isValid) {
- console.error('askRelayerStatus', ensName, validate?.errors)
- throw new Error(this.app.i18n.t('canNotFetchStatusFromTheRelayer'))
- }
- const hasEnabledLightProxy = rootGetters['application/hasEnabledLightProxy']
- const getIsUpdated = () => {
- const relayerVersion = response.data.version
- if (relayerVersion === '5.0.0') {
- return true
- }
- const requiredMajor = hasEnabledLightProxy ? '5' : '4'
- const { major, patch, prerelease } = parseSemanticVersion(relayerVersion)
- const isUpdatedMajor = major === requiredMajor
- if (isUpdatedMajor && prerelease) {
- const minimalBeta = 11
- const [betaVersion] = prerelease.split('.').slice(-1)
- return Number(betaVersion) >= minimalBeta
- }
- const minimalPatch = 4
- return isUpdatedMajor && Number(patch) >= minimalPatch
- }
- if (!getIsUpdated()) {
- throw new Error('Outdated version.')
- }
- return {
- isValid,
- realUrl: url,
- stakeBalance,
- name: ensName,
- relayerAddress,
- netId: response.data.netId,
- ethPrices: response.data.ethPrices,
- address: response.data.rewardAccount,
- currentQueue: response.data.currentQueue,
- tornadoServiceFee: response.data.tornadoServiceFee
- }
- } catch (e) {
- console.error('askRelayerStatus', ensName, e.message)
- return { isValid: false, error: e.message }
- }
- },
- async observeRelayer({ dispatch }, { relayer }) {
- const result = await dispatch('askRelayerStatus', relayer)
- return result
- },
- async pickRandomRelayer({ rootGetters, commit, dispatch, getters }) {
- const netId = rootGetters['metamask/netId']
- const { ensSubdomainKey } = rootGetters['metamask/networkConfig']
- commit('SET_IS_LOADING_RELAYERS', true)
- const registeredRelayers = await relayerRegisterService(getters.ethProvider).getRelayers(ensSubdomainKey)
- const requests = []
- for (const registeredRelayer of registeredRelayers) {
- requests.push(dispatch('observeRelayer', { relayer: registeredRelayer }))
- }
- let statuses = await Promise.all(requests)
- statuses = statuses.filter((status) => status.isValid)
- // const validRelayerENSnames = statuses.map((relayer) => relayer.name)
- commit('SAVE_VALIDATED_RELAYERS', statuses)
- console.log('filtered statuses ', statuses)
- try {
- const {
- name,
- realUrl,
- address,
- ethPrices,
- stakeBalance,
- tornadoServiceFee
- } = pickWeightedRandomRelayer(statuses, netId)
- console.log('Selected relayer', name, tornadoServiceFee)
- commit('SET_SELECTED_RELAYER', {
- name,
- address,
- ethPrices,
- url: realUrl,
- stakeBalance,
- tornadoServiceFee
- })
- } catch {
- console.error('Method pickRandomRelayer has not picked relayer')
- }
- commit('SET_IS_LOADING_RELAYERS', false)
- },
- async getKnownRelayerData({ rootGetters, getters }, { relayerAddress, name }) {
- const { ensSubdomainKey } = rootGetters['metamask/networkConfig']
- const [validRelayer] = await relayerRegisterService(getters.ethProvider).getValidRelayers(
- [{ relayerAddress, ensName: name.replace(`${ensSubdomainKey}.`, '') }],
- ensSubdomainKey
- )
- console.warn('validRelayer', validRelayer)
- return validRelayer
- },
- async getCustomRelayerData({ rootState, state, getters, rootGetters, dispatch }, { url, name }) {
- const provider = getters.ethProvider.eth
- const PROTOCOL_REGEXP = /^(http(s?))/
- if (!PROTOCOL_REGEXP.test(url)) {
- if (url.endsWith('.onion')) {
- url = `http://${url}`
- } else {
- url = `https://${url}`
- }
- }
- const urlParser = new URL(url)
- urlParser.href = url
- let ensName = name
- if (urlParser.hostname.endsWith('.eth')) {
- ensName = urlParser.hostname
- let resolverInstance = await provider.ens.getResolver(ensName)
- if (new BN(resolverInstance._address).isZero()) {
- throw new Error('missingENSSubdomain')
- }
- resolverInstance = new provider.Contract(ENSABI, resolverInstance._address)
- const ensNameHash = namehash.hash(ensName)
- const hostname = await resolverInstance.methods.text(ensNameHash, 'url').call()
- if (!hostname) {
- throw new Error('canNotFetchStatusFromTheRelayer')
- }
- urlParser.host = hostname
- }
- const hostname = urlParser.host
- return { hostname, ensName, stakeBalance: 0 }
- },
- async getRelayerData({ state, dispatch }, { url, name }) {
- const knownRelayer = state.validRelayers.find((el) => el.name === name)
- if (knownRelayer) {
- const knownRelayerData = await dispatch('getKnownRelayerData', knownRelayer)
- return knownRelayerData
- }
- const customRelayerData = await dispatch('getCustomRelayerData', { url, name })
- return customRelayerData
- },
- async setupRelayer({ commit, rootState, dispatch }, { url, name }) {
- try {
- const relayerData = await dispatch('getRelayerData', { url, name })
- const { error, isValid, realUrl, address, ethPrices, tornadoServiceFee } = await dispatch(
- 'askRelayerStatus',
- relayerData
- )
- if (!isValid) {
- return { error, isValid: false }
- }
- return {
- isValid,
- name,
- url: realUrl || '',
- address: address || '',
- tornadoServiceFee: tornadoServiceFee || 0.0,
- ethPrices: ethPrices || { torn: '1' }
- }
- } catch (err) {
- return {
- isValid: false,
- error: this.app.i18n.t(err.message)
- }
- }
- },
- async relayTornadoWithdraw({ state, commit, dispatch, rootState }, { note }) {
- const { currency, netId, amount, commitmentHex } = parseNote(note)
- const config = networkConfig[`netId${netId}`]
- const contract = config.tokens[currency].instanceAddress[amount]
- try {
- const { proof, args } = rootState.application.notes[note]
- const message = {
- args,
- proof,
- contract
- }
- dispatch(
- 'loading/changeText',
- { message: this.app.i18n.t('relayerIsNowSendingYourTransaction') },
- { root: true }
- )
- const response = await fetch(state.selectedRelayer.url + 'v1/tornadoWithdraw', {
- method: 'POST',
- mode: 'cors',
- cache: 'no-cache',
- headers: {
- 'Content-Type': 'application/json'
- },
- redirect: 'error',
- body: JSON.stringify(message)
- })
- if (response.status === 400) {
- const { error } = await response.json()
- throw new Error(error)
- }
- if (response.status === 200) {
- const { id } = await response.json()
- const timestamp = Math.round(new Date().getTime() / 1000)
- commit('SAVE_JOB', {
- id,
- netId,
- type: 'tornado',
- action: 'Deposit',
- relayerUrl: state.selectedRelayer.url,
- commitmentHex,
- amount,
- currency,
- timestamp,
- note
- })
- dispatch('runJobWatcherWithNotifications', { id, type: 'tornado', netId })
- } else {
- throw new Error(this.app.i18n.t('unknownError'))
- }
- } catch (e) {
- console.error('relayTornadoWithdraw', e)
- const { name, url } = state.selectedRelayer
- throw new Error(this.app.i18n.t('relayRequestFailed', { relayerName: name === 'custom' ? url : name }))
- }
- },
- async runJobWatcherWithNotifications({ dispatch, state }, { routerLink, id, netId, type }) {
- const { amount, currency } = state.jobs[`netId${netId}`][type][id]
- const noticeId = await dispatch(
- 'notice/addNotice',
- {
- notice: {
- title: {
- path: 'withdrawing',
- amount,
- currency
- },
- type: 'loading',
- routerLink
- }
- },
- { root: true }
- )
- try {
- await dispatch('runJobWatcher', { id, netId, type, noticeId })
- dispatch('deleteJob', { id, netId, type })
- } catch (err) {
- dispatch(
- 'notice/updateNotice',
- {
- id: noticeId,
- notice: {
- title: 'transactionFailed',
- type: 'danger',
- routerLink: undefined
- }
- },
- { root: true }
- )
- dispatch(
- 'notice/addNoticeWithInterval',
- {
- notice: {
- title: 'relayerError',
- type: 'danger'
- }
- },
- { root: true }
- )
- }
- },
- deleteJob({ state, dispatch, commit }, { id, netId, type }) {
- dispatch('stopFinishJobWatcher', { id })
- const { amount, currency, action, fee, txHash, note } = state.jobs[`netId${netId}`][type][id]
- commit('DELETE_JOB', { id, netId, type })
- dispatch(
- 'txHashKeeper/updateDeposit',
- { amount, currency, netId, type, action, note, txHash, fee },
- { root: true }
- )
- },
- runJobWatcher({ state, dispatch }, { id, netId, type, noticeId }) {
- console.log('runJobWatcher started for job', id)
- return new Promise((resolve, reject) => {
- const getConfirmations = async ({ id, netId, type, noticeId, retryAttempt = 0, noticeCalls = 0 }) => {
- try {
- const job = state.jobs[`netId${netId}`][type][id]
- if (job.status === 'FAILED') {
- retryAttempt = 6
- throw new Error('Relayer is not responding')
- }
- const response = await fetch(`${job.relayerUrl}v1/jobs/${id}`, {
- method: 'GET',
- mode: 'cors',
- cache: 'no-cache',
- headers: {
- 'Content-Type': 'application/json'
- },
- redirect: 'error'
- })
- if (response.status === 400) {
- const { error } = await response.json()
- console.error('runJobWatcher', error)
- throw new Error(this.app.i18n.t('relayerError'))
- }
- if (response.status === 200) {
- await dispatch('handleResponse', {
- id,
- response,
- job,
- type,
- netId,
- retryAttempt,
- noticeId,
- noticeCalls,
- resolve,
- getConfirmations
- })
- } else {
- throw new Error(this.app.i18n.t('unknownError'))
- }
- } catch (e) {
- if (retryAttempt < 5) {
- retryAttempt++
- setTimeout(
- () =>
- getConfirmations({
- id,
- netId,
- type,
- noticeId,
- retryAttempt,
- noticeCalls
- }),
- 3000
- )
- }
- reject(e.message)
- }
- }
- getConfirmations({ id, netId, type, noticeId })
- dispatch('finishJobWatcher', { id, netId, type })
- })
- },
- async handleResponse(
- { state, rootGetters, commit, dispatch, getters, rootState },
- { response, id, job, type, netId, retryAttempt, resolve, getConfirmations, noticeId, noticeCalls }
- ) {
- const { amount, currency } = job
- const { txHash, confirmations, status, failedReason } = await response.json()
- console.log('txHash, confirmations, status, failedReason', txHash, confirmations, status, failedReason)
- commit('UPDATE_JOB', { id, netId, type, txHash, confirmations, status, failedReason })
- if (status === 'FAILED') {
- dispatch('stopFinishJobWatcher', { id })
- commit('DELETE_JOB', { id, netId, type })
- retryAttempt = 6
- console.error('runJobWatcher.handleResponse', failedReason)
- throw new Error(this.app.i18n.t('relayerError'))
- }
- if (txHash && noticeCalls === 0 && (Number(confirmations) > 0 || status === 'CONFIRMED')) {
- noticeCalls++
- dispatch(
- 'notice/updateNotice',
- {
- id: noticeId,
- notice: {
- title: {
- path: 'withdrawnValue',
- amount,
- currency
- },
- type: 'success',
- txHash
- },
- interval: 10000
- },
- { root: true }
- )
- }
- if (status === 'CONFIRMED') {
- console.log(`Job ${id} has enough confirmations`)
- resolve(txHash)
- } else {
- setTimeout(() => getConfirmations({ id, netId, type, noticeId, retryAttempt, noticeCalls }), 3000)
- }
- },
- finishJobWatcher({ state, rootGetters, commit, dispatch, getters, rootState }, { id, netId, type }) {
- const timerId = setTimeout(() => {
- const { txHash, confirmations } = state.jobs[`netId${netId}`][type][id]
- commit('UPDATE_JOB', {
- id,
- netId,
- type,
- txHash,
- confirmations,
- status: 'FAILED',
- failedReason: this.app.i18n.t('relayerIsNotResponding')
- })
- commit('DELETE_JOB_WATCHER', { id })
- }, 15 * 60 * 1000)
- commit('ADD_JOB_WATCHER', { id, timerId })
- },
- stopFinishJobWatcher({ state, rootGetters, commit, dispatch, getters, rootState }, { id }) {
- console.log(`Stop finishJobWatcher ${id}`)
- const { timerId } = state.jobWatchers[id]
- clearTimeout(timerId)
- commit('DELETE_JOB_WATCHER', { id })
- },
- runAllJobs({ state, commit, dispatch, rootState }) {
- const netId = rootState.metamask.netId
- const jobs = state.jobs[`netId${netId}`]
- for (const type in jobs) {
- for (const [id, { status }] of Object.entries(jobs[type])) {
- const job = { id, netId, type }
- if (status === 'FAILED') {
- commit('DELETE_JOB', job)
- } else {
- dispatch('runJobWatcherWithNotifications', job)
- }
- }
- }
- }
- }
|