init
This commit is contained in:
commit
44f31f8b9f
402 changed files with 47865 additions and 0 deletions
981
store/application.js
Normal file
981
store/application.js
Normal file
|
@ -0,0 +1,981 @@
|
|||
/* 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, rootState, rootGetters) => {
|
||||
const netId = rootGetters['metamask/netId']
|
||||
let action = ACTION.WITHDRAW_WITH_EXTRA
|
||||
|
||||
if (getters.hasEnabledLightProxy) {
|
||||
action = ACTION.WITHDRAW
|
||||
}
|
||||
|
||||
if (Number(netId) === 10) {
|
||||
action = ACTION.OP_WITHDRAW
|
||||
}
|
||||
|
||||
if (Number(netId) === 42161) {
|
||||
action = ACTION.ARB_WITHDRAW
|
||||
}
|
||||
|
||||
return ACTION_GAS[action]
|
||||
},
|
||||
networkFee: (state, getters, rootState, rootGetters) => {
|
||||
const gasPrice = rootGetters['gasPrices/fastGasPrice']
|
||||
|
||||
return toBN(gasPrice).mul(toBN(getters.withdrawGas))
|
||||
},
|
||||
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'])
|
||||
}
|
||||
}
|
||||
|
||||
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 { NOTE_ACCOUNT_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
|
||||
}
|
3
store/encryptedNote.js
Normal file
3
store/encryptedNote.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { actions, getters, state, mutations } from '@/modules/account/store'
|
||||
|
||||
export { actions, getters, mutations, state }
|
191
store/gasPrices.js
Normal file
191
store/gasPrices.js
Normal file
|
@ -0,0 +1,191 @@
|
|||
/* eslint-disable no-console */
|
||||
import { GasPriceOracle } from 'gas-price-oracle'
|
||||
import { toHex, toWei, toBN, fromWei } from 'web3-utils'
|
||||
|
||||
export const state = () => {
|
||||
return {
|
||||
oracle: {
|
||||
instant: 80,
|
||||
fast: 50,
|
||||
standard: 25,
|
||||
low: 8
|
||||
},
|
||||
eip: {
|
||||
instant: {
|
||||
baseFee: 80,
|
||||
maxFeePerGas: 80,
|
||||
maxPriorityFeePerGas: 3
|
||||
},
|
||||
fast: {
|
||||
baseFee: 50,
|
||||
maxFeePerGas: 50,
|
||||
maxPriorityFeePerGas: 3
|
||||
},
|
||||
standard: {
|
||||
baseFee: 25,
|
||||
maxFeePerGas: 27,
|
||||
maxPriorityFeePerGas: 2
|
||||
},
|
||||
low: {
|
||||
baseFee: 8,
|
||||
maxFeePerGas: 9,
|
||||
maxPriorityFeePerGas: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
oracle: (state, getters, rootState, rootGetters) => {
|
||||
const netId = Number(rootGetters['metamask/netId'])
|
||||
const { gasPrices } = rootGetters['metamask/networkConfig']
|
||||
|
||||
return new GasPriceOracle({
|
||||
chainId: netId,
|
||||
defaultRpc: rootGetters['settings/currentRpc'].url,
|
||||
defaultFallbackGasPrices: gasPrices
|
||||
})
|
||||
},
|
||||
eipSupported: (state, getters, rootState, rootGetters) => {
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const networksWithEIP1559 = [1, 5]
|
||||
|
||||
return networksWithEIP1559.includes(netId)
|
||||
},
|
||||
getGasParams: (state, getters) => (speed = 'fast', isDisable = false) => {
|
||||
const { maxFeePerGas, maxPriorityFeePerGas } = state.eip[speed]
|
||||
|
||||
if (!isDisable && getters.eipSupported) {
|
||||
return {
|
||||
maxFeePerGas: toHex(maxFeePerGas),
|
||||
maxPriorityFeePerGas: toHex(maxPriorityFeePerGas)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
gasPrice: getters.getGasPrice(speed)
|
||||
}
|
||||
},
|
||||
getGasPrice: (state, getters) => (speed = 'fast') => {
|
||||
const gasPrices = getters.gasPrices
|
||||
return toHex(toWei(gasPrices[speed].toString(), 'gwei'))
|
||||
},
|
||||
fastGasPrice: (state, getters) => {
|
||||
return getters.getGasPrice('fast')
|
||||
},
|
||||
gasPrices: (state, getters) => {
|
||||
const parseGwei = (value) => String(Math.floor(Number(fromWei(String(value), 'gwei')) * 100) / 100)
|
||||
|
||||
const { eip, oracle } = state
|
||||
|
||||
if (getters.eipSupported) {
|
||||
return {
|
||||
instant: parseGwei(eip.instant.maxFeePerGas),
|
||||
low: parseGwei(eip.low.maxFeePerGas),
|
||||
standard: parseGwei(eip.standard.maxFeePerGas),
|
||||
fast: parseGwei(eip.fast.maxFeePerGas)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
instant: String(oracle.instant),
|
||||
low: String(oracle.low),
|
||||
standard: String(oracle.standard),
|
||||
fast: String(oracle.fast)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
SAVE_ORACLE_GAS_PRICES(state, { instant, fast, standard, low }) {
|
||||
this._vm.$set(state.oracle, 'instant', instant)
|
||||
this._vm.$set(state.oracle, 'fast', fast)
|
||||
this._vm.$set(state.oracle, 'standard', standard)
|
||||
this._vm.$set(state.oracle, 'low', low)
|
||||
},
|
||||
SAVE_EIP_GAS_PRICES(state, { instant, fast, standard, low }) {
|
||||
this._vm.$set(state.eip, 'instant', instant)
|
||||
this._vm.$set(state.eip, 'fast', fast)
|
||||
this._vm.$set(state.eip, 'standard', standard)
|
||||
this._vm.$set(state.eip, 'low', low)
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
async fetchGasPrice({ getters, commit, dispatch, rootGetters, state }) {
|
||||
const { pollInterval } = rootGetters['metamask/networkConfig']
|
||||
|
||||
try {
|
||||
if (getters.eipSupported) {
|
||||
const result = await dispatch('estimateFees')
|
||||
commit('SAVE_EIP_GAS_PRICES', result)
|
||||
} else {
|
||||
const gas = await dispatch('getGasPrice')
|
||||
commit('SAVE_ORACLE_GAS_PRICES', gas)
|
||||
}
|
||||
|
||||
setTimeout(() => dispatch('fetchGasPrice'), 1000 * pollInterval)
|
||||
} catch (e) {
|
||||
console.error('fetchGasPrice', e)
|
||||
setTimeout(() => dispatch('fetchGasPrice'), 1000 * pollInterval)
|
||||
}
|
||||
},
|
||||
async estimateFees({ rootGetters }) {
|
||||
try {
|
||||
const { url } = rootGetters['settings/currentRpc']
|
||||
const web3 = this.$provider.getWeb3(url)
|
||||
const latestBlock = await web3.eth.getBlock('latest')
|
||||
|
||||
if (!latestBlock.baseFeePerGas) {
|
||||
throw new Error('An error occurred while fetching current base fee, falling back')
|
||||
}
|
||||
|
||||
const baseFee = toBN(latestBlock.baseFeePerGas)
|
||||
|
||||
const potentialMaxFee = baseFee.mul(toBN(1125)).div(toBN(1000))
|
||||
|
||||
const GWEI = (amount) => toBN(toWei(amount, 'gwei'))
|
||||
|
||||
const fastPriorityFee = GWEI('4')
|
||||
const standardPriorityFee = GWEI('2.5')
|
||||
const lowPriorityFee = GWEI('1')
|
||||
|
||||
return {
|
||||
instant: {
|
||||
baseFee,
|
||||
maxFeePerGas: potentialMaxFee.add(fastPriorityFee),
|
||||
maxPriorityFeePerGas: fastPriorityFee
|
||||
},
|
||||
fast: {
|
||||
baseFee,
|
||||
maxFeePerGas: potentialMaxFee.add(fastPriorityFee),
|
||||
maxPriorityFeePerGas: fastPriorityFee
|
||||
},
|
||||
standard: {
|
||||
baseFee,
|
||||
maxFeePerGas: potentialMaxFee.add(standardPriorityFee),
|
||||
maxPriorityFeePerGas: standardPriorityFee
|
||||
},
|
||||
low: {
|
||||
baseFee,
|
||||
maxFeePerGas: potentialMaxFee.add(lowPriorityFee),
|
||||
maxPriorityFeePerGas: lowPriorityFee
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(err.message)
|
||||
}
|
||||
},
|
||||
async getGasPrice({ state, getters }) {
|
||||
try {
|
||||
const gas = await getters.oracle.gasPrices(state.oracle)
|
||||
return gas
|
||||
} catch (err) {
|
||||
throw new Error(err.message)
|
||||
}
|
||||
},
|
||||
setDefault({ commit, rootGetters }) {
|
||||
const { gasPrices } = rootGetters['metamask/networkConfig']
|
||||
commit('SAVE_ORACLE_GAS_PRICES', gasPrices)
|
||||
}
|
||||
}
|
922
store/governance/gov.js
Normal file
922
store/governance/gov.js
Normal file
|
@ -0,0 +1,922 @@
|
|||
/* eslint-disable no-console */
|
||||
/* eslint-disable import/order */
|
||||
import Web3 from 'web3'
|
||||
import { ToastProgrammatic as Toast } from 'buefy'
|
||||
|
||||
import networkConfig from '@/networkConfig'
|
||||
|
||||
import ERC20ABI from '@/abis/Governance.abi.json'
|
||||
import AggregatorABI from '@/abis/Aggregator.abi.json'
|
||||
|
||||
const { numberToHex, toWei, fromWei, toBN, hexToNumber, hexToNumberString } = require('web3-utils')
|
||||
|
||||
const state = () => {
|
||||
return {
|
||||
approvalAmount: 'unlimited',
|
||||
lockedBalance: '0',
|
||||
isFetchingLockedBalance: false,
|
||||
currentDelegate: '0x0000000000000000000000000000000000000000',
|
||||
timestamp: 0,
|
||||
delegatedBalance: '0',
|
||||
isFetchingDelegatedBalance: false,
|
||||
delegators: [],
|
||||
latestProposalId: {
|
||||
value: null,
|
||||
status: null
|
||||
},
|
||||
isFetchingProposals: true,
|
||||
proposals: [],
|
||||
voterReceipts: [],
|
||||
hasActiveProposals: false,
|
||||
constants: {
|
||||
EXECUTION_DELAY: 172800,
|
||||
EXECUTION_EXPIRATION: 259200,
|
||||
PROPOSAL_THRESHOLD: '1000000000000000000000',
|
||||
QUORUM_VOTES: '25000000000000000000000',
|
||||
VOTING_PERIOD: 432000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getters = {
|
||||
govContract: (state, getters, rootState) => ({ netId }) => {
|
||||
const config = networkConfig[`netId${netId}`]
|
||||
const { url } = rootState.settings[`netId${netId}`].rpc
|
||||
const address = config['governance.contract.tornadocash.eth']
|
||||
if (address) {
|
||||
const web3 = new Web3(url)
|
||||
return new web3.eth.Contract(ERC20ABI, address)
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
aggregatorContract: (state, getters, rootState, rootGetters) => {
|
||||
const { aggregatorContract } = rootGetters['metamask/networkConfig']
|
||||
const { url } = rootGetters['settings/currentRpc']
|
||||
const web3 = new Web3(url)
|
||||
return new web3.eth.Contract(AggregatorABI, aggregatorContract)
|
||||
},
|
||||
isFetchingProposals: ({ proposals, isFetchingProposals }) => {
|
||||
if (proposals && proposals.length && !isFetchingProposals) {
|
||||
return false
|
||||
}
|
||||
|
||||
return isFetchingProposals
|
||||
},
|
||||
votingPower: (state) => {
|
||||
return toBN(state.lockedBalance)
|
||||
.add(toBN(state.delegatedBalance))
|
||||
.toString(10)
|
||||
},
|
||||
quorumVotes: (state, getters, rootState, rootGetters) => {
|
||||
return rootGetters['token/toDecimals'](state.constants.QUORUM_VOTES, 18)
|
||||
},
|
||||
votingPeriod: (state) => {
|
||||
return toBN(state.constants.VOTING_PERIOD)
|
||||
.divRound(toBN(24 * 60 * 60))
|
||||
.toNumber()
|
||||
},
|
||||
isEnabledGovernance: (state, getters, rootState, rootGetters) => {
|
||||
return Boolean(rootGetters['metamask/networkConfig']['governance.contract.tornadocash.eth'])
|
||||
},
|
||||
constants: (state) => {
|
||||
return state.constants
|
||||
},
|
||||
isFetchingBalances: (state) => {
|
||||
return state.isFetchingLockedBalance || state.isFetchingDelegatedBalance
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
SET_APPROVAL_AMOUNT(state, { approvalAmount }) {
|
||||
state.approvalAmount = approvalAmount
|
||||
},
|
||||
SAVE_FETCHING_PROPOSALS(state, status) {
|
||||
this._vm.$set(state, 'isFetchingProposals', status)
|
||||
},
|
||||
SAVE_LOCKED_BALANCE(state, { balance }) {
|
||||
this._vm.$set(state, 'lockedBalance', balance)
|
||||
},
|
||||
SAVE_FETCHING_LOCKED_BALANCE(state, status) {
|
||||
this._vm.$set(state, 'isFetchingLockedBalance', status)
|
||||
},
|
||||
SAVE_LOCKED_TIMESTAMP(state, { timestamp }) {
|
||||
this._vm.$set(state, 'timestamp', timestamp)
|
||||
},
|
||||
SAVE_LATEST_PROPOSAL_ID(state, { id, status }) {
|
||||
this._vm.$set(state, 'latestProposalId', { value: id, status })
|
||||
},
|
||||
SAVE_DELEGATEE(state, { currentDelegate }) {
|
||||
this._vm.$set(state, 'currentDelegate', currentDelegate)
|
||||
},
|
||||
SAVE_PROPOSALS(state, proposals) {
|
||||
this._vm.$set(state, 'proposals', proposals)
|
||||
},
|
||||
SAVE_VOTER_RECEIPT(state, { hasVoted, support, balance, id }) {
|
||||
this._vm.$set(state.voterReceipts, id, { hasVoted, support, balance })
|
||||
},
|
||||
SAVE_DELEGATED_BALANCE(state, balance) {
|
||||
this._vm.$set(state, 'delegatedBalance', balance)
|
||||
},
|
||||
SAVE_FETCHING_DELEGATED_BALANCE(state, status) {
|
||||
this._vm.$set(state, 'isFetchingDelegatedBalance', status)
|
||||
},
|
||||
SAVE_DELEGATORS(state, uniq) {
|
||||
this._vm.$set(state, 'delegators', uniq)
|
||||
},
|
||||
SET_HAS_ACTIVE_PROPOSALS(state, condition) {
|
||||
this._vm.$set(state, 'hasActiveProposals', condition)
|
||||
},
|
||||
SAVE_CONSTANTS(state, constants) {
|
||||
this._vm.$set(state, 'constants', constants)
|
||||
}
|
||||
}
|
||||
|
||||
// enum ProposalState { Pending, Active, Defeated, Timelocked, AwaitingExecution, Executed, Expired }
|
||||
|
||||
const ProposalState = [
|
||||
'pending',
|
||||
'active',
|
||||
'defeated',
|
||||
'timeLocked',
|
||||
'awaitingExecution',
|
||||
'executed',
|
||||
'expired'
|
||||
]
|
||||
|
||||
const proposalIntervalConstants = [
|
||||
// 'CLOSING_PERIOD',
|
||||
'EXECUTION_DELAY',
|
||||
'EXECUTION_EXPIRATION',
|
||||
// 'VOTE_EXTEND_TIME',
|
||||
// 'VOTING_DELAY',
|
||||
'VOTING_PERIOD'
|
||||
]
|
||||
const govConstants = ['PROPOSAL_THRESHOLD', 'QUORUM_VOTES']
|
||||
|
||||
const actions = {
|
||||
async createProposal(
|
||||
{ getters, rootGetters, state, commit, rootState, dispatch },
|
||||
{ proposalAddress, title, description }
|
||||
) {
|
||||
try {
|
||||
const { ethAccount } = rootState.metamask
|
||||
const netId = rootGetters['metamask/netId']
|
||||
|
||||
const govInstance = getters.govContract({ netId })
|
||||
const json = JSON.stringify({ title, description })
|
||||
const data = await govInstance.methods.propose(proposalAddress, json).encodeABI()
|
||||
|
||||
const gas = await govInstance.methods
|
||||
.propose(proposalAddress, json)
|
||||
.estimateGas({ from: ethAccount, value: 0 })
|
||||
|
||||
const callParams = {
|
||||
method: 'eth_sendTransaction',
|
||||
params: {
|
||||
to: govInstance._address,
|
||||
gas: numberToHex(gas + 100000),
|
||||
data
|
||||
},
|
||||
watcherParams: {
|
||||
title: 'creatingProposal',
|
||||
successTitle: 'proposalCreated',
|
||||
storeType: 'govTxs',
|
||||
onSuccess: () => {
|
||||
dispatch('torn/fetchTokenBalance', {}, { root: true })
|
||||
}
|
||||
},
|
||||
isSaving: false
|
||||
}
|
||||
|
||||
const txHash = await dispatch('metamask/sendTransaction', callParams, { root: true })
|
||||
|
||||
commit(
|
||||
'txHashKeeper/SAVE_TX_HASH',
|
||||
{
|
||||
txHash,
|
||||
storeType: 'govTxs',
|
||||
type: 'Proposal',
|
||||
netId
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
|
||||
await dispatch('fetchProposals', { requestId: state.proposals.length })
|
||||
|
||||
this.$router.push('/governance')
|
||||
} catch (e) {
|
||||
console.error('createProposal', e.message)
|
||||
dispatch(
|
||||
'notice/addNoticeWithInterval',
|
||||
{
|
||||
notice: {
|
||||
title: 'internalError',
|
||||
type: 'danger'
|
||||
},
|
||||
interval: 3000
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
}
|
||||
},
|
||||
async lock({ getters, rootGetters, commit, rootState, dispatch }) {
|
||||
try {
|
||||
const { ethAccount } = rootState.metamask
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const { deadline, v, r, s } = rootState.torn.signature
|
||||
const govInstance = getters.govContract({ netId })
|
||||
const amount = toWei(rootState.torn.signature.amount.toString())
|
||||
const gas = await govInstance.methods
|
||||
.lock(ethAccount, amount, deadline, v, r, s)
|
||||
.estimateGas({ from: ethAccount, value: 0 })
|
||||
const data = await govInstance.methods.lock(ethAccount, amount, deadline, v, r, s).encodeABI()
|
||||
|
||||
const callParams = {
|
||||
method: 'eth_sendTransaction',
|
||||
params: {
|
||||
to: govInstance._address,
|
||||
gas: numberToHex(gas + 30000),
|
||||
data
|
||||
},
|
||||
watcherParams: {
|
||||
title: 'locking',
|
||||
successTitle: 'lockedNotice',
|
||||
storeType: 'govTxs',
|
||||
onSuccess: () => {
|
||||
dispatch('fetchBalances')
|
||||
dispatch('torn/fetchTokenBalance', {}, { root: true })
|
||||
commit('torn/REMOVE_SIGNATURE', {}, { root: true })
|
||||
}
|
||||
},
|
||||
isAwait: false
|
||||
}
|
||||
|
||||
const txHash = await dispatch('metamask/sendTransaction', callParams, { root: true })
|
||||
|
||||
commit(
|
||||
'txHashKeeper/SAVE_TX_HASH',
|
||||
{
|
||||
txHash,
|
||||
storeType: 'govTxs',
|
||||
type: 'Lock',
|
||||
netId
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('lock', e.message)
|
||||
dispatch(
|
||||
'notice/addNoticeWithInterval',
|
||||
{
|
||||
notice: {
|
||||
title: 'internalError',
|
||||
type: 'danger'
|
||||
},
|
||||
interval: 3000
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
}
|
||||
},
|
||||
async lockWithApproval({ getters, rootGetters, commit, rootState, dispatch }, { amount }) {
|
||||
try {
|
||||
const { ethAccount } = rootState.metamask
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const govInstance = getters.govContract({ netId })
|
||||
amount = toWei(amount.toString())
|
||||
const gas = await govInstance.methods
|
||||
.lockWithApproval(amount)
|
||||
.estimateGas({ from: ethAccount, value: 0 })
|
||||
const data = await govInstance.methods.lockWithApproval(amount).encodeABI()
|
||||
|
||||
const callParams = {
|
||||
method: 'eth_sendTransaction',
|
||||
params: {
|
||||
to: govInstance._address,
|
||||
gas: numberToHex(gas + 100000),
|
||||
data
|
||||
},
|
||||
watcherParams: {
|
||||
title: 'locking',
|
||||
successTitle: 'lockedNotice',
|
||||
storeType: 'govTxs',
|
||||
onSuccess: () => {
|
||||
dispatch('fetchBalances')
|
||||
dispatch('torn/fetchTokenBalance', {}, { root: true })
|
||||
dispatch('torn/fetchTokenAllowance', {}, { root: true })
|
||||
}
|
||||
},
|
||||
isAwait: false
|
||||
}
|
||||
|
||||
const txHash = await dispatch('metamask/sendTransaction', callParams, { root: true })
|
||||
|
||||
commit(
|
||||
'txHashKeeper/SAVE_TX_HASH',
|
||||
{
|
||||
txHash,
|
||||
storeType: 'govTxs',
|
||||
type: 'Lock',
|
||||
netId
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('lockWithApproval', e.message)
|
||||
Toast.open({
|
||||
message: this.app.i18n.t('internalError'),
|
||||
type: 'is-danger',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
},
|
||||
async castVote({ getters, rootGetters, commit, rootState, dispatch, state }, { id, support }) {
|
||||
try {
|
||||
const { ethAccount } = rootState.metamask
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const govInstance = getters.govContract({ netId })
|
||||
const delegators = [...state.delegators]
|
||||
|
||||
if (toBN(state.lockedBalance).gt(toBN('0'))) {
|
||||
delegators.push(ethAccount)
|
||||
}
|
||||
|
||||
const gas = await govInstance.methods
|
||||
.castDelegatedVote(delegators, id, support)
|
||||
.estimateGas({ from: ethAccount, value: 0 })
|
||||
const data = await govInstance.methods.castDelegatedVote(delegators, id, support).encodeABI()
|
||||
|
||||
const callParams = {
|
||||
method: 'eth_sendTransaction',
|
||||
params: {
|
||||
to: govInstance._address,
|
||||
gas: numberToHex(gas + 30000),
|
||||
data
|
||||
},
|
||||
watcherParams: {
|
||||
title: support ? 'votingFor' : 'votingAgainst',
|
||||
successTitle: support ? 'votedFor' : 'votedAgainst',
|
||||
storeType: 'govTxs',
|
||||
onSuccess: () => {
|
||||
dispatch('fetchProposals', { requestId: id })
|
||||
}
|
||||
},
|
||||
isAwait: false
|
||||
}
|
||||
|
||||
const txHash = await dispatch('metamask/sendTransaction', callParams, { root: true })
|
||||
|
||||
commit(
|
||||
'txHashKeeper/SAVE_TX_HASH',
|
||||
{
|
||||
txHash,
|
||||
storeType: 'govTxs',
|
||||
type: 'CastVote',
|
||||
netId
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('castVote', e.message)
|
||||
dispatch(
|
||||
'notice/addNoticeWithInterval',
|
||||
{
|
||||
notice: {
|
||||
title: 'internalError',
|
||||
type: 'danger'
|
||||
},
|
||||
interval: 3000
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
} finally {
|
||||
dispatch('loading/disable', {}, { root: true })
|
||||
}
|
||||
},
|
||||
async executeProposal({ getters, rootGetters, commit, rootState, dispatch }, { id }) {
|
||||
try {
|
||||
const { ethAccount } = rootState.metamask
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const govInstance = getters.govContract({ netId })
|
||||
const gas = await govInstance.methods.execute(id).estimateGas({ from: ethAccount, value: 0 })
|
||||
const data = await govInstance.methods.execute(id).encodeABI()
|
||||
|
||||
const callParams = {
|
||||
method: 'eth_sendTransaction',
|
||||
params: {
|
||||
to: govInstance._address,
|
||||
gas: numberToHex(gas + 100000),
|
||||
data
|
||||
},
|
||||
watcherParams: {
|
||||
title: 'executingProposal',
|
||||
successTitle: 'proposalExecuted',
|
||||
storeType: 'govTxs',
|
||||
onSuccess: () => {
|
||||
dispatch('fetchProposals', { requestId: id })
|
||||
}
|
||||
},
|
||||
isAwait: false
|
||||
}
|
||||
|
||||
const txHash = await dispatch('metamask/sendTransaction', callParams, { root: true })
|
||||
|
||||
commit(
|
||||
'txHashKeeper/SAVE_TX_HASH',
|
||||
{
|
||||
txHash,
|
||||
storeType: 'govTxs',
|
||||
type: 'ExecuteProposal',
|
||||
netId
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('executeProposal', e.message)
|
||||
dispatch(
|
||||
'notice/addNoticeWithInterval',
|
||||
{
|
||||
notice: {
|
||||
title: 'internalError',
|
||||
type: 'danger'
|
||||
},
|
||||
interval: 3000
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
}
|
||||
},
|
||||
async unlock({ getters, rootGetters, commit, rootState, dispatch }, { amount }) {
|
||||
try {
|
||||
const { ethAccount } = rootState.metamask
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const govInstance = getters.govContract({ netId })
|
||||
amount = toWei(amount.toString())
|
||||
const gas = await govInstance.methods.unlock(amount).estimateGas({ from: ethAccount, value: 0 })
|
||||
const data = await govInstance.methods.unlock(amount).encodeABI()
|
||||
|
||||
const callParams = {
|
||||
method: 'eth_sendTransaction',
|
||||
params: {
|
||||
to: govInstance._address,
|
||||
gas: numberToHex(gas + 100000),
|
||||
data
|
||||
},
|
||||
watcherParams: {
|
||||
title: 'unlocking',
|
||||
successTitle: 'unlocked',
|
||||
storeType: 'govTxs',
|
||||
onSuccess: () => {
|
||||
dispatch('fetchBalances')
|
||||
dispatch('torn/fetchTokenBalance', {}, { root: true })
|
||||
commit('torn/REMOVE_SIGNATURE', {}, { root: true })
|
||||
}
|
||||
},
|
||||
isAwait: false
|
||||
}
|
||||
|
||||
const txHash = await dispatch('metamask/sendTransaction', callParams, { root: true })
|
||||
|
||||
commit(
|
||||
'txHashKeeper/SAVE_TX_HASH',
|
||||
{
|
||||
txHash,
|
||||
storeType: 'govTxs',
|
||||
type: 'Unlock',
|
||||
netId
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('unlock', e.message)
|
||||
dispatch(
|
||||
'notice/addNoticeWithInterval',
|
||||
{
|
||||
notice: {
|
||||
title: 'internalError',
|
||||
type: 'danger'
|
||||
},
|
||||
interval: 3000
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
}
|
||||
},
|
||||
async delegate({ getters, rootGetters, commit, rootState, dispatch }, { delegatee }) {
|
||||
try {
|
||||
const { ethAccount } = rootState.metamask
|
||||
const netId = rootGetters['metamask/netId']
|
||||
|
||||
const govInstance = getters.govContract({ netId })
|
||||
const gas = await govInstance.methods.delegate(delegatee).estimateGas({ from: ethAccount, value: 0 })
|
||||
const data = await govInstance.methods.delegate(delegatee).encodeABI()
|
||||
|
||||
const callParams = {
|
||||
method: 'eth_sendTransaction',
|
||||
params: {
|
||||
to: govInstance._address,
|
||||
gas: numberToHex(gas + 100000),
|
||||
data
|
||||
},
|
||||
watcherParams: {
|
||||
title: 'delegating',
|
||||
successTitle: 'delegated',
|
||||
storeType: 'govTxs',
|
||||
onSuccess: () => {
|
||||
dispatch('fetchDelegatee')
|
||||
}
|
||||
},
|
||||
isAwait: false
|
||||
}
|
||||
|
||||
const txHash = await dispatch('metamask/sendTransaction', callParams, { root: true })
|
||||
|
||||
commit(
|
||||
'txHashKeeper/SAVE_TX_HASH',
|
||||
{
|
||||
storeType: 'govTxs',
|
||||
txHash,
|
||||
type: 'Delegate',
|
||||
netId
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('delegate', e.message)
|
||||
dispatch(
|
||||
'notice/addNoticeWithInterval',
|
||||
{
|
||||
notice: {
|
||||
title: 'internalError',
|
||||
type: 'danger'
|
||||
},
|
||||
interval: 3000
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
}
|
||||
},
|
||||
async undelegate({ getters, rootGetters, commit, rootState, dispatch }) {
|
||||
try {
|
||||
const { ethAccount } = rootState.metamask
|
||||
const netId = rootGetters['metamask/netId']
|
||||
|
||||
const govInstance = getters.govContract({ netId })
|
||||
const gas = await govInstance.methods.undelegate().estimateGas({ from: ethAccount, value: 0 })
|
||||
const data = await govInstance.methods.undelegate().encodeABI()
|
||||
|
||||
const callParams = {
|
||||
method: 'eth_sendTransaction',
|
||||
params: {
|
||||
to: govInstance._address,
|
||||
gas: numberToHex(gas + 100000),
|
||||
data
|
||||
},
|
||||
watcherParams: {
|
||||
title: 'undelegating',
|
||||
successTitle: 'undelegated',
|
||||
storeType: 'govTxs',
|
||||
onSuccess: () => {
|
||||
dispatch('fetchDelegatee')
|
||||
}
|
||||
},
|
||||
isAwait: false
|
||||
}
|
||||
|
||||
const txHash = await dispatch('metamask/sendTransaction', callParams, { root: true })
|
||||
|
||||
commit(
|
||||
'txHashKeeper/SAVE_TX_HASH',
|
||||
{
|
||||
txHash,
|
||||
storeType: 'govTxs',
|
||||
type: 'Undelegate',
|
||||
netId
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('undelegate', e.message)
|
||||
dispatch(
|
||||
'notice/addNoticeWithInterval',
|
||||
{
|
||||
notice: {
|
||||
title: 'internalError',
|
||||
type: 'danger'
|
||||
},
|
||||
interval: 3000
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
}
|
||||
},
|
||||
async fetchProposals({ rootGetters, getters, commit }, { requestId }) {
|
||||
let proposals
|
||||
try {
|
||||
commit('SAVE_FETCHING_PROPOSALS', true)
|
||||
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const aggregatorContract = getters.aggregatorContract
|
||||
const govInstance = getters.govContract({ netId })
|
||||
|
||||
if (!govInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
proposals = await govInstance.getPastEvents('ProposalCreated', {
|
||||
fromBlock: 0,
|
||||
toBlock: 'latest'
|
||||
})
|
||||
let title, description, rest
|
||||
proposals = proposals.map((e) => {
|
||||
try {
|
||||
;({ title, description } = JSON.parse(e.returnValues.description))
|
||||
} catch (err) {
|
||||
;[title, ...rest] = e.returnValues.description.split('\n', 2)
|
||||
description = rest.join('\n')
|
||||
}
|
||||
|
||||
const netId = rootGetters['metamask/netId']
|
||||
if (netId === 1) {
|
||||
switch (Number(e.returnValues.id)) {
|
||||
case 1:
|
||||
description = 'See: https://torn.community/t/proposal-1-enable-torn-transfers/38'
|
||||
break
|
||||
case 10:
|
||||
;({ title, description } = JSON.parse(e.returnValues.description.replaceAll('\n', '')))
|
||||
break
|
||||
case 11:
|
||||
;({ title, description } = JSON.parse(
|
||||
e.returnValues.description.replace('"description"', ',"description"')
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: Number(e.returnValues.id),
|
||||
title,
|
||||
description,
|
||||
endTime: Number(e.returnValues.endTime),
|
||||
startTime: Number(e.returnValues.startTime),
|
||||
proposer: e.returnValues.proposer,
|
||||
target: e.returnValues.target,
|
||||
results: {
|
||||
for: '0',
|
||||
against: '0'
|
||||
}
|
||||
}
|
||||
})
|
||||
const statuses = await aggregatorContract.methods.getAllProposals(govInstance._address).call()
|
||||
proposals = proposals
|
||||
.map((p) => {
|
||||
const proposal = statuses[Number(p.id) - 1]
|
||||
|
||||
if (proposal.extended) {
|
||||
p.endTime = Number(proposal.endTime)
|
||||
}
|
||||
|
||||
p.status = ProposalState[Number(proposal.state)]
|
||||
p.results = {
|
||||
for: fromWei(proposal.forVotes),
|
||||
against: fromWei(proposal.againstVotes)
|
||||
}
|
||||
return p
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.id - b.id
|
||||
})
|
||||
if (requestId) {
|
||||
return proposals[requestId]
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('fetchProposals', e.message)
|
||||
} finally {
|
||||
commit('SAVE_PROPOSALS', proposals)
|
||||
commit('SAVE_FETCHING_PROPOSALS', false)
|
||||
}
|
||||
},
|
||||
async fetchBalances({ getters, rootGetters, commit, rootState }) {
|
||||
try {
|
||||
commit('SAVE_FETCHING_LOCKED_BALANCE', true)
|
||||
const { ethAccount } = rootState.metamask
|
||||
|
||||
if (!ethAccount) {
|
||||
return
|
||||
}
|
||||
|
||||
const netId = rootGetters['metamask/netId']
|
||||
|
||||
const govInstance = getters.govContract({ netId })
|
||||
const balance = await govInstance.methods.lockedBalance(ethAccount).call()
|
||||
commit('SAVE_LOCKED_BALANCE', { balance })
|
||||
} catch (e) {
|
||||
console.error('fetchBalances', e.message)
|
||||
} finally {
|
||||
commit('SAVE_FETCHING_LOCKED_BALANCE', false)
|
||||
}
|
||||
},
|
||||
async fetchedLockedTimestamp({ getters, rootGetters, commit, rootState, dispatch }) {
|
||||
try {
|
||||
const { ethAccount } = rootState.metamask
|
||||
|
||||
if (!ethAccount) {
|
||||
return
|
||||
}
|
||||
|
||||
const netId = rootGetters['metamask/netId']
|
||||
|
||||
const govInstance = getters.govContract({ netId })
|
||||
const timestamp = await govInstance.methods.canWithdrawAfter(ethAccount).call()
|
||||
commit('SAVE_LOCKED_TIMESTAMP', { timestamp })
|
||||
} catch (e) {
|
||||
console.error('fetchedLockedTimestamp', e.message)
|
||||
}
|
||||
},
|
||||
async fetchLatestProposalId({ getters, rootGetters, commit, rootState, state }) {
|
||||
try {
|
||||
const { ethAccount } = rootState.metamask
|
||||
|
||||
if (!ethAccount) {
|
||||
return
|
||||
}
|
||||
|
||||
const netId = rootGetters['metamask/netId']
|
||||
|
||||
const govInstance = getters.govContract({ netId })
|
||||
const id = await govInstance.methods.latestProposalIds(ethAccount).call()
|
||||
let status = null
|
||||
if (Number(id)) {
|
||||
status = await govInstance.methods.state(id).call()
|
||||
status = Number(status) > 1 ? null : 'active'
|
||||
}
|
||||
commit('SAVE_LATEST_PROPOSAL_ID', { id, status })
|
||||
console.log('status', state.latestProposalId)
|
||||
} catch (e) {
|
||||
console.error('fetchLatestProposalId', e.message)
|
||||
}
|
||||
},
|
||||
async fetchDelegatedBalance({ getters, rootGetters, commit, rootState, dispatch }) {
|
||||
try {
|
||||
commit('SAVE_FETCHING_DELEGATED_BALANCE', true)
|
||||
const { ethAccount } = rootState.metamask
|
||||
|
||||
if (!ethAccount) {
|
||||
return
|
||||
}
|
||||
|
||||
const netId = rootGetters['metamask/netId']
|
||||
|
||||
const aggregatorContract = getters.aggregatorContract
|
||||
const govInstance = getters.govContract({ netId })
|
||||
let delegatedAccs = await govInstance.getPastEvents('Delegated', {
|
||||
filter: {
|
||||
to: ethAccount
|
||||
},
|
||||
fromBlock: 0,
|
||||
toBlock: 'latest'
|
||||
})
|
||||
let undelegatedAccs = await govInstance.getPastEvents('Undelegated', {
|
||||
filter: {
|
||||
from: ethAccount
|
||||
},
|
||||
fromBlock: 0,
|
||||
toBlock: 'latest'
|
||||
})
|
||||
delegatedAccs = delegatedAccs.map((acc) => acc.returnValues.account)
|
||||
undelegatedAccs = undelegatedAccs.map((acc) => acc.returnValues.account)
|
||||
const uniq = delegatedAccs.filter((obj, index, self) => {
|
||||
const indexUndelegated = undelegatedAccs.indexOf(obj)
|
||||
if (indexUndelegated !== -1) {
|
||||
undelegatedAccs.splice(indexUndelegated, 1)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
let balances = await aggregatorContract.methods.getGovernanceBalances(govInstance._address, uniq).call()
|
||||
balances = balances.reduce((acc, balance, i) => {
|
||||
acc = acc.add(toBN(balance))
|
||||
return acc
|
||||
}, toBN('0'))
|
||||
commit('SAVE_DELEGATED_BALANCE', balances.toString(10))
|
||||
commit('SAVE_DELEGATORS', uniq)
|
||||
} catch (e) {
|
||||
console.error('fetchDelegatedBalance', e.message)
|
||||
} finally {
|
||||
commit('SAVE_FETCHING_DELEGATED_BALANCE', false)
|
||||
}
|
||||
},
|
||||
async fetchDelegatee({ getters, rootGetters, commit, rootState, dispatch }) {
|
||||
try {
|
||||
const { ethAccount } = rootState.metamask
|
||||
|
||||
if (!ethAccount) {
|
||||
return
|
||||
}
|
||||
|
||||
const netId = rootGetters['metamask/netId']
|
||||
|
||||
const govInstance = getters.govContract({ netId })
|
||||
const currentDelegate = await govInstance.methods.delegatedTo(ethAccount).call()
|
||||
console.log('currentDelegate', currentDelegate)
|
||||
commit('SAVE_DELEGATEE', { currentDelegate })
|
||||
} catch (e) {
|
||||
console.error('fetchDelegatee', e.message)
|
||||
}
|
||||
},
|
||||
async fetchReceipt({ getters, rootGetters, commit, rootState, dispatch }, { id }) {
|
||||
try {
|
||||
const { ethAccount } = rootState.metamask
|
||||
const netId = rootGetters['metamask/netId']
|
||||
console.log('fetchReceipt', id)
|
||||
const govInstance = getters.govContract({ netId })
|
||||
const [hasVoted, support, balance] = await govInstance.methods.getReceipt(id, ethAccount).call()
|
||||
console.log('fetchReceipt', hasVoted, support, balance)
|
||||
commit('SAVE_VOTER_RECEIPT', { hasVoted, support, balance, id })
|
||||
} catch (e) {
|
||||
console.error('fetchReceipt', e.message)
|
||||
}
|
||||
},
|
||||
async fetchUserData({ getters, rootGetters, commit, rootState, dispatch }) {
|
||||
try {
|
||||
commit('SAVE_FETCHING_LOCKED_BALANCE', true)
|
||||
const { ethAccount } = rootState.metamask
|
||||
|
||||
if (!ethAccount) {
|
||||
return
|
||||
}
|
||||
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const govInstance = getters.govContract({ netId })
|
||||
const aggregatorContract = getters.aggregatorContract
|
||||
const {
|
||||
balance,
|
||||
latestProposalId,
|
||||
timelock,
|
||||
delegatee,
|
||||
...userdata
|
||||
} = await aggregatorContract.methods.getUserData(govInstance._address, ethAccount).call()
|
||||
commit('SAVE_DELEGATEE', { currentDelegate: delegatee })
|
||||
|
||||
const latestProposalIdState = ProposalState[Number(userdata.latestProposalIdState)]
|
||||
commit('SAVE_LATEST_PROPOSAL_ID', { id: Number(latestProposalId), status: latestProposalIdState })
|
||||
commit('SAVE_LOCKED_TIMESTAMP', { timestamp: Number(timelock) })
|
||||
commit('SAVE_LOCKED_BALANCE', { balance })
|
||||
} catch (e) {
|
||||
console.error('fetchUserData', e.message)
|
||||
} finally {
|
||||
commit('SAVE_FETCHING_LOCKED_BALANCE', false)
|
||||
}
|
||||
},
|
||||
async checkActiveProposals({ getters, rootGetters, commit }) {
|
||||
if (!getters.isEnabledGovernance) {
|
||||
return
|
||||
}
|
||||
|
||||
const { 'governance.contract.tornadocash.eth': governanceAddress } = rootGetters['metamask/networkConfig']
|
||||
const aggregatorContract = getters.aggregatorContract
|
||||
|
||||
const statuses = await aggregatorContract.methods.getAllProposals(governanceAddress).call()
|
||||
let isActive = false
|
||||
if (statuses && Array.isArray(statuses)) {
|
||||
isActive = statuses.find((status) => Number(status.state) === 1)
|
||||
}
|
||||
|
||||
commit('SET_HAS_ACTIVE_PROPOSALS', Boolean(isActive))
|
||||
},
|
||||
async fetchConstants({ commit, getters, dispatch, rootGetters }) {
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const govInstance = getters.govContract({ netId })
|
||||
const constants = [].concat(govConstants, proposalIntervalConstants)
|
||||
|
||||
const params = constants.map((name) => {
|
||||
return {
|
||||
target: govInstance._address,
|
||||
callData: govInstance.methods[name]().encodeABI()
|
||||
}
|
||||
})
|
||||
|
||||
const multicallArray = await dispatch('application/aggregateMulticall', { params }, { root: true })
|
||||
|
||||
if (multicallArray) {
|
||||
const hexToNumberConverter = (acc, curr, index) => {
|
||||
const name = constants[index]
|
||||
const value = govConstants.includes(name) ? hexToNumberString(curr) : hexToNumber(curr)
|
||||
|
||||
return { ...acc, [name]: value }
|
||||
}
|
||||
|
||||
const result = multicallArray.reduce(hexToNumberConverter, {})
|
||||
|
||||
commit('SAVE_CONSTANTS', result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions
|
||||
}
|
137
store/governance/staking.js
Normal file
137
store/governance/staking.js
Normal file
|
@ -0,0 +1,137 @@
|
|||
import Web3 from 'web3'
|
||||
import { numberToHex, fromWei } from 'web3-utils'
|
||||
|
||||
import networkConfig from '@/networkConfig'
|
||||
|
||||
import TornadoStakingRewardsABI from '@/abis/TornadoStakingRewards.abi.json'
|
||||
|
||||
export const state = () => {
|
||||
return {
|
||||
accumulatedReward: '0',
|
||||
isCheckingReward: false
|
||||
}
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
stakingRewardsContract: (state, getters, rootState) => ({ netId }) => {
|
||||
const config = networkConfig[`netId${netId}`]
|
||||
const { url } = rootState.settings[`netId${netId}`].rpc
|
||||
const address = config['staking-rewards.contract.tornadocash.eth']
|
||||
|
||||
if (address) {
|
||||
const web3 = new Web3(url)
|
||||
return new web3.eth.Contract(TornadoStakingRewardsABI, address)
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
reward: (state) => {
|
||||
return fromWei(state.accumulatedReward)
|
||||
},
|
||||
isCheckingReward: (state) => {
|
||||
return state.isCheckingReward
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
SAVE_ACCUMULATED_REWARD(state, payload) {
|
||||
state.accumulatedReward = payload
|
||||
},
|
||||
SAVE_CHECKING_REWARD(state, payload) {
|
||||
this._vm.$set(state, 'isCheckingReward', payload)
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
async checkReward({ getters, rootGetters, rootState, commit }) {
|
||||
try {
|
||||
commit('SAVE_CHECKING_REWARD', true)
|
||||
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const { ethAccount } = rootState.metamask
|
||||
const stakingRewardsInstance = getters.stakingRewardsContract({ netId })
|
||||
|
||||
if (!stakingRewardsInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
const reward = await stakingRewardsInstance.methods.checkReward(ethAccount).call()
|
||||
|
||||
commit('SAVE_ACCUMULATED_REWARD', reward)
|
||||
} catch (err) {
|
||||
console.error('checkReward', err.message)
|
||||
} finally {
|
||||
commit('SAVE_CHECKING_REWARD', false)
|
||||
}
|
||||
},
|
||||
async claimReward({ state, getters, rootGetters, rootState, commit, dispatch }) {
|
||||
try {
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const { ethAccount } = rootState.metamask
|
||||
const stakingRewardsInstance = getters.stakingRewardsContract({ netId })
|
||||
|
||||
if (!stakingRewardsInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
const data = await stakingRewardsInstance.methods.getReward().encodeABI()
|
||||
const gas = await stakingRewardsInstance.methods.getReward().estimateGas({ from: ethAccount, value: 0 })
|
||||
|
||||
const currency = 'TORN'
|
||||
const amount = rootGetters['token/toDecimals'](state.accumulatedReward, 18)
|
||||
|
||||
const callParams = {
|
||||
method: 'eth_sendTransaction',
|
||||
params: {
|
||||
to: stakingRewardsInstance._address,
|
||||
gas: numberToHex(gas + 100000),
|
||||
data
|
||||
},
|
||||
watcherParams: {
|
||||
title: {
|
||||
path: 'claiming',
|
||||
amount,
|
||||
currency
|
||||
},
|
||||
successTitle: {
|
||||
path: 'claimedValue',
|
||||
amount,
|
||||
currency
|
||||
},
|
||||
storeType: 'govTxs',
|
||||
onSuccess: () => {
|
||||
dispatch('torn/fetchTokenBalance', {}, { root: true })
|
||||
dispatch('checkReward')
|
||||
}
|
||||
},
|
||||
isSaving: false
|
||||
}
|
||||
|
||||
const txHash = await dispatch('metamask/sendTransaction', callParams, { root: true })
|
||||
|
||||
commit(
|
||||
'txHashKeeper/SAVE_TX_HASH',
|
||||
{
|
||||
txHash,
|
||||
storeType: 'govTxs',
|
||||
type: 'Reward',
|
||||
netId
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('claimReward', err.message)
|
||||
dispatch(
|
||||
'notice/addNoticeWithInterval',
|
||||
{
|
||||
notice: {
|
||||
title: 'internalError',
|
||||
type: 'danger'
|
||||
},
|
||||
interval: 3000
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
42
store/loading.js
Normal file
42
store/loading.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
export const state = () => {
|
||||
return {
|
||||
message: '',
|
||||
enabled: false,
|
||||
type: null
|
||||
}
|
||||
}
|
||||
|
||||
export const getters = {}
|
||||
|
||||
export const mutations = {
|
||||
ENABLE(state, { message, type }) {
|
||||
state.message = message
|
||||
state.enabled = true
|
||||
state.type = type
|
||||
},
|
||||
DISABLE(state) {
|
||||
state.message = ''
|
||||
state.enabled = false
|
||||
state.type = null
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
enable({ commit }, { message = this.app.i18n.t('loading') }) {
|
||||
commit('ENABLE', { message })
|
||||
},
|
||||
changeText({ commit }, { message, type }) {
|
||||
commit('ENABLE', { message, type })
|
||||
},
|
||||
disable({ commit }) {
|
||||
commit('DISABLE')
|
||||
},
|
||||
showConfirmLoader({ dispatch, rootState }) {
|
||||
dispatch('changeText', {
|
||||
message: this.app.i18n.t('pleaseConfirmTransactionInWallet', {
|
||||
wallet: rootState.metamask.walletName
|
||||
}),
|
||||
type: 'approve'
|
||||
})
|
||||
}
|
||||
}
|
614
store/metamask.js
Normal file
614
store/metamask.js
Normal file
|
@ -0,0 +1,614 @@
|
|||
/* eslint-disable no-console */
|
||||
import BN from 'bignumber.js'
|
||||
import { hexToNumber, numberToHex } from 'web3-utils'
|
||||
import { SnackbarProgrammatic as Snackbar } from 'buefy'
|
||||
|
||||
import { PROVIDERS } from '@/constants'
|
||||
import networkConfig from '@/networkConfig'
|
||||
import { walletConnectConnector } from '@/services'
|
||||
|
||||
import SanctionsListAbi from '@/abis/SanctionsList.abi'
|
||||
|
||||
const { toChecksumAddress } = require('web3-utils')
|
||||
|
||||
const state = () => {
|
||||
return {
|
||||
netId: 1,
|
||||
walletName: '',
|
||||
ethBalance: '0',
|
||||
ethAccount: null,
|
||||
providerConfig: {},
|
||||
providerName: null,
|
||||
isInitialized: false,
|
||||
isReconnecting: false,
|
||||
mismatchNetwork: false
|
||||
}
|
||||
}
|
||||
|
||||
const getters = {
|
||||
isWalletConnect(state) {
|
||||
return state.providerConfig.name === 'WalletConnect'
|
||||
},
|
||||
isPartialSupport(state) {
|
||||
return state.providerConfig.isPartialSupport
|
||||
},
|
||||
hasEthAccount(state) {
|
||||
return state.ethAccount !== null
|
||||
},
|
||||
mismatchNetwork(state) {
|
||||
return state.mismatchNetwork
|
||||
},
|
||||
netId(state) {
|
||||
return state.netId
|
||||
},
|
||||
networkName(state) {
|
||||
return networkConfig[`netId${state.netId}`].networkName
|
||||
},
|
||||
currency(state) {
|
||||
return networkConfig[`netId${state.netId}`].currencyName
|
||||
},
|
||||
nativeCurrency(state) {
|
||||
return networkConfig[`netId${state.netId}`].nativeCurrency
|
||||
},
|
||||
networkConfig(state) {
|
||||
const conf = networkConfig[`netId${state.netId}`]
|
||||
return conf || networkConfig.netId1
|
||||
},
|
||||
getEthereumProvider: (state, getters) => (netId) => {
|
||||
switch (state.providerName) {
|
||||
case 'walletConnect':
|
||||
return walletConnectConnector(netId || getters.netId)
|
||||
case 'metamask':
|
||||
case 'trustwallet':
|
||||
case 'imtoken':
|
||||
case 'alphawallet':
|
||||
case 'generic':
|
||||
default:
|
||||
if (window.ethereum) {
|
||||
return window.ethereum
|
||||
} else {
|
||||
throw new Error(this.app.i18n.t('networkDoesNotHaveEthereumProperty'))
|
||||
}
|
||||
}
|
||||
},
|
||||
isLoggedIn: (state, getters) => {
|
||||
return !!state.providerName && getters.hasEthAccount
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
IDENTIFY(state, ethAccount) {
|
||||
state.ethAccount = ethAccount
|
||||
},
|
||||
SET_NET_ID(state, netId) {
|
||||
netId = parseInt(netId, 10)
|
||||
state.netId = netId
|
||||
},
|
||||
SET_RECONNECTING(state, bool) {
|
||||
state.isReconnecting = bool
|
||||
},
|
||||
SET_MISMATCH_NETWORK(state, payload) {
|
||||
state.mismatchNetwork = payload
|
||||
},
|
||||
SAVE_BALANCE(state, ethBalance) {
|
||||
state.ethBalance = ethBalance
|
||||
},
|
||||
SET_WALLET_NAME(state, walletName) {
|
||||
state.walletName = walletName
|
||||
},
|
||||
SET_PROVIDER_NAME(state, providerName) {
|
||||
state.providerName = providerName
|
||||
state.providerConfig = PROVIDERS[providerName]
|
||||
window.localStorage.setItem('provider', providerName)
|
||||
},
|
||||
CLEAR_PROVIDER(state) {
|
||||
state.providerName = null
|
||||
state.providerConfig = {}
|
||||
},
|
||||
SET_INITIALIZED(state, initialized) {
|
||||
state.isInitialized = initialized
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
async initialize({ dispatch, commit, getters, rootState, rootGetters }, payload) {
|
||||
await dispatch('askPermission', payload)
|
||||
|
||||
dispatch('governance/gov/checkActiveProposals', {}, { root: true })
|
||||
},
|
||||
onSetInitializeData({ commit, dispatch, state }, isMismatch) {
|
||||
if (isMismatch) {
|
||||
commit('IDENTIFY', null)
|
||||
commit('SET_INITIALIZED', false)
|
||||
} else {
|
||||
const providerName = window.localStorage.getItem('provider')
|
||||
|
||||
if (providerName && !state.isInitialized) {
|
||||
dispatch('initialize', { providerName })
|
||||
}
|
||||
}
|
||||
|
||||
commit('SET_MISMATCH_NETWORK', isMismatch)
|
||||
},
|
||||
async checkMismatchNetwork({ dispatch, commit, state, getters }, netId) {
|
||||
if (getters.isWalletConnect) {
|
||||
const { id } = this.$provider.config
|
||||
|
||||
const isMismatch = Number(netId) !== Number(id)
|
||||
|
||||
await dispatch('onSetInitializeData', isMismatch)
|
||||
return
|
||||
}
|
||||
if (!window.ethereum) {
|
||||
return
|
||||
}
|
||||
const chainId = await window.ethereum.request({ method: 'eth_chainId' })
|
||||
const isMismatch = Number(netId) !== hexToNumber(chainId)
|
||||
|
||||
await dispatch('onSetInitializeData', isMismatch)
|
||||
},
|
||||
async sendTransaction(
|
||||
{ dispatch, state, rootGetters },
|
||||
{ method, params, watcherParams, isAwait = true, isSaving = true, eipDisable = false }
|
||||
) {
|
||||
try {
|
||||
const { ethAccount, netId } = state
|
||||
const gasParams = rootGetters['gasPrices/getGasParams']('fast', eipDisable)
|
||||
|
||||
if (params.gasPrice && 'gasPrice' in gasParams) {
|
||||
gasParams.gasPrice = params.gasPrice.value
|
||||
}
|
||||
|
||||
const callParams = {
|
||||
method,
|
||||
params: [
|
||||
{
|
||||
value: '0x00',
|
||||
from: ethAccount,
|
||||
...params,
|
||||
...gasParams
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
dispatch('loading/showConfirmLoader', {}, { root: true })
|
||||
|
||||
const txHash = await this.$provider.sendRequest(callParams)
|
||||
|
||||
dispatch(
|
||||
'loading/changeText',
|
||||
{ message: this.app.i18n.t('waitUntilTransactionIsMined') },
|
||||
{ root: true }
|
||||
)
|
||||
|
||||
const activeWatcher = () =>
|
||||
dispatch(
|
||||
'txHashKeeper/runTxWatcherWithNotifications',
|
||||
{
|
||||
...watcherParams,
|
||||
txHash,
|
||||
isSaving,
|
||||
netId
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
|
||||
if (isAwait) {
|
||||
await activeWatcher()
|
||||
} else {
|
||||
activeWatcher()
|
||||
}
|
||||
|
||||
dispatch('loading/disable', {}, { root: true })
|
||||
|
||||
return txHash
|
||||
} catch (err) {
|
||||
if (err.message.includes('EIP-1559')) {
|
||||
return await dispatch('sendTransaction', {
|
||||
method,
|
||||
params,
|
||||
watcherParams,
|
||||
isAwait,
|
||||
isSaving,
|
||||
eipDisable: true
|
||||
})
|
||||
} else {
|
||||
throw new Error(this.app.i18n.t('rejectedRequest', { description: state.walletName }))
|
||||
}
|
||||
} finally {
|
||||
dispatch('loading/disable', {}, { root: true })
|
||||
}
|
||||
},
|
||||
async getEncryptionPublicKey({ state }) {
|
||||
try {
|
||||
const { ethAccount } = state
|
||||
|
||||
const callParams = {
|
||||
method: 'eth_getEncryptionPublicKey',
|
||||
params: [ethAccount]
|
||||
}
|
||||
|
||||
const key = await this.$provider.sendRequest(callParams)
|
||||
return key
|
||||
} catch (err) {
|
||||
let errorMessage = 'decryptFailed'
|
||||
|
||||
if (err.message.includes('Trezor')) {
|
||||
errorMessage = 'trezorNotSupported'
|
||||
} else if (err.message.includes('Ledger')) {
|
||||
errorMessage = 'ledgerNotSupported'
|
||||
}
|
||||
|
||||
const isRejected = err.message.includes(
|
||||
'MetaMask EncryptionPublicKey: User denied message EncryptionPublicKey.'
|
||||
)
|
||||
|
||||
if (isRejected) {
|
||||
throw new Error(this.app.i18n.t('rejectedRequest', { description: state.walletName }))
|
||||
}
|
||||
|
||||
throw new Error(this.app.i18n.t(errorMessage))
|
||||
}
|
||||
},
|
||||
async ethDecrypt({ state }, hexData) {
|
||||
try {
|
||||
const { ethAccount } = state
|
||||
|
||||
const callParams = {
|
||||
method: 'eth_decrypt',
|
||||
params: [hexData, ethAccount]
|
||||
}
|
||||
|
||||
const encryptedData = await this.$provider.sendRequest(callParams)
|
||||
return encryptedData
|
||||
} catch (err) {
|
||||
throw new Error(`Method ethDecrypt has error: ${err.message}`)
|
||||
}
|
||||
},
|
||||
async onAccountsChanged({ dispatch, commit }, { newAccount }) {
|
||||
if (newAccount) {
|
||||
const account = toChecksumAddress(newAccount)
|
||||
commit('IDENTIFY', account)
|
||||
await dispatch('updateAccountBalance')
|
||||
} else {
|
||||
await dispatch('onLogOut')
|
||||
}
|
||||
},
|
||||
onLogOut({ commit, getters, dispatch }) {
|
||||
if (getters.isWalletConnect) {
|
||||
const mobileProvider = this.$provider.provider
|
||||
|
||||
if (typeof mobileProvider.close === 'function') {
|
||||
mobileProvider.close()
|
||||
}
|
||||
}
|
||||
commit('IDENTIFY', null)
|
||||
dispatch('clearProvider')
|
||||
commit('SET_INITIALIZED', false)
|
||||
},
|
||||
async mobileWalletReconnect({ state, dispatch, commit, rootState }, { netId }) {
|
||||
try {
|
||||
commit('SET_RECONNECTING', true)
|
||||
const { providerName } = state
|
||||
const { enabled } = rootState.loading
|
||||
|
||||
await dispatch('onLogOut')
|
||||
await dispatch('initialize', { providerName, chosenNetId: netId })
|
||||
|
||||
if (enabled) {
|
||||
await dispatch('loading/disable', {}, { root: true })
|
||||
}
|
||||
} catch ({ message }) {
|
||||
throw new Error(`Mobile wallet reconnect error: ${message}`)
|
||||
} finally {
|
||||
commit('SET_RECONNECTING', false)
|
||||
}
|
||||
},
|
||||
async networkChangeHandler({ state, getters, commit, dispatch }, params) {
|
||||
try {
|
||||
if (getters.isWalletConnect) {
|
||||
await dispatch('mobileWalletReconnect', params)
|
||||
this.$provider._onNetworkChanged({ id: params.netId })
|
||||
} else {
|
||||
if (state.isInitialized) {
|
||||
await dispatch('switchNetwork', params)
|
||||
}
|
||||
await dispatch('onNetworkChanged', params)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('networkChangeHandler', err.message)
|
||||
}
|
||||
},
|
||||
async checkIsSanctioned({ rootGetters }, { address }) {
|
||||
const ethProvider = rootGetters['relayer/ethProvider']
|
||||
const contract = new ethProvider.eth.Contract(
|
||||
SanctionsListAbi,
|
||||
'0x40C57923924B5c5c5455c48D93317139ADDaC8fb'
|
||||
)
|
||||
|
||||
const isSanctioned = await contract.methods.isSanctioned(address).call()
|
||||
|
||||
if (isSanctioned) {
|
||||
window.onbeforeunload = null
|
||||
window.location = 'https://twitter.com/TornadoCash/status/1514904975037669386'
|
||||
}
|
||||
},
|
||||
async onNetworkChanged({ state, getters, commit, dispatch }, { netId }) {
|
||||
dispatch('checkMismatchNetwork', netId)
|
||||
|
||||
if (netId !== 'loading' && Number(state.netId) !== Number(netId)) {
|
||||
try {
|
||||
if (!networkConfig[`netId${netId}`]) {
|
||||
dispatch('clearProvider')
|
||||
|
||||
Snackbar.open({
|
||||
message: this.app.i18n.t('currentNetworkIsNotSupported'),
|
||||
type: 'is-primary',
|
||||
position: 'is-top',
|
||||
actionText: 'OK',
|
||||
indefinite: true
|
||||
})
|
||||
throw new Error(this.app.i18n.t('currentNetworkIsNotSupported'))
|
||||
}
|
||||
|
||||
commit('SET_NET_ID', netId)
|
||||
await dispatch('application/setNativeCurrency', { netId }, { root: true })
|
||||
|
||||
// TODO what if all rpc failed
|
||||
await dispatch('settings/checkCurrentRpc', {}, { root: true })
|
||||
|
||||
dispatch('application/updateSelectEvents', {}, { root: true })
|
||||
|
||||
if (getters.isLoggedIn) {
|
||||
await dispatch('updateAccountBalance')
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e.message)
|
||||
}
|
||||
}
|
||||
},
|
||||
async updateAccountBalance({ state, commit }, account = '') {
|
||||
try {
|
||||
const address = account || state.ethAccount
|
||||
if (!address) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const balance = await this.$provider.getBalance({ address })
|
||||
commit('SAVE_BALANCE', balance)
|
||||
return balance
|
||||
} catch (err) {
|
||||
console.error(`updateAccountBalance has error ${err.message}`)
|
||||
}
|
||||
},
|
||||
clearProvider({ commit, state }) {
|
||||
if (state.providerConfig.storageName) {
|
||||
window.localStorage.removeItem(state.providerConfig.storageName)
|
||||
}
|
||||
|
||||
commit('CLEAR_PROVIDER')
|
||||
|
||||
window.localStorage.removeItem('provider')
|
||||
window.localStorage.removeItem('network')
|
||||
},
|
||||
async askPermission(
|
||||
{ commit, dispatch, getters, rootGetters, state, rootState },
|
||||
{ providerName, chosenNetId }
|
||||
) {
|
||||
commit('SET_PROVIDER_NAME', providerName)
|
||||
|
||||
const { name, listener } = state.providerConfig
|
||||
|
||||
commit('SET_WALLET_NAME', name)
|
||||
|
||||
try {
|
||||
const provider = await getters.getEthereumProvider(chosenNetId)
|
||||
|
||||
if (providerName === 'walletConnect') {
|
||||
await dispatch(listener, { provider })
|
||||
}
|
||||
const address = await this.$provider.initProvider(provider, {})
|
||||
|
||||
if (!address) {
|
||||
throw new Error('lockedMetamask')
|
||||
}
|
||||
|
||||
await dispatch('checkIsSanctioned', { address })
|
||||
|
||||
commit('IDENTIFY', address)
|
||||
|
||||
const netId = await dispatch('checkNetworkVersion')
|
||||
|
||||
await dispatch('onNetworkChanged', { netId })
|
||||
commit('SET_INITIALIZED', true)
|
||||
|
||||
const { url } = rootGetters['settings/currentRpc']
|
||||
this.$provider.initWeb3(url)
|
||||
|
||||
await dispatch('updateAccountBalance', address)
|
||||
|
||||
if (getters.isWalletConnect) {
|
||||
if (provider.wc.peerMeta) {
|
||||
commit('SET_WALLET_NAME', provider.wc.peerMeta.name)
|
||||
}
|
||||
}
|
||||
|
||||
this.$provider.on({
|
||||
method: 'chainChanged',
|
||||
callback: () => {
|
||||
dispatch('onNetworkChanged', { netId })
|
||||
}
|
||||
})
|
||||
|
||||
this.$provider.on({
|
||||
method: 'accountsChanged',
|
||||
callback: ([newAccount]) => {
|
||||
dispatch('onAccountsChanged', { newAccount })
|
||||
}
|
||||
})
|
||||
|
||||
return { netId, ethAccount: address }
|
||||
} catch (err) {
|
||||
if (providerName === 'walletConnect') {
|
||||
const mobileProvider = this.$provider.provider
|
||||
|
||||
if (typeof mobileProvider.disconnect === 'function') {
|
||||
mobileProvider.disconnect()
|
||||
}
|
||||
await dispatch('onLogOut')
|
||||
}
|
||||
throw new Error(`method askPermission has error: ${err.message}`)
|
||||
}
|
||||
},
|
||||
walletConnectSocketListener({ state, commit, dispatch, getters, rootState }, { provider }) {
|
||||
const { enabled } = rootState.loading
|
||||
|
||||
try {
|
||||
provider.wc.on('disconnect', (error, payload) => {
|
||||
if (state.isReconnecting) {
|
||||
console.warn('Provider reconnect payload', { payload, error, isReconnecting: state.isReconnecting })
|
||||
|
||||
if (enabled) {
|
||||
dispatch('loading/disable', {}, { root: true })
|
||||
}
|
||||
commit('SET_RECONNECTING', false)
|
||||
return
|
||||
}
|
||||
|
||||
const prevConnection = localStorage.getItem('walletconnectTimeStamp')
|
||||
|
||||
const isPrevConnection = new BN(Date.now()).minus(prevConnection).isGreaterThanOrEqualTo(5000)
|
||||
|
||||
if (isPrevConnection) {
|
||||
console.warn('Provider disconnect payload', {
|
||||
payload,
|
||||
error,
|
||||
isReconnecting: state.isReconnecting
|
||||
})
|
||||
|
||||
dispatch('onLogOut')
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
dispatch('loading/disable', {}, { root: true })
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('WalletConnect listeners error: ', err)
|
||||
}
|
||||
},
|
||||
async switchNetwork({ dispatch }, { netId }) {
|
||||
try {
|
||||
await this.$provider.sendRequest({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [{ chainId: numberToHex(netId) }]
|
||||
})
|
||||
} catch (err) {
|
||||
// This error indicates that the chain has not been added to MetaMask.
|
||||
if (err.message.includes('wallet_addEthereumChain')) {
|
||||
return dispatch('addNetwork', { netId })
|
||||
}
|
||||
|
||||
throw new Error(err.message)
|
||||
}
|
||||
},
|
||||
async addNetwork(_, { netId }) {
|
||||
const METAMASK_LIST = {
|
||||
56: {
|
||||
chainId: '0x38',
|
||||
chainName: 'Binance Smart Chain Mainnet',
|
||||
rpcUrls: ['https://bsc-dataseed1.ninicoin.io'],
|
||||
nativeCurrency: {
|
||||
name: 'Binance Chain Native Token',
|
||||
symbol: 'BNB',
|
||||
decimals: 18
|
||||
},
|
||||
blockExplorerUrls: ['https://bscscan.com']
|
||||
},
|
||||
10: {
|
||||
chainId: '0xa',
|
||||
chainName: 'Optimism',
|
||||
rpcUrls: ['https://mainnet.optimism.io/'],
|
||||
nativeCurrency: {
|
||||
name: 'Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18
|
||||
},
|
||||
blockExplorerUrls: ['https://optimistic.etherscan.io']
|
||||
},
|
||||
100: {
|
||||
chainId: '0x64',
|
||||
chainName: 'Gnosis Chain (formerly xDai)',
|
||||
rpcUrls: ['https://rpc.gnosischain.com'],
|
||||
nativeCurrency: {
|
||||
name: 'xDAI',
|
||||
symbol: 'xDAI',
|
||||
decimals: 18
|
||||
},
|
||||
blockExplorerUrls: ['https://blockscout.com/xdai/mainnet']
|
||||
},
|
||||
137: {
|
||||
chainId: '0x89',
|
||||
chainName: 'Polygon Mainnet',
|
||||
rpcUrls: ['https://polygon-rpc.com/'],
|
||||
nativeCurrency: {
|
||||
name: 'MATIC',
|
||||
symbol: 'MATIC',
|
||||
decimals: 18
|
||||
},
|
||||
blockExplorerUrls: ['https://polygonscan.com']
|
||||
},
|
||||
42161: {
|
||||
chainId: '0xA4B1',
|
||||
chainName: 'Arbitrum One',
|
||||
rpcUrls: ['https://arb1.arbitrum.io/rpc'],
|
||||
nativeCurrency: {
|
||||
name: 'Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18
|
||||
},
|
||||
blockExplorerUrls: ['https://arbiscan.io']
|
||||
},
|
||||
43114: {
|
||||
chainId: '0xA86A',
|
||||
chainName: 'Avalanche C-Chain',
|
||||
rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'],
|
||||
nativeCurrency: {
|
||||
name: 'Avalanche',
|
||||
symbol: 'AVAX',
|
||||
decimals: 18
|
||||
},
|
||||
blockExplorerUrls: ['https://snowtrace.io']
|
||||
}
|
||||
}
|
||||
|
||||
if (METAMASK_LIST[netId]) {
|
||||
await this.$provider.sendRequest({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [METAMASK_LIST[netId]]
|
||||
})
|
||||
}
|
||||
},
|
||||
async checkNetworkVersion() {
|
||||
try {
|
||||
const id = Number(
|
||||
await this.$provider.sendRequest({
|
||||
method: 'eth_chainId',
|
||||
params: []
|
||||
})
|
||||
)
|
||||
|
||||
return id
|
||||
} catch (err) {
|
||||
throw new Error(err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions
|
||||
}
|
95
store/notice.js
Normal file
95
store/notice.js
Normal file
|
@ -0,0 +1,95 @@
|
|||
const NOTICE_INTERVAL = 10000
|
||||
|
||||
export const state = () => {
|
||||
return {
|
||||
notices: [],
|
||||
timers: {}
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
ADD_NOTICE(state, notice) {
|
||||
state.notices.push(notice)
|
||||
},
|
||||
UPDATE_NOTICE(state, { index, notice }) {
|
||||
this._vm.$set(state.notices, index, notice)
|
||||
},
|
||||
DELETE_NOTICE(state, index) {
|
||||
this._vm.$delete(state.notices, index)
|
||||
},
|
||||
ADD_NOTICE_TIMER(state, { id, timerId }) {
|
||||
this._vm.$set(state.timers, id, { timerId })
|
||||
},
|
||||
DELETE_NOTICE_TIMER(state, id) {
|
||||
this._vm.$delete(state.timers, id)
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
addNotice({ commit }, { notice }) {
|
||||
return new Promise((resolve) => {
|
||||
const id = `f${(+new Date()).toString(16)}`
|
||||
commit('ADD_NOTICE', { ...notice, id, isShowed: true })
|
||||
resolve(id)
|
||||
})
|
||||
},
|
||||
addNoticeTimer({ commit, dispatch }, { id, interval = NOTICE_INTERVAL }) {
|
||||
const timerId = setTimeout(() => {
|
||||
dispatch('deleteNotice', { id })
|
||||
}, interval)
|
||||
commit('ADD_NOTICE_TIMER', { id, timerId })
|
||||
},
|
||||
deleteNoticeTimer({ state, commit }, { id }) {
|
||||
if (state.timers[id]) {
|
||||
clearTimeout(state.timers[id].timerId)
|
||||
commit('DELETE_NOTICE_TIMER', id)
|
||||
}
|
||||
},
|
||||
addNoticeWithInterval({ dispatch }, { notice, interval }) {
|
||||
return new Promise(async (resolve) => {
|
||||
const id = await dispatch('addNotice', { notice })
|
||||
dispatch('addNoticeTimer', { id, interval })
|
||||
resolve(id)
|
||||
})
|
||||
},
|
||||
deleteNotice({ state, commit, dispatch }, { id }) {
|
||||
const index = state.notices.findIndex((i) => {
|
||||
return i.id === id
|
||||
})
|
||||
if (index !== -1) {
|
||||
commit('DELETE_NOTICE', index)
|
||||
dispatch('deleteNoticeTimer', { id })
|
||||
}
|
||||
},
|
||||
updateNotice({ state, commit, dispatch }, { id = `f${(+new Date()).toString(16)}`, notice, interval }) {
|
||||
const { notices } = state
|
||||
const index = notices.findIndex((i) => {
|
||||
return i.id === id
|
||||
})
|
||||
if (index !== -1) {
|
||||
commit('UPDATE_NOTICE', {
|
||||
index,
|
||||
notice: {
|
||||
...notices[index],
|
||||
isShowed: true,
|
||||
...notice
|
||||
}
|
||||
})
|
||||
} else {
|
||||
commit('ADD_NOTICE', { ...notice, id, isShowed: true })
|
||||
}
|
||||
|
||||
if (interval) {
|
||||
dispatch('deleteNoticeTimer', { id })
|
||||
dispatch('addNoticeTimer', { id, interval })
|
||||
}
|
||||
},
|
||||
showNotice({ state, commit, dispatch }, { id, isShowed = true }) {
|
||||
dispatch('updateNotice', {
|
||||
id,
|
||||
notice: {
|
||||
isShowed
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
114
store/price.js
Normal file
114
store/price.js
Normal file
|
@ -0,0 +1,114 @@
|
|||
/* eslint-disable no-console */
|
||||
import { toBN, toChecksumAddress } from 'web3-utils'
|
||||
|
||||
import networkConfig from '@/networkConfig'
|
||||
import offchainOracleABI from '@/abis/OffchainOracle.abi.json'
|
||||
|
||||
const offchainOracleAddress = '0x07D91f5fb9Bf7798734C3f606dB065549F6893bb'
|
||||
|
||||
const TOKENS = {
|
||||
torn: {
|
||||
tokenAddress: '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
|
||||
symbol: 'TORN',
|
||||
decimals: 18
|
||||
}
|
||||
}
|
||||
|
||||
export const state = () => {
|
||||
return {
|
||||
prices: {
|
||||
dai: '593970928097706',
|
||||
cdai: '12623454013395',
|
||||
usdc: '593319851383838',
|
||||
usdt: '592852719537467',
|
||||
torn: '85362951428474830',
|
||||
wbtc: '32269526951862905063'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
tokenRate: (state, getters, rootState, rootGetters) => {
|
||||
return state.prices[rootState.application.selectedStatistic.currency]
|
||||
},
|
||||
getArgsForOracle: (state, getters, rootState, rootGetters) => {
|
||||
const tokens = {
|
||||
...networkConfig.netId1.tokens,
|
||||
...TOKENS
|
||||
}
|
||||
const tokenAddresses = []
|
||||
const oneUintAmount = []
|
||||
const currencyLookup = {}
|
||||
Object.entries(tokens).map(([currency, data]) => {
|
||||
if (currency !== 'eth') {
|
||||
tokenAddresses.push(data.tokenAddress)
|
||||
oneUintAmount.push(
|
||||
toBN('10')
|
||||
.pow(toBN(data.decimals.toString()))
|
||||
.toString()
|
||||
)
|
||||
currencyLookup[data.tokenAddress] = currency
|
||||
}
|
||||
})
|
||||
return { tokenAddresses, oneUintAmount, currencyLookup }
|
||||
},
|
||||
getTokenPrice: (state, getters, rootState, rootGetters) => (currency) => {
|
||||
return state.prices[currency]
|
||||
},
|
||||
isPriceWatcherDisabled: (state, getters, rootState, rootGetters) => {
|
||||
const nativeCurrency = rootGetters['metamask/nativeCurrency']
|
||||
const tokens = Object.keys(rootGetters['metamask/networkConfig'].tokens)
|
||||
return tokens.includes(nativeCurrency) && tokens.length === 1
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
SAVE_TOKEN_PRICES(state, prices) {
|
||||
state.prices = {
|
||||
...state.prices,
|
||||
...prices
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
async fetchTokenPrice({ getters, commit, dispatch, rootState }) {
|
||||
if (getters.isPriceWatcherDisabled) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const web3 = this.$provider.getWeb3(rootState.settings.netId1.rpc.url)
|
||||
const offchainOracle = new web3.eth.Contract(offchainOracleABI, offchainOracleAddress)
|
||||
const { tokenAddresses, oneUintAmount, currencyLookup } = getters.getArgsForOracle
|
||||
|
||||
const prices = {}
|
||||
for (let i = 0; i < tokenAddresses.length; i++) {
|
||||
try {
|
||||
const isWrap =
|
||||
toChecksumAddress(tokenAddresses[i]) ===
|
||||
toChecksumAddress('0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643')
|
||||
|
||||
const price = await offchainOracle.methods.getRateToEth(tokenAddresses[i], isWrap).call()
|
||||
const numerator = toBN(oneUintAmount[i])
|
||||
const denominator = toBN(10).pow(toBN(18)) // eth decimals
|
||||
const priceFormatted = toBN(price)
|
||||
.mul(numerator)
|
||||
.div(denominator)
|
||||
|
||||
prices[currencyLookup[tokenAddresses[i]]] = priceFormatted.toString()
|
||||
} catch (e) {
|
||||
console.error('cant get price of ', tokenAddresses[i])
|
||||
}
|
||||
}
|
||||
|
||||
console.log('prices', prices)
|
||||
commit('SAVE_TOKEN_PRICES', prices)
|
||||
|
||||
setTimeout(() => dispatch('fetchTokenPrice'), 1000 * 30)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setTimeout(() => dispatch('fetchTokenPrice'), 1000 * 30)
|
||||
}
|
||||
}
|
||||
}
|
688
store/relayer.js
Normal file
688
store/relayer.js
Normal file
|
@ -0,0 +1,688 @@
|
|||
/* 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,
|
||||
miningServiceFee: 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 requiredMajor = hasEnabledLightProxy ? '5' : '4'
|
||||
const { major, prerelease } = parseSemanticVersion(response.data.version)
|
||||
|
||||
const isUpdatedMajor = major === requiredMajor
|
||||
|
||||
if (isUpdatedMajor && prerelease && netId === 42161) {
|
||||
const minimalBeta = 10
|
||||
const [betaVersion] = prerelease.split('.').slice(-1)
|
||||
return Number(betaVersion) >= minimalBeta
|
||||
}
|
||||
|
||||
return isUpdatedMajor
|
||||
}
|
||||
|
||||
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,
|
||||
miningServiceFee: response.data.miningServiceFee,
|
||||
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,
|
||||
miningServiceFee
|
||||
} = pickWeightedRandomRelayer(statuses, netId)
|
||||
|
||||
console.log('Selected relayer', name, tornadoServiceFee)
|
||||
commit('SET_SELECTED_RELAYER', {
|
||||
name,
|
||||
address,
|
||||
ethPrices,
|
||||
url: realUrl,
|
||||
stakeBalance,
|
||||
tornadoServiceFee,
|
||||
miningServiceFee
|
||||
})
|
||||
} 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
|
||||
|
||||
if (!url.startsWith('https:') && !url.startsWith('http:')) {
|
||||
if (url.includes('.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,
|
||||
miningServiceFee,
|
||||
tornadoServiceFee
|
||||
} = await dispatch('askRelayerStatus', relayerData)
|
||||
|
||||
if (!isValid) {
|
||||
return { error, isValid: false }
|
||||
}
|
||||
|
||||
return {
|
||||
isValid,
|
||||
name,
|
||||
url: realUrl || '',
|
||||
address: address || '',
|
||||
tornadoServiceFee: tornadoServiceFee || 0.0,
|
||||
miningServiceFee: miningServiceFee || 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
93
store/settings.js
Normal file
93
store/settings.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
/* eslint-disable no-console */
|
||||
import Web3 from 'web3'
|
||||
|
||||
import networkConfig from '@/networkConfig'
|
||||
|
||||
const getFirstRpcs = (acc, [netId, { rpcUrls }]) => {
|
||||
const [rpc] = Object.values(rpcUrls)
|
||||
|
||||
acc[netId] = {
|
||||
rpc
|
||||
}
|
||||
|
||||
return acc
|
||||
}
|
||||
|
||||
const rpcData = Object.entries(networkConfig).reduce(getFirstRpcs, {})
|
||||
|
||||
export const state = () => {
|
||||
return {
|
||||
...rpcData,
|
||||
isActiveNotification: {
|
||||
first: true,
|
||||
second: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
getRpc: (state) => (netId) => {
|
||||
return state[`netId${netId}`].rpc
|
||||
},
|
||||
currentRpc: (state, getters, rootState) => {
|
||||
const netId = rootState.metamask.netId
|
||||
return state[`netId${netId}`].rpc
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
SAVE_RPC(state, { netId, name, url }) {
|
||||
this._vm.$set(state[`netId${netId}`], 'rpc', { name, url })
|
||||
},
|
||||
DISABLE_NOTIFICATION(state, { key }) {
|
||||
this._vm.$set(state, 'isActiveNotification', { ...state.isActiveNotification, [key]: false })
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
disableNotification({ commit }, params) {
|
||||
commit('DISABLE_NOTIFICATION', params)
|
||||
},
|
||||
async checkCurrentRpc({ dispatch, getters, rootGetters }) {
|
||||
const netId = rootGetters['metamask/netId']
|
||||
await dispatch('preselectRpc', { netId })
|
||||
},
|
||||
async preselectRpc({ getters, commit, dispatch }, { netId }) {
|
||||
const savedRpc = getters.getRpc(netId)
|
||||
const { isValid } = await dispatch('checkRpc', { ...savedRpc, netId })
|
||||
|
||||
if (isValid) {
|
||||
return
|
||||
}
|
||||
|
||||
const { rpcUrls } = networkConfig[`netId${netId}`]
|
||||
|
||||
for (const [, { name, url }] of Object.entries(rpcUrls)) {
|
||||
const { isValid, error } = await dispatch('checkRpc', { url, netId })
|
||||
if (isValid) {
|
||||
commit('SAVE_RPC', { netId, name, url })
|
||||
return
|
||||
} else {
|
||||
console.error('preselectRpc', url, error)
|
||||
}
|
||||
}
|
||||
throw new Error(this.app.i18n.t('rpcSelectError'))
|
||||
},
|
||||
async checkRpc(_, { url, netId }) {
|
||||
try {
|
||||
const web3 = new Web3(url)
|
||||
|
||||
const chainId = await web3.eth.getChainId()
|
||||
|
||||
const isCurrent = Number(chainId) === Number(netId)
|
||||
if (isCurrent) {
|
||||
return { isValid: true }
|
||||
} else {
|
||||
return { isValid: false, error: this.app.i18n.t('thisRpcIsForDifferentNetwork') }
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('checkRpc', e)
|
||||
return { isValid: false, error: this.app.i18n.t('rpcIsDown') }
|
||||
}
|
||||
}
|
||||
}
|
113
store/snark.js
Normal file
113
store/snark.js
Normal file
|
@ -0,0 +1,113 @@
|
|||
/* eslint-disable no-console */
|
||||
import Web3 from 'web3'
|
||||
import Jszip from 'jszip'
|
||||
import axios from 'axios'
|
||||
import ENS, { getEnsAddress } from '@ensdomains/ensjs'
|
||||
|
||||
import { detectMob } from '@/utils'
|
||||
import networkConfig from '@/networkConfig'
|
||||
|
||||
const { APP_ENS_NAME } = process.env
|
||||
|
||||
const groth16 = require('websnark/src/groth16')
|
||||
const jszip = new Jszip()
|
||||
|
||||
function buildGroth16() {
|
||||
const isMobile = detectMob()
|
||||
const wasmMemory = isMobile ? 1000 : 2000
|
||||
return groth16({ wasmInitialMemory: wasmMemory })
|
||||
}
|
||||
|
||||
function getEns() {
|
||||
const provider = new Web3.providers.HttpProvider(networkConfig.netId1.rpcUrls.Infura.url)
|
||||
return new ENS({ provider, ensAddress: getEnsAddress('1') })
|
||||
}
|
||||
|
||||
async function getTornadoKeys(getProgress) {
|
||||
try {
|
||||
const keys = await Promise.all([
|
||||
download({ name: 'tornado.json.zip', contentType: 'string' }),
|
||||
download({ name: 'tornadoProvingKey.bin.zip', contentType: 'arraybuffer', getProgress })
|
||||
])
|
||||
return { circuit: JSON.parse(keys[0]), provingKey: keys[1] }
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function getIPFSIdFromENS(ensName) {
|
||||
try {
|
||||
const ens = getEns()
|
||||
|
||||
const ensInterface = await ens.name(ensName)
|
||||
const { value } = await ensInterface.getContent(ensName)
|
||||
|
||||
const [, id] = value.split('://')
|
||||
return id
|
||||
} catch (err) {
|
||||
throw new Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchFile({ url, name, getProgress, id, retryAttempt = 0 }) {
|
||||
try {
|
||||
const response = await axios.get(`${url}/${name}`, {
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
onDownloadProgress: (progressEvent) => {
|
||||
if (typeof getProgress === 'function') {
|
||||
const progress = Math.round((progressEvent.loaded * 100) / 9626311)
|
||||
getProgress(progress)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (err) {
|
||||
if (!id) {
|
||||
id = await getIPFSIdFromENS(APP_ENS_NAME)
|
||||
}
|
||||
const knownResources = [
|
||||
url,
|
||||
`https://ipfs.io/ipfs/${id}`,
|
||||
`https://dweb.link/ipfs/${id}`,
|
||||
`https://gateway.pinata.cloud/ipfs/${id}`
|
||||
]
|
||||
|
||||
if (retryAttempt < knownResources.length) {
|
||||
const fallbackUrl = knownResources[retryAttempt]
|
||||
retryAttempt++
|
||||
|
||||
const response = await fetchFile({ name, getProgress, retryAttempt, id, url: fallbackUrl })
|
||||
|
||||
return response
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Function to download
|
||||
* @param {*} name filename
|
||||
* @param {'base64'|'string'|'binarystring'|'text'|'blob'|'uint8array'|'arraybuffer'|'array'|'nodebuffer'} contentType type of the content.
|
||||
* @param getProgress function
|
||||
*/
|
||||
async function download({ name, contentType, getProgress, eventName = 'events' }) {
|
||||
try {
|
||||
// eslint-disable-next-line no-undef
|
||||
const prefix = __webpack_public_path__.slice(0, -7)
|
||||
const response = await fetchFile({ getProgress, url: prefix, name })
|
||||
|
||||
const zip = await jszip.loadAsync(response.data)
|
||||
const file = zip.file(name.replace(`${eventName}/`, '').slice(0, -4))
|
||||
|
||||
const content = await file.async(contentType)
|
||||
|
||||
return content
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export { getTornadoKeys, buildGroth16, download }
|
222
store/token.js
Normal file
222
store/token.js
Normal file
|
@ -0,0 +1,222 @@
|
|||
/* eslint-disable no-console */
|
||||
/* eslint-disable import/order */
|
||||
import Web3 from 'web3'
|
||||
import networkConfig from '@/networkConfig'
|
||||
import ERC20ABI from '@/abis/ERC20.abi.json'
|
||||
const { numberToHex, toBN, toWei } = require('web3-utils')
|
||||
const BN = require('bn.js')
|
||||
const zero = new BN(0)
|
||||
const negative1 = new BN(-1)
|
||||
|
||||
export const state = () => {
|
||||
return {
|
||||
approvalAmount: 'unlimited',
|
||||
allowance: '',
|
||||
balance: ''
|
||||
}
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
tokenContract: (state, getters, rootState, rootGetters) => ({ currency, netId }) => {
|
||||
const config = networkConfig[`netId${netId}`]
|
||||
const { url } = rootState.settings[`netId${netId}`].rpc
|
||||
const address = config.tokens[currency].tokenAddress
|
||||
const web3 = new Web3(url)
|
||||
return new web3.eth.Contract(ERC20ABI, address)
|
||||
},
|
||||
// similar to fromWei from web3
|
||||
toDecimals: (state, getters, rootState, rootGetters) => (value, decimals, fixed) => {
|
||||
const { currency } = rootState.application.selectedStatistic
|
||||
decimals = decimals || rootGetters['metamask/networkConfig'].tokens[currency].decimals
|
||||
fixed = fixed || 2
|
||||
|
||||
value = new BN(value)
|
||||
const negative = value.lt(zero)
|
||||
const base = new BN('10').pow(new BN(decimals))
|
||||
const baseLength = base.toString(10).length - 1 || 1
|
||||
|
||||
if (negative) {
|
||||
value = value.mul(negative1)
|
||||
}
|
||||
|
||||
let fraction = value.mod(base).toString(10)
|
||||
while (fraction.length < baseLength) {
|
||||
fraction = `0${fraction}`
|
||||
}
|
||||
fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1]
|
||||
const whole = value.div(base).toString(10)
|
||||
|
||||
if (fixed && fraction !== '0') {
|
||||
fraction = fraction.slice(0, fixed)
|
||||
}
|
||||
value = `${whole}${fraction === '0' ? '' : `.${fraction}`}`
|
||||
|
||||
if (negative) {
|
||||
value = `-${value}`
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
// similar to toWei from web3
|
||||
fromDecimals: (state, getters, rootState, rootGetters) => (value, decimals) => {
|
||||
const { currency } = rootState.application.selectedStatistic
|
||||
decimals = decimals || rootGetters['metamask/networkConfig'].tokens[currency].decimals
|
||||
value = value.toString()
|
||||
let ether = value.toString()
|
||||
const base = new BN('10').pow(new BN(decimals))
|
||||
const baseLength = base.toString(10).length - 1 || 1
|
||||
|
||||
const negative = ether.substring(0, 1) === '-'
|
||||
if (negative) {
|
||||
ether = ether.substring(1)
|
||||
}
|
||||
|
||||
if (ether === '.') {
|
||||
throw new Error(this.app.i18n.t('unitInvalidValue', { value }))
|
||||
}
|
||||
|
||||
// Split it into a whole and fractional part
|
||||
const comps = ether.split('.')
|
||||
if (comps.length > 2) {
|
||||
throw new Error(this.app.i18n.t('tooManyDecimalPoints', { value }))
|
||||
}
|
||||
|
||||
let whole = comps[0]
|
||||
let fraction = comps[1]
|
||||
|
||||
if (!whole) {
|
||||
whole = '0'
|
||||
}
|
||||
if (!fraction) {
|
||||
fraction = '0'
|
||||
}
|
||||
if (fraction.length > baseLength) {
|
||||
throw new Error(this.app.i18n.t('tooManyDecimalPlaces', { value }))
|
||||
}
|
||||
|
||||
while (fraction.length < baseLength) {
|
||||
fraction += '0'
|
||||
}
|
||||
|
||||
whole = new BN(whole)
|
||||
fraction = new BN(fraction)
|
||||
let wei = whole.mul(base).add(fraction)
|
||||
|
||||
if (negative) {
|
||||
wei = wei.mul(negative)
|
||||
}
|
||||
|
||||
return new BN(wei.toString(10), 10)
|
||||
},
|
||||
isSufficientAllowance: (state, getters, rootState, rootGetters) => {
|
||||
const { currency, amount } = rootState.application.selectedInstance
|
||||
const { decimals } = rootGetters['metamask/networkConfig'].tokens[currency]
|
||||
return toBN(state.allowance).gte(toBN(getters.fromDecimals(amount, decimals)))
|
||||
},
|
||||
isSufficientBalance: (state, getters, rootState, rootGetters) => {
|
||||
const ethBalance = rootState.metamask.ethBalance
|
||||
const { currency, amount } = rootState.application.selectedInstance
|
||||
const { decimals } = rootGetters['metamask/networkConfig'].tokens[currency]
|
||||
const nativeCurrency = rootGetters['metamask/nativeCurrency']
|
||||
if (currency === nativeCurrency) {
|
||||
return toBN(ethBalance).gte(toBN(toWei(amount.toString())))
|
||||
} else {
|
||||
return toBN(state.balance).gte(toBN(getters.fromDecimals(amount, decimals)))
|
||||
}
|
||||
},
|
||||
getSymbol: (state, getters, rootState, rootGetters) => (currency) => {
|
||||
const tokens = rootGetters['metamask/networkConfig'].tokens
|
||||
if (tokens[currency]) {
|
||||
return tokens[currency].symbol
|
||||
}
|
||||
return currency.toUpperCase()
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
SET_APPROVAL_AMOUNT(state, { approvalAmount }) {
|
||||
state.approvalAmount = approvalAmount
|
||||
},
|
||||
SAVE_ALLOWANCE(state, { allowance }) {
|
||||
this._vm.$set(state, 'allowance', allowance)
|
||||
},
|
||||
SAVE_BALANCE(state, { balance }) {
|
||||
this._vm.$set(state, 'balance', balance)
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
async approve({ rootState, getters, dispatch, rootGetters, state }) {
|
||||
try {
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const { currency } = rootState.application.selectedInstance
|
||||
const { decimals } = rootGetters['metamask/networkConfig'].tokens[currency]
|
||||
const tokenInstance = getters.tokenContract({ currency, netId })
|
||||
const tornadoProxy = rootGetters['application/tornadoProxyContract']({ netId })
|
||||
|
||||
const { ethAccount } = rootState.metamask
|
||||
const amountToApprove =
|
||||
state.approvalAmount === 'unlimited'
|
||||
? toBN('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
|
||||
: toBN(getters.fromDecimals(state.approvalAmount, decimals))
|
||||
const data = tokenInstance.methods
|
||||
.approve(tornadoProxy._address, amountToApprove.toString())
|
||||
.encodeABI()
|
||||
const gas = await tokenInstance.methods
|
||||
.approve(tornadoProxy._address, amountToApprove.toString())
|
||||
.estimateGas({
|
||||
from: ethAccount
|
||||
})
|
||||
|
||||
const callParams = {
|
||||
method: 'eth_sendTransaction',
|
||||
params: {
|
||||
to: tokenInstance._address,
|
||||
gas: numberToHex(gas + 30000),
|
||||
data
|
||||
},
|
||||
watcherParams: {
|
||||
title: 'approve',
|
||||
successTitle: 'approved'
|
||||
},
|
||||
isSaving: false
|
||||
}
|
||||
|
||||
await dispatch('metamask/sendTransaction', callParams, { root: true })
|
||||
} catch (e) {
|
||||
console.error('approve action', e)
|
||||
throw new Error(e.message)
|
||||
}
|
||||
},
|
||||
async fetchTokenAllowance({ getters, rootGetters, commit, rootState }) {
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const { currency } = rootState.application.selectedInstance
|
||||
const { ethAccount } = rootState.metamask
|
||||
try {
|
||||
const tornadoInstance = rootGetters['application/tornadoProxyContract']({ netId })
|
||||
const nativeCurrency = rootGetters['metamask/nativeCurrency']
|
||||
if (currency !== nativeCurrency && ethAccount) {
|
||||
const tokenInstance = getters.tokenContract({ currency, netId })
|
||||
const allowance = await tokenInstance.methods.allowance(ethAccount, tornadoInstance._address).call()
|
||||
commit('SAVE_ALLOWANCE', { allowance })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('fetchTokenAllowance', e.message)
|
||||
}
|
||||
},
|
||||
async fetchTokenBalance({ state, getters, rootGetters, commit, rootState }) {
|
||||
try {
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const { currency } = rootState.application.selectedInstance
|
||||
const { ethAccount } = rootState.metamask
|
||||
const nativeCurrency = rootGetters['metamask/nativeCurrency']
|
||||
if (currency !== nativeCurrency && ethAccount) {
|
||||
const tokenInstance = getters.tokenContract({ currency, netId })
|
||||
const balance = await tokenInstance.methods.balanceOf(ethAccount).call()
|
||||
commit('SAVE_BALANCE', { balance })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('fetchTokenBalance', e.message)
|
||||
}
|
||||
}
|
||||
}
|
227
store/torn.js
Normal file
227
store/torn.js
Normal file
|
@ -0,0 +1,227 @@
|
|||
/* eslint-disable no-console */
|
||||
import Web3 from 'web3'
|
||||
import { toBN } from 'web3-utils'
|
||||
import tornABI from '../abis/ERC20.abi.json'
|
||||
|
||||
import { isWalletRejection } from '@/utils'
|
||||
|
||||
const { toWei, toHex, numberToHex } = require('web3-utils')
|
||||
const { PermitSigner } = require('../lib/Permit')
|
||||
|
||||
const state = () => {
|
||||
return {
|
||||
approvalAmount: 'unlimited',
|
||||
allowance: '0',
|
||||
balance: '',
|
||||
signature: {
|
||||
v: '',
|
||||
r: '',
|
||||
s: '',
|
||||
amount: '',
|
||||
deadline: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getters = {
|
||||
tokenContract: (state, getters, rootState, rootGetters) => {
|
||||
const tornContract = rootGetters['metamask/networkConfig']['torn.contract.tornadocash.eth']
|
||||
const { url } = rootGetters['settings/currentRpc']
|
||||
const web3 = new Web3(url)
|
||||
return new web3.eth.Contract(tornABI, tornContract)
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
SET_APPROVAL_AMOUNT(state, { approvalAmount }) {
|
||||
state.approvalAmount = approvalAmount
|
||||
},
|
||||
SAVE_ALLOWANCE(state, { allowance }) {
|
||||
this._vm.$set(state, 'allowance', allowance)
|
||||
},
|
||||
SAVE_BALANCE(state, { balance }) {
|
||||
this._vm.$set(state, 'balance', balance)
|
||||
},
|
||||
SAVE_SIGNATURE(state, { v, r, s, amount, deadline }) {
|
||||
this._vm.$set(state, 'signature', { v, r, s, amount, deadline })
|
||||
},
|
||||
REMOVE_SIGNATURE(state) {
|
||||
this._vm.$set(state, 'signature', { v: '', r: '', s: '', amount: '', deadline: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
async approve({ rootState, getters, commit, dispatch, rootGetters, state }, { amount }) {
|
||||
try {
|
||||
console.log('call approve')
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const govInstance = rootGetters['governance/gov/govContract']({ netId })
|
||||
const amountToApprove = toHex(toWei(amount.toString()))
|
||||
const data = getters.tokenContract.methods
|
||||
.approve(govInstance._address, amountToApprove.toString())
|
||||
.encodeABI()
|
||||
const gas = 100000
|
||||
|
||||
const callParams = {
|
||||
method: 'eth_sendTransaction',
|
||||
params: {
|
||||
data,
|
||||
gas: numberToHex(gas + 100000),
|
||||
to: getters.tokenContract._address
|
||||
},
|
||||
watcherParams: {
|
||||
title: 'approve',
|
||||
successTitle: 'approved'
|
||||
},
|
||||
isSaving: false
|
||||
}
|
||||
|
||||
await dispatch('metamask/sendTransaction', callParams, { root: true })
|
||||
|
||||
dispatch('fetchTokenAllowance')
|
||||
} catch (e) {
|
||||
console.error('approve action', e)
|
||||
throw new Error(e.message)
|
||||
}
|
||||
},
|
||||
async fetchTokenAllowance({ getters, rootGetters, commit, rootState }) {
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const { ethAccount } = rootState.metamask
|
||||
|
||||
if (!ethAccount) {
|
||||
return
|
||||
}
|
||||
|
||||
const govInstance = rootGetters['governance/gov/govContract']({ netId })
|
||||
try {
|
||||
const allowance = await getters.tokenContract.methods.allowance(ethAccount, govInstance._address).call()
|
||||
commit('SAVE_ALLOWANCE', { allowance })
|
||||
|
||||
return allowance
|
||||
} catch (e) {
|
||||
console.error('fetchTokenAllowance', e.message)
|
||||
}
|
||||
},
|
||||
async fetchTokenBalance({ state, getters, rootGetters, commit, rootState }) {
|
||||
try {
|
||||
const { ethAccount } = rootState.metamask
|
||||
if (ethAccount) {
|
||||
const balance = await getters.tokenContract.methods.balanceOf(ethAccount).call()
|
||||
console.log('torn', balance)
|
||||
commit('SAVE_BALANCE', { balance })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('fetchTokenBalance', e.message)
|
||||
}
|
||||
},
|
||||
async detectedAllowance({ dispatch }, initValue) {
|
||||
try {
|
||||
const allowance = await dispatch('fetchTokenAllowance')
|
||||
|
||||
if (toBN(allowance).gte(initValue)) {
|
||||
return true
|
||||
}
|
||||
setTimeout(() => {
|
||||
return dispatch('detectedAllowance', initValue)
|
||||
}, 5000)
|
||||
} catch (err) {
|
||||
console.error('detectedAllowance has error', err.message)
|
||||
setTimeout(() => {
|
||||
return dispatch('detectedAllowance', initValue)
|
||||
}, 5000)
|
||||
}
|
||||
},
|
||||
async signApprove({ rootState, getters, rootGetters, dispatch, commit }, { amount }) {
|
||||
try {
|
||||
const { ethAccount } = rootState.metamask
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const govInstance = rootGetters['governance/gov/govContract']({ netId })
|
||||
|
||||
const domain = {
|
||||
name: 'TornadoCash',
|
||||
version: '1',
|
||||
chainId: netId,
|
||||
verifyingContract: getters.tokenContract._address
|
||||
}
|
||||
const oneDayFromNow = Math.ceil(Date.now() / 1000) + 86400
|
||||
// fetch nonces
|
||||
let nonce = 0
|
||||
try {
|
||||
nonce = await getters.tokenContract.methods.nonces(ethAccount).call()
|
||||
console.log('nonce', nonce)
|
||||
} catch (e) {
|
||||
throw new Error(e.message)
|
||||
}
|
||||
|
||||
// const pre = toHex(toWei(amount.toString())).substr(2)
|
||||
// let result = pre.length % 2 === 0 ? pre : '0' + pre
|
||||
// result = '0x' + result
|
||||
|
||||
const value = toWei(amount.toString())
|
||||
|
||||
const args = {
|
||||
owner: ethAccount,
|
||||
spender: govInstance._address,
|
||||
value, // 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
|
||||
nonce,
|
||||
deadline: oneDayFromNow
|
||||
}
|
||||
|
||||
const permitSigner = new PermitSigner(domain, args)
|
||||
const message = permitSigner.getPayload()
|
||||
|
||||
const callParams = {
|
||||
method: 'eth_signTypedData_v4',
|
||||
params: [ethAccount, JSON.stringify(message)]
|
||||
}
|
||||
|
||||
dispatch(
|
||||
'loading/changeText',
|
||||
{
|
||||
message: this.app.i18n.t('pleaseSignRequestInWallet', {
|
||||
wallet: rootState.metamask.walletName
|
||||
}),
|
||||
type: 'approve'
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
|
||||
let signature = await this.$provider.sendRequest(callParams)
|
||||
signature = signature.substring(2)
|
||||
const r = '0x' + signature.substring(0, 64)
|
||||
const s = '0x' + signature.substring(64, 128)
|
||||
let v = parseInt(signature.substring(128, 130), 16)
|
||||
|
||||
// fix ledger sign
|
||||
if (v === 0 || v === 1) {
|
||||
v = v + 27
|
||||
}
|
||||
|
||||
console.log('signature', v, r, s, signature)
|
||||
|
||||
// signature validation on contract
|
||||
await getters.tokenContract.methods
|
||||
.permit(args.owner, args.spender, args.value, args.deadline, v, r, s)
|
||||
.call()
|
||||
|
||||
commit('SAVE_SIGNATURE', { v, r, s, amount, deadline: oneDayFromNow })
|
||||
} catch (e) {
|
||||
console.error('signApprove', e.message)
|
||||
if (!isWalletRejection(e)) {
|
||||
setTimeout(async () => {
|
||||
await dispatch('approve', { amount })
|
||||
dispatch('detectedAllowance', amount)
|
||||
}, 1000)
|
||||
}
|
||||
} finally {
|
||||
dispatch('loading/disable', {}, { root: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions
|
||||
}
|
451
store/txHashKeeper.js
Normal file
451
store/txHashKeeper.js
Normal file
|
@ -0,0 +1,451 @@
|
|||
/* eslint-disable no-console */
|
||||
import { hexToNumber } from 'web3-utils'
|
||||
|
||||
import txStatus from './txStatus'
|
||||
|
||||
import { eventsType } from '@/constants'
|
||||
import { createChainIdState, parseNote } from '@/utils'
|
||||
|
||||
const initialState = createChainIdState({
|
||||
txs: {},
|
||||
govTxs: {},
|
||||
encryptedTxs: {}
|
||||
})
|
||||
|
||||
export const state = () => {
|
||||
return initialState
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
txExplorerUrl: (state, getters, rootState, rootGetters) => (txHash) => {
|
||||
const { explorerUrl } = rootGetters['metamask/networkConfig']
|
||||
return explorerUrl.tx + txHash
|
||||
},
|
||||
addressExplorerUrl: (state, getters, rootState, rootGetters) => (address) => {
|
||||
const { explorerUrl } = rootGetters['metamask/networkConfig']
|
||||
return explorerUrl.address + address
|
||||
},
|
||||
blockExplorerUrl: (state, getters, rootState, rootGetters) => (block) => {
|
||||
const { explorerUrl } = rootGetters['metamask/networkConfig']
|
||||
return explorerUrl.block + block
|
||||
},
|
||||
encryptedTxs: (state, getters, rootState, rootGetters) => {
|
||||
const netId = rootGetters['metamask/netId']
|
||||
|
||||
const txsToRender = Object.entries(state[`netId${netId}`].encryptedTxs)
|
||||
.reverse()
|
||||
.map(([txHash, tx]) => {
|
||||
return {
|
||||
isEncrypted: true,
|
||||
txHash,
|
||||
status,
|
||||
...tx
|
||||
}
|
||||
})
|
||||
return txsToRender
|
||||
},
|
||||
txs: (state, getters, rootState, rootGetters) => {
|
||||
const netId = rootGetters['metamask/netId']
|
||||
const txsToRender = Object.entries(state[`netId${netId}`].txs)
|
||||
.reverse()
|
||||
.map(([txHash, tx]) => {
|
||||
return {
|
||||
txHash,
|
||||
status,
|
||||
...tx
|
||||
}
|
||||
})
|
||||
return txsToRender
|
||||
},
|
||||
allTxs: (state, getters) => {
|
||||
const { txs, encryptedTxs } = getters
|
||||
return txs.concat(encryptedTxs)
|
||||
},
|
||||
allTxsHash: (state, getters, rootState, rootGetters) => {
|
||||
const netId = rootGetters['metamask/netId']
|
||||
return Object.entries(state[`netId${netId}`].txs)
|
||||
.reverse()
|
||||
.map(([txHash]) => txHash)
|
||||
},
|
||||
txStatusClass: () => (status) => {
|
||||
let cssClass
|
||||
switch (status) {
|
||||
case txStatus.waitingForReciept:
|
||||
cssClass = 'is-loading'
|
||||
break
|
||||
case txStatus.success:
|
||||
cssClass = 'is-success'
|
||||
break
|
||||
case txStatus.fail:
|
||||
cssClass = 'is-danger'
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return cssClass
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
SAVE_TX_HASH(state, { storeType = 'txs', amount = '0', note = null, netId, txHash, status, ...rest }) {
|
||||
this._vm.$set(state[`netId${netId}`][storeType], [txHash], {
|
||||
...rest,
|
||||
status: status || txStatus.waitingForReciept,
|
||||
note,
|
||||
amount
|
||||
})
|
||||
},
|
||||
CHANGE_TX_STATUS(state, { storeType = 'txs', txHash, status, blockNumber, netId }) {
|
||||
this._vm.$set(state[`netId${netId}`][storeType][txHash], 'status', status)
|
||||
this._vm.$set(state[`netId${netId}`][storeType][txHash], 'blockNumber', blockNumber)
|
||||
},
|
||||
SET_SPENT(state, { netId, storeType = 'txs', txHash }) {
|
||||
this._vm.$set(state[`netId${netId}`][storeType][txHash], 'isSpent', true)
|
||||
},
|
||||
DELETE_TX(state, { storeType = 'txs', txHash }) {
|
||||
const netId = this._vm['metamask/netId']
|
||||
this._vm.$delete(state[`netId${netId}`][storeType], txHash)
|
||||
},
|
||||
UPDATE_DEPOSIT(state, { storeType = 'txs', txHash, netId, withdrawTxHash, timestamp, status, ...rest }) {
|
||||
const tx = state[`netId${netId}`][storeType][txHash]
|
||||
this._vm.$delete(state[`netId${netId}`][storeType], txHash)
|
||||
|
||||
this._vm.$set(state[`netId${netId}`][storeType], withdrawTxHash, {
|
||||
...tx,
|
||||
timestamp,
|
||||
depositTxHash: txHash,
|
||||
txHash: withdrawTxHash,
|
||||
depositBlock: tx.blockNumber,
|
||||
status: status || txStatus.waitingForReciept,
|
||||
...rest
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
async getInstances({ rootGetters }, { txs }) {
|
||||
const eventsInterface = rootGetters['application/eventsInterface']
|
||||
|
||||
const instances = txs.reduce((acc, curr) => {
|
||||
const [, currency, amount, netId] = curr.prefix.split('-')
|
||||
|
||||
const name = `${amount}${currency}`
|
||||
if (!acc[name]) {
|
||||
const service = eventsInterface.getService({ netId, amount, currency })
|
||||
acc[name] = { currency, amount, netId, service }
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
await Promise.all(
|
||||
[].concat(
|
||||
Object.values(instances).map((instance) => instance.service.updateEvents(eventsType.DEPOSIT)),
|
||||
Object.values(instances).map((instance) => instance.service.updateEvents(eventsType.WITHDRAWAL))
|
||||
)
|
||||
)
|
||||
|
||||
return instances
|
||||
},
|
||||
async checkPendingEncryptedTransaction({ dispatch, getters }) {
|
||||
const transactions = getters.encryptedTxs
|
||||
|
||||
const pendingTxs = transactions.filter((tx) => tx.status === 1)
|
||||
|
||||
const instances = await dispatch('getInstances', { txs: pendingTxs })
|
||||
|
||||
for (const tx of pendingTxs) {
|
||||
const [, currency, amount, netId] = tx.prefix.split('-')
|
||||
|
||||
dispatch('checkSpeedUpEncryptedTx', {
|
||||
netId,
|
||||
txHash: tx.txHash,
|
||||
type: eventsType.DEPOSIT,
|
||||
commitment: tx.commitmentHex,
|
||||
service: instances[`${amount}${currency}`]
|
||||
})
|
||||
}
|
||||
},
|
||||
async checkSpeedUpEncryptedTx({ commit, dispatch }, { txHash, commitment, netId, service, type }) {
|
||||
try {
|
||||
const result = await this.$provider.web3.eth.getTransactionReceipt(txHash)
|
||||
|
||||
if (result) {
|
||||
const status = result.status === '0x0' ? txStatus.fail : txStatus.success
|
||||
|
||||
commit('CHANGE_TX_STATUS', {
|
||||
netId,
|
||||
txHash,
|
||||
status,
|
||||
storeType: 'encryptedTxs',
|
||||
blockNumber: hexToNumber(result.blockNumber)
|
||||
})
|
||||
} else {
|
||||
const foundEvent = await service.findEvent({ eventName: 'commitment', eventToFind: commitment, type })
|
||||
|
||||
if (foundEvent) {
|
||||
commit('UPDATE_DEPOSIT', {
|
||||
netId,
|
||||
txHash,
|
||||
storeType: 'encryptedTxs',
|
||||
status: txStatus.success,
|
||||
timestamp: foundEvent.timestamp,
|
||||
withdrawTxHash: foundEvent.txHash,
|
||||
blockNumber: foundEvent.depositBlock
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`Method checkSpeedUpEncryptedTx has error ${err.message}`)
|
||||
}
|
||||
},
|
||||
async cleanEncryptedTxs({ getters, commit, dispatch }) {
|
||||
getters.encryptedTxs.forEach(({ status, txHash, type }) => {
|
||||
if (status === txStatus.fail) {
|
||||
commit('DELETE_TX', { txHash, storeType: 'encryptedTxs' })
|
||||
}
|
||||
})
|
||||
|
||||
const instances = await dispatch('getInstances', {
|
||||
txs: getters.encryptedTxs
|
||||
})
|
||||
|
||||
for (const tx of getters.encryptedTxs) {
|
||||
if (!tx.isSpent) {
|
||||
const { currency, amount, netId, nullifierHex } = parseNote(`${tx.prefix}-${tx.note}`)
|
||||
|
||||
const isSpent = await instances[`${amount}${currency}`].service.findEvent({
|
||||
eventName: 'nullifierHash',
|
||||
eventToFind: nullifierHex,
|
||||
type: eventsType.WITHDRAWAL
|
||||
})
|
||||
|
||||
if (isSpent) {
|
||||
commit('SET_SPENT', { txHash: tx.txHash, storeType: 'encryptedTxs', netId })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
checkPendingTransaction({ getters, dispatch }) {
|
||||
const transactions = getters.txs
|
||||
|
||||
const pendingTxs = transactions.filter((tx) => tx.status === 1)
|
||||
|
||||
for (const tx of pendingTxs) {
|
||||
const [, , , netId] = tx.prefix.split('-')
|
||||
|
||||
dispatch('checkPendingSpeedUpTx', { txHash: tx.txHash, note: `${tx.prefix}-${tx.note}`, netId })
|
||||
}
|
||||
},
|
||||
async checkPendingSpeedUpTx({ commit, dispatch }, { txHash, netId, note }) {
|
||||
try {
|
||||
const result = await this.$provider.web3.eth.getTransactionReceipt(txHash)
|
||||
|
||||
if (result) {
|
||||
const status = result.status === '0x0' ? txStatus.fail : txStatus.success
|
||||
|
||||
commit('CHANGE_TX_STATUS', {
|
||||
storeType: 'txs',
|
||||
txHash,
|
||||
blockNumber: hexToNumber(result.blockNumber),
|
||||
status,
|
||||
netId
|
||||
})
|
||||
} else {
|
||||
const response = await dispatch(
|
||||
'application/loadDepositEvent',
|
||||
{ withdrawNote: note },
|
||||
{ root: true }
|
||||
)
|
||||
|
||||
if (response) {
|
||||
commit('UPDATE_DEPOSIT', {
|
||||
netId,
|
||||
txHash,
|
||||
storeType: 'txs',
|
||||
status: txStatus.success,
|
||||
timestamp: response.timestamp,
|
||||
withdrawTxHash: response.txHash,
|
||||
blockNumber: response.depositBlock
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`Method checkPendingSpeedUpTx has error ${err.message}`)
|
||||
}
|
||||
},
|
||||
async runTxWatcher({ commit, dispatch }, { storeType = 'txs', txHash, netId, isSaving = true }) {
|
||||
console.log('runTxWatcher storeType txHash, netId', storeType, txHash, netId)
|
||||
try {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { status, blockNumber } = await this.$provider.waitForTxReceipt({ txHash })
|
||||
status = status === '0x0' ? txStatus.fail : txStatus.success
|
||||
|
||||
if (isSaving) {
|
||||
commit('CHANGE_TX_STATUS', {
|
||||
storeType,
|
||||
txHash,
|
||||
blockNumber: hexToNumber(blockNumber),
|
||||
status,
|
||||
netId
|
||||
})
|
||||
}
|
||||
|
||||
return status === txStatus.success
|
||||
} catch (e) {
|
||||
console.error('runTxWatcher', e)
|
||||
return false
|
||||
}
|
||||
},
|
||||
async runTxWatcherWithNotifications(
|
||||
{ dispatch },
|
||||
{ title, successTitle, storeType = 'txs', txHash, netId, onSuccess, isSaving }
|
||||
) {
|
||||
try {
|
||||
const noticeId = await dispatch(
|
||||
'notice/addNotice',
|
||||
{
|
||||
notice: {
|
||||
title,
|
||||
type: 'loading',
|
||||
txHash
|
||||
}
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
const success = await dispatch('runTxWatcher', { storeType, txHash, netId, isSaving })
|
||||
if (success) {
|
||||
dispatch(
|
||||
'notice/updateNotice',
|
||||
{
|
||||
id: noticeId,
|
||||
notice: {
|
||||
title: successTitle,
|
||||
type: 'success'
|
||||
},
|
||||
interval: 10000
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
if (typeof onSuccess === 'function') {
|
||||
onSuccess(txHash)
|
||||
}
|
||||
} else {
|
||||
dispatch(
|
||||
'notice/updateNotice',
|
||||
{
|
||||
id: noticeId,
|
||||
notice: {
|
||||
title: 'transactionFailed',
|
||||
type: 'danger'
|
||||
}
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
}
|
||||
return success
|
||||
} catch (e) {
|
||||
console.error('runTxWatcherWithNotifications', e)
|
||||
return false
|
||||
}
|
||||
},
|
||||
async cleanTxs({ getters, commit, dispatch }) {
|
||||
// isSpentArray
|
||||
getters.txs.forEach(({ status, txHash, type }) => {
|
||||
if (status === txStatus.fail) {
|
||||
commit('DELETE_TX', { txHash })
|
||||
}
|
||||
})
|
||||
|
||||
const instances = await dispatch('getInstances', {
|
||||
txs: getters.txs
|
||||
})
|
||||
|
||||
for (const tx of getters.txs) {
|
||||
if (tx && !tx.isSpent) {
|
||||
const { currency, amount, netId, nullifierHex } = parseNote(`${tx.prefix}-${tx.note}`)
|
||||
|
||||
const isSpent = await instances[`${amount}${currency}`].service.findEvent({
|
||||
eventName: 'nullifierHash',
|
||||
eventToFind: nullifierHex,
|
||||
type: eventsType.WITHDRAWAL
|
||||
})
|
||||
|
||||
if (isSpent) {
|
||||
commit('SET_SPENT', { txHash: tx.txHash, netId })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async updateDeposit(
|
||||
{ getters, commit, dispatch, rootGetters },
|
||||
{ netId, type = 'tornado', action = 'Withdraw', note, txHash, fee, amount, currency }
|
||||
) {
|
||||
const timestamp = Math.round(new Date().getTime() / 1000)
|
||||
|
||||
let txMutation = 'SAVE_TX_HASH'
|
||||
const tx = {
|
||||
txHash,
|
||||
type: action,
|
||||
amount,
|
||||
currency,
|
||||
fee,
|
||||
netId,
|
||||
timestamp,
|
||||
status: 2
|
||||
}
|
||||
|
||||
if (type === 'tornado') {
|
||||
const [tornado, , , , hexNote] = note.split('-')
|
||||
const { commitmentHex } = parseNote(note)
|
||||
tx.prefix = `${tornado}-${currency}-${amount}-${netId}`
|
||||
tx.isSpent = true
|
||||
|
||||
const encryptedTxs = getters.encryptedTxs
|
||||
const encrypted = encryptedTxs.find((tx) => {
|
||||
return tx.commitmentHex === commitmentHex
|
||||
})
|
||||
|
||||
tx.storeType = encrypted ? 'encryptedTxs' : 'txs'
|
||||
|
||||
const deposit =
|
||||
encrypted ||
|
||||
getters.txs.find(({ note }) => {
|
||||
return note === hexNote
|
||||
})
|
||||
|
||||
tx.note = encrypted ? encrypted.note : hexNote
|
||||
|
||||
const blockNumber = await dispatch('getBlockNumber', { txHash })
|
||||
tx.blockNumber = blockNumber
|
||||
|
||||
if (deposit && deposit.txHash) {
|
||||
txMutation = 'UPDATE_DEPOSIT'
|
||||
tx.txHash = deposit.txHash
|
||||
tx.withdrawTxHash = txHash
|
||||
} else {
|
||||
const events = await dispatch('application/loadDepositEvent', { withdrawNote: note }, { root: true })
|
||||
|
||||
tx.withdrawTxHash = txHash
|
||||
tx.txHash = events.txHash
|
||||
tx.depositBlock = events.depositBlock
|
||||
tx.index = events.leafIndex
|
||||
}
|
||||
}
|
||||
|
||||
commit(txMutation, tx)
|
||||
},
|
||||
async getBlockNumber({ rootState }, { txHash }) {
|
||||
try {
|
||||
const { netId } = rootState.metamask
|
||||
const { url } = rootState.settings[`netId${netId}`].rpc
|
||||
|
||||
const web3 = this.$provider.getWeb3(url)
|
||||
|
||||
const { blockNumber } = await web3.eth.getTransaction(txHash)
|
||||
|
||||
return blockNumber
|
||||
} catch (err) {
|
||||
console.log('getBlockNumber has error:', err.message)
|
||||
}
|
||||
}
|
||||
}
|
8
store/txStatus.js
Normal file
8
store/txStatus.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
const statuses = Object.freeze({
|
||||
nonExistent: 0,
|
||||
waitingForReciept: 1,
|
||||
success: 2,
|
||||
fail: 3
|
||||
})
|
||||
|
||||
export default statuses
|
Loading…
Add table
Add a link
Reference in a new issue