relayer.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. /* eslint-disable no-console */
  2. import Web3 from 'web3'
  3. import BN from 'bignumber.js'
  4. import namehash from 'eth-ens-namehash'
  5. import { schema, relayerRegisterService } from '@/services'
  6. import { createChainIdState, parseNote, parseSemanticVersion } from '@/utils'
  7. import ENSABI from '@/abis/ENS.abi.json'
  8. import networkConfig from '@/networkConfig'
  9. const getAxios = () => {
  10. return import('axios')
  11. }
  12. const calculateScore = ({ stakeBalance, tornadoServiceFee }, minFee = 0.33, maxFee = 0.53) => {
  13. if (tornadoServiceFee < minFee) {
  14. tornadoServiceFee = minFee
  15. } else if (tornadoServiceFee >= maxFee) {
  16. return new BN(0)
  17. }
  18. const serviceFeeCoefficient = (tornadoServiceFee - minFee) ** 2
  19. const feeDiffCoefficient = 1 / (maxFee - minFee) ** 2
  20. const coefficientsMultiplier = 1 - feeDiffCoefficient * serviceFeeCoefficient
  21. return new BN(stakeBalance).multipliedBy(coefficientsMultiplier)
  22. }
  23. const getWeightRandom = (weightsScores, random) => {
  24. for (let i = 0; i < weightsScores.length; i++) {
  25. if (random.isLessThan(weightsScores[i])) {
  26. return i
  27. }
  28. random = random.minus(weightsScores[i])
  29. }
  30. return Math.floor(Math.random() * weightsScores.length)
  31. }
  32. const pickWeightedRandomRelayer = (items, netId) => {
  33. let minFee, maxFee
  34. if (netId !== 1) {
  35. minFee = 0.01
  36. maxFee = 0.3
  37. }
  38. const weightsScores = items.map((el) => calculateScore(el, minFee, maxFee))
  39. const totalWeight = weightsScores.reduce((acc, curr) => {
  40. return (acc = acc.plus(curr))
  41. }, new BN('0'))
  42. const random = totalWeight.multipliedBy(Math.random())
  43. const weightRandomIndex = getWeightRandom(weightsScores, random)
  44. return items[weightRandomIndex]
  45. }
  46. const initialJobsState = createChainIdState({
  47. tornado: {}
  48. })
  49. export const state = () => {
  50. return {
  51. prices: {
  52. dai: '6700000000000000'
  53. },
  54. selectedRelayer: {
  55. url: '',
  56. name: '',
  57. stakeBalance: 0,
  58. tornadoServiceFee: 0.05,
  59. address: null,
  60. ethPrices: {
  61. torn: '1'
  62. }
  63. },
  64. isLoadingRelayers: false,
  65. validRelayers: [],
  66. jobs: initialJobsState,
  67. jobWatchers: {}
  68. }
  69. }
  70. export const getters = {
  71. ethProvider: (state, getters, rootState) => {
  72. const { url } = rootState.settings.netId1.rpc
  73. return new Web3(url)
  74. },
  75. jobs: (state, getters, rootState, rootGetters) => (type) => {
  76. const netId = rootGetters['metamask/netId']
  77. const jobsToRender = Object.entries(state.jobs[`netId${netId}`][type])
  78. .reverse()
  79. .map(
  80. ([
  81. id,
  82. {
  83. action,
  84. relayerUrl,
  85. amount,
  86. currency,
  87. fee,
  88. timestamp,
  89. txHash,
  90. confirmations,
  91. status,
  92. failedReason
  93. }
  94. ]) => {
  95. return {
  96. id,
  97. action,
  98. relayerUrl,
  99. amount,
  100. currency,
  101. fee,
  102. timestamp,
  103. txHash,
  104. confirmations,
  105. status,
  106. failedReason
  107. }
  108. }
  109. )
  110. return jobsToRender
  111. }
  112. }
  113. export const mutations = {
  114. SET_SELECTED_RELAYER(state, payload) {
  115. this._vm.$set(state, 'selectedRelayer', payload)
  116. },
  117. SAVE_VALIDATED_RELAYERS(state, relayers) {
  118. state.validRelayers = relayers
  119. },
  120. SAVE_JOB(
  121. state,
  122. {
  123. id,
  124. netId,
  125. type,
  126. action,
  127. relayerUrl,
  128. amount,
  129. currency,
  130. fee,
  131. commitmentHex,
  132. timestamp,
  133. note,
  134. accountAfter,
  135. account
  136. }
  137. ) {
  138. this._vm.$set(state.jobs[`netId${netId}`][type], id, {
  139. action,
  140. relayerUrl,
  141. amount,
  142. currency,
  143. fee,
  144. commitmentHex,
  145. timestamp,
  146. note,
  147. accountAfter,
  148. account
  149. })
  150. },
  151. UPDATE_JOB(state, { id, netId, type, txHash, confirmations, status, failedReason }) {
  152. const job = state.jobs[`netId${netId}`][type][id]
  153. this._vm.$set(state.jobs[`netId${netId}`][type], id, {
  154. ...job,
  155. txHash,
  156. confirmations,
  157. status,
  158. failedReason
  159. })
  160. },
  161. DELETE_JOB(state, { id, netId, type }) {
  162. this._vm.$delete(state.jobs[`netId${netId}`][type], id)
  163. },
  164. ADD_JOB_WATCHER(state, { id, timerId }) {
  165. this._vm.$set(state.jobWatchers, id, {
  166. timerId
  167. })
  168. },
  169. DELETE_JOB_WATCHER(state, { id }) {
  170. this._vm.$delete(state.jobWatchers, id)
  171. },
  172. SET_IS_LOADING_RELAYERS(state, isLoadingRelayers) {
  173. state.isLoadingRelayers = isLoadingRelayers
  174. }
  175. }
  176. export const actions = {
  177. async askRelayerStatus(
  178. { rootState, dispatch, rootGetters },
  179. { hostname, relayerAddress, stakeBalance, ensName }
  180. ) {
  181. try {
  182. const axios = await getAxios()
  183. if (!hostname.endsWith('/')) {
  184. hostname += '/'
  185. }
  186. const url = `${window.location.protocol}//${hostname}`
  187. const response = await axios.get(`${url}status`, { timeout: 5000 }).catch(() => {
  188. throw new Error(this.app.i18n.t('canNotFetchStatusFromTheRelayer'))
  189. })
  190. if (Number(response.data.currentQueue) > 5) {
  191. throw new Error(this.app.i18n.t('withdrawalQueueIsOverloaded'))
  192. }
  193. const netId = Number(rootGetters['metamask/netId'])
  194. if (Number(response.data.netId) !== netId) {
  195. throw new Error(this.app.i18n.t('thisRelayerServesADifferentNetwork'))
  196. }
  197. const validate = schema.getRelayerValidateFunction(netId)
  198. // check rewardAccount === relayerAddress for TORN burn, custom relayer - exception
  199. if (netId === 1 && relayerAddress && response.data.rewardAccount !== relayerAddress) {
  200. throw new Error('The Relayer reward address must match registered address')
  201. }
  202. const isValid = validate(response.data)
  203. if (!isValid) {
  204. console.error('askRelayerStatus', ensName, validate?.errors)
  205. throw new Error(this.app.i18n.t('canNotFetchStatusFromTheRelayer'))
  206. }
  207. const hasEnabledLightProxy = rootGetters['application/hasEnabledLightProxy']
  208. const getIsUpdated = () => {
  209. const relayerVersion = response.data.version
  210. if (relayerVersion === '5.0.0') {
  211. return true
  212. }
  213. const requiredMajor = hasEnabledLightProxy ? '5' : '4'
  214. const { major, patch, prerelease } = parseSemanticVersion(relayerVersion)
  215. const isUpdatedMajor = major === requiredMajor
  216. if (isUpdatedMajor && prerelease) {
  217. const minimalBeta = 11
  218. const [betaVersion] = prerelease.split('.').slice(-1)
  219. return Number(betaVersion) >= minimalBeta
  220. }
  221. const minimalPatch = 4
  222. return isUpdatedMajor && Number(patch) >= minimalPatch
  223. }
  224. if (!getIsUpdated()) {
  225. throw new Error('Outdated version.')
  226. }
  227. return {
  228. isValid,
  229. realUrl: url,
  230. stakeBalance,
  231. name: ensName,
  232. relayerAddress,
  233. netId: response.data.netId,
  234. ethPrices: response.data.ethPrices,
  235. address: response.data.rewardAccount,
  236. currentQueue: response.data.currentQueue,
  237. tornadoServiceFee: response.data.tornadoServiceFee
  238. }
  239. } catch (e) {
  240. console.error('askRelayerStatus', ensName, e.message)
  241. return { isValid: false, error: e.message }
  242. }
  243. },
  244. async observeRelayer({ dispatch }, { relayer }) {
  245. const result = await dispatch('askRelayerStatus', relayer)
  246. return result
  247. },
  248. async pickRandomRelayer({ rootGetters, commit, dispatch, getters }) {
  249. const netId = rootGetters['metamask/netId']
  250. const { ensSubdomainKey } = rootGetters['metamask/networkConfig']
  251. commit('SET_IS_LOADING_RELAYERS', true)
  252. const registeredRelayers = await relayerRegisterService(getters.ethProvider).getRelayers(ensSubdomainKey)
  253. const requests = []
  254. for (const registeredRelayer of registeredRelayers) {
  255. requests.push(dispatch('observeRelayer', { relayer: registeredRelayer }))
  256. }
  257. let statuses = await Promise.all(requests)
  258. statuses = statuses.filter((status) => status.isValid)
  259. // const validRelayerENSnames = statuses.map((relayer) => relayer.name)
  260. commit('SAVE_VALIDATED_RELAYERS', statuses)
  261. console.log('filtered statuses ', statuses)
  262. try {
  263. const {
  264. name,
  265. realUrl,
  266. address,
  267. ethPrices,
  268. stakeBalance,
  269. tornadoServiceFee
  270. } = pickWeightedRandomRelayer(statuses, netId)
  271. console.log('Selected relayer', name, tornadoServiceFee)
  272. commit('SET_SELECTED_RELAYER', {
  273. name,
  274. address,
  275. ethPrices,
  276. url: realUrl,
  277. stakeBalance,
  278. tornadoServiceFee
  279. })
  280. } catch {
  281. console.error('Method pickRandomRelayer has not picked relayer')
  282. }
  283. commit('SET_IS_LOADING_RELAYERS', false)
  284. },
  285. async getKnownRelayerData({ rootGetters, getters }, { relayerAddress, name }) {
  286. const { ensSubdomainKey } = rootGetters['metamask/networkConfig']
  287. const [validRelayer] = await relayerRegisterService(getters.ethProvider).getValidRelayers(
  288. [{ relayerAddress, ensName: name.replace(`${ensSubdomainKey}.`, '') }],
  289. ensSubdomainKey
  290. )
  291. console.warn('validRelayer', validRelayer)
  292. return validRelayer
  293. },
  294. async getCustomRelayerData({ rootState, state, getters, rootGetters, dispatch }, { url, name }) {
  295. const provider = getters.ethProvider.eth
  296. const PROTOCOL_REGEXP = /^(http(s?))/
  297. if (!PROTOCOL_REGEXP.test(url)) {
  298. if (url.endsWith('.onion')) {
  299. url = `http://${url}`
  300. } else {
  301. url = `https://${url}`
  302. }
  303. }
  304. const urlParser = new URL(url)
  305. urlParser.href = url
  306. let ensName = name
  307. if (urlParser.hostname.endsWith('.eth')) {
  308. ensName = urlParser.hostname
  309. let resolverInstance = await provider.ens.getResolver(ensName)
  310. if (new BN(resolverInstance._address).isZero()) {
  311. throw new Error('missingENSSubdomain')
  312. }
  313. resolverInstance = new provider.Contract(ENSABI, resolverInstance._address)
  314. const ensNameHash = namehash.hash(ensName)
  315. const hostname = await resolverInstance.methods.text(ensNameHash, 'url').call()
  316. if (!hostname) {
  317. throw new Error('canNotFetchStatusFromTheRelayer')
  318. }
  319. urlParser.host = hostname
  320. }
  321. const hostname = urlParser.host
  322. return { hostname, ensName, stakeBalance: 0 }
  323. },
  324. async getRelayerData({ state, dispatch }, { url, name }) {
  325. const knownRelayer = state.validRelayers.find((el) => el.name === name)
  326. if (knownRelayer) {
  327. const knownRelayerData = await dispatch('getKnownRelayerData', knownRelayer)
  328. return knownRelayerData
  329. }
  330. const customRelayerData = await dispatch('getCustomRelayerData', { url, name })
  331. return customRelayerData
  332. },
  333. async setupRelayer({ commit, rootState, dispatch }, { url, name }) {
  334. try {
  335. const relayerData = await dispatch('getRelayerData', { url, name })
  336. const { error, isValid, realUrl, address, ethPrices, tornadoServiceFee } = await dispatch(
  337. 'askRelayerStatus',
  338. relayerData
  339. )
  340. if (!isValid) {
  341. return { error, isValid: false }
  342. }
  343. return {
  344. isValid,
  345. name,
  346. url: realUrl || '',
  347. address: address || '',
  348. tornadoServiceFee: tornadoServiceFee || 0.0,
  349. ethPrices: ethPrices || { torn: '1' }
  350. }
  351. } catch (err) {
  352. return {
  353. isValid: false,
  354. error: this.app.i18n.t(err.message)
  355. }
  356. }
  357. },
  358. async relayTornadoWithdraw({ state, commit, dispatch, rootState }, { note }) {
  359. const { currency, netId, amount, commitmentHex } = parseNote(note)
  360. const config = networkConfig[`netId${netId}`]
  361. const contract = config.tokens[currency].instanceAddress[amount]
  362. try {
  363. const { proof, args } = rootState.application.notes[note]
  364. const message = {
  365. args,
  366. proof,
  367. contract
  368. }
  369. dispatch(
  370. 'loading/changeText',
  371. { message: this.app.i18n.t('relayerIsNowSendingYourTransaction') },
  372. { root: true }
  373. )
  374. const response = await fetch(state.selectedRelayer.url + 'v1/tornadoWithdraw', {
  375. method: 'POST',
  376. mode: 'cors',
  377. cache: 'no-cache',
  378. headers: {
  379. 'Content-Type': 'application/json'
  380. },
  381. redirect: 'error',
  382. body: JSON.stringify(message)
  383. })
  384. if (response.status === 400) {
  385. const { error } = await response.json()
  386. throw new Error(error)
  387. }
  388. if (response.status === 200) {
  389. const { id } = await response.json()
  390. const timestamp = Math.round(new Date().getTime() / 1000)
  391. commit('SAVE_JOB', {
  392. id,
  393. netId,
  394. type: 'tornado',
  395. action: 'Deposit',
  396. relayerUrl: state.selectedRelayer.url,
  397. commitmentHex,
  398. amount,
  399. currency,
  400. timestamp,
  401. note
  402. })
  403. dispatch('runJobWatcherWithNotifications', { id, type: 'tornado', netId })
  404. } else {
  405. throw new Error(this.app.i18n.t('unknownError'))
  406. }
  407. } catch (e) {
  408. console.error('relayTornadoWithdraw', e)
  409. const { name, url } = state.selectedRelayer
  410. throw new Error(this.app.i18n.t('relayRequestFailed', { relayerName: name === 'custom' ? url : name }))
  411. }
  412. },
  413. async runJobWatcherWithNotifications({ dispatch, state }, { routerLink, id, netId, type }) {
  414. const { amount, currency } = state.jobs[`netId${netId}`][type][id]
  415. const noticeId = await dispatch(
  416. 'notice/addNotice',
  417. {
  418. notice: {
  419. title: {
  420. path: 'withdrawing',
  421. amount,
  422. currency
  423. },
  424. type: 'loading',
  425. routerLink
  426. }
  427. },
  428. { root: true }
  429. )
  430. try {
  431. await dispatch('runJobWatcher', { id, netId, type, noticeId })
  432. dispatch('deleteJob', { id, netId, type })
  433. } catch (err) {
  434. dispatch(
  435. 'notice/updateNotice',
  436. {
  437. id: noticeId,
  438. notice: {
  439. title: 'transactionFailed',
  440. type: 'danger',
  441. routerLink: undefined
  442. }
  443. },
  444. { root: true }
  445. )
  446. dispatch(
  447. 'notice/addNoticeWithInterval',
  448. {
  449. notice: {
  450. title: 'relayerError',
  451. type: 'danger'
  452. }
  453. },
  454. { root: true }
  455. )
  456. }
  457. },
  458. deleteJob({ state, dispatch, commit }, { id, netId, type }) {
  459. dispatch('stopFinishJobWatcher', { id })
  460. const { amount, currency, action, fee, txHash, note } = state.jobs[`netId${netId}`][type][id]
  461. commit('DELETE_JOB', { id, netId, type })
  462. dispatch(
  463. 'txHashKeeper/updateDeposit',
  464. { amount, currency, netId, type, action, note, txHash, fee },
  465. { root: true }
  466. )
  467. },
  468. runJobWatcher({ state, dispatch }, { id, netId, type, noticeId }) {
  469. console.log('runJobWatcher started for job', id)
  470. return new Promise((resolve, reject) => {
  471. const getConfirmations = async ({ id, netId, type, noticeId, retryAttempt = 0, noticeCalls = 0 }) => {
  472. try {
  473. const job = state.jobs[`netId${netId}`][type][id]
  474. if (job.status === 'FAILED') {
  475. retryAttempt = 6
  476. throw new Error('Relayer is not responding')
  477. }
  478. const response = await fetch(`${job.relayerUrl}v1/jobs/${id}`, {
  479. method: 'GET',
  480. mode: 'cors',
  481. cache: 'no-cache',
  482. headers: {
  483. 'Content-Type': 'application/json'
  484. },
  485. redirect: 'error'
  486. })
  487. if (response.status === 400) {
  488. const { error } = await response.json()
  489. console.error('runJobWatcher', error)
  490. throw new Error(this.app.i18n.t('relayerError'))
  491. }
  492. if (response.status === 200) {
  493. await dispatch('handleResponse', {
  494. id,
  495. response,
  496. job,
  497. type,
  498. netId,
  499. retryAttempt,
  500. noticeId,
  501. noticeCalls,
  502. resolve,
  503. getConfirmations
  504. })
  505. } else {
  506. throw new Error(this.app.i18n.t('unknownError'))
  507. }
  508. } catch (e) {
  509. if (retryAttempt < 5) {
  510. retryAttempt++
  511. setTimeout(
  512. () =>
  513. getConfirmations({
  514. id,
  515. netId,
  516. type,
  517. noticeId,
  518. retryAttempt,
  519. noticeCalls
  520. }),
  521. 3000
  522. )
  523. }
  524. reject(e.message)
  525. }
  526. }
  527. getConfirmations({ id, netId, type, noticeId })
  528. dispatch('finishJobWatcher', { id, netId, type })
  529. })
  530. },
  531. async handleResponse(
  532. { state, rootGetters, commit, dispatch, getters, rootState },
  533. { response, id, job, type, netId, retryAttempt, resolve, getConfirmations, noticeId, noticeCalls }
  534. ) {
  535. const { amount, currency } = job
  536. const { txHash, confirmations, status, failedReason } = await response.json()
  537. console.log('txHash, confirmations, status, failedReason', txHash, confirmations, status, failedReason)
  538. commit('UPDATE_JOB', { id, netId, type, txHash, confirmations, status, failedReason })
  539. if (status === 'FAILED') {
  540. dispatch('stopFinishJobWatcher', { id })
  541. commit('DELETE_JOB', { id, netId, type })
  542. retryAttempt = 6
  543. console.error('runJobWatcher.handleResponse', failedReason)
  544. throw new Error(this.app.i18n.t('relayerError'))
  545. }
  546. if (txHash && noticeCalls === 0 && (Number(confirmations) > 0 || status === 'CONFIRMED')) {
  547. noticeCalls++
  548. dispatch(
  549. 'notice/updateNotice',
  550. {
  551. id: noticeId,
  552. notice: {
  553. title: {
  554. path: 'withdrawnValue',
  555. amount,
  556. currency
  557. },
  558. type: 'success',
  559. txHash
  560. },
  561. interval: 10000
  562. },
  563. { root: true }
  564. )
  565. }
  566. if (status === 'CONFIRMED') {
  567. console.log(`Job ${id} has enough confirmations`)
  568. resolve(txHash)
  569. } else {
  570. setTimeout(() => getConfirmations({ id, netId, type, noticeId, retryAttempt, noticeCalls }), 3000)
  571. }
  572. },
  573. finishJobWatcher({ state, rootGetters, commit, dispatch, getters, rootState }, { id, netId, type }) {
  574. const timerId = setTimeout(() => {
  575. const { txHash, confirmations } = state.jobs[`netId${netId}`][type][id]
  576. commit('UPDATE_JOB', {
  577. id,
  578. netId,
  579. type,
  580. txHash,
  581. confirmations,
  582. status: 'FAILED',
  583. failedReason: this.app.i18n.t('relayerIsNotResponding')
  584. })
  585. commit('DELETE_JOB_WATCHER', { id })
  586. }, 15 * 60 * 1000)
  587. commit('ADD_JOB_WATCHER', { id, timerId })
  588. },
  589. stopFinishJobWatcher({ state, rootGetters, commit, dispatch, getters, rootState }, { id }) {
  590. console.log(`Stop finishJobWatcher ${id}`)
  591. const { timerId } = state.jobWatchers[id]
  592. clearTimeout(timerId)
  593. commit('DELETE_JOB_WATCHER', { id })
  594. },
  595. runAllJobs({ state, commit, dispatch, rootState }) {
  596. const netId = rootState.metamask.netId
  597. const jobs = state.jobs[`netId${netId}`]
  598. for (const type in jobs) {
  599. for (const [id, { status }] of Object.entries(jobs[type])) {
  600. const job = { id, netId, type }
  601. if (status === 'FAILED') {
  602. commit('DELETE_JOB', job)
  603. } else {
  604. dispatch('runJobWatcherWithNotifications', job)
  605. }
  606. }
  607. }
  608. }
  609. }