This commit is contained in:
Danil Kovtonyuk 2022-04-22 13:05:56 +10:00
commit 44f31f8b9f
No known key found for this signature in database
GPG key ID: E72A919BF08C3746
402 changed files with 47865 additions and 0 deletions

981
store/application.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,8 @@
const statuses = Object.freeze({
nonExistent: 0,
waitingForReciept: 1,
success: 2,
fail: 3
})
export default statuses