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

View file

@ -0,0 +1,36 @@
<template>
<div class="field">
<div class="label">
{{ $t('relayerTotal.label') }}
</div>
<div class="withdraw-data">
<div class="withdraw-data-item">
{{ $t('relayerTotal.name') }}
<span>{{ relayerName }}</span>
</div>
<div class="withdraw-data-item">
{{ $t('relayerTotal.fee') }}
<span>{{ selectedRelayer.tornadoServiceFee }}%</span>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState('relayer', ['selectedRelayer']),
relayerName() {
const { name, url } = this.selectedRelayer
if (name === 'custom') {
return url.replace(/http(s)?|:|\//g, '')
}
return name
}
}
}
</script>

View file

@ -0,0 +1,526 @@
<template>
<b-tab-item :label="$t('withdraw')">
<div class="field">
<div class="label-with-buttons">
<div class="label">
{{ $t('note') }}
<b-tooltip :label="$t('noteTooltip')" size="is-small" position="is-right" multilined>
<button class="button is-primary has-icon">
<span class="icon icon-info"></span>
</button>
</b-tooltip>
</div>
<a
v-show="!hasErrorNote && depositTxHash"
:href="txExplorerUrl(depositTxHash)"
target="_blank"
class="button is-icon"
>
<b-tooltip
:label="$t('depositTransactionOnEtherscan')"
size="is-small"
position="is-left"
multilined
>
<LinkIcon />
</b-tooltip>
</a>
<button v-show="shouldSettingsShow" class="button is-icon" @click="onSettings">
<b-tooltip :label="$t('withdrawalSettings')" size="is-small" position="is-right" multilined>
<SettingsIcon />
</b-tooltip>
</button>
</div>
<b-input
v-model="withdrawNote"
:placeholder="$t('pleaseEnterYourNote')"
:custom-class="hasErrorNote ? hasErrorNote.type : 'is-primary'"
></b-input>
<div v-if="hasErrorNote" class="help" :class="hasErrorNote.type">
<!-- eslint-disable vue/no-v-html -->
<p v-html="hasErrorNote.msg"></p>
</div>
</div>
<div v-if="!hasErrorNote && depositTxHash" class="field field-withdraw">
<div class="withdraw-data">
<div class="withdraw-data-item">
{{ $t('amount') }} <span>{{ selectedAmount }} {{ selectedStatisticCurrency }}</span>
</div>
<div class="withdraw-data-item">
{{ $t('timePassed') }}
<b-tooltip
:active="notEnoughPassedTime"
:label="$t('timePassedTooltip')"
position="is-left"
multilined
size="is-large"
:class="{ 'has-low-anonymity': notEnoughPassedTime }"
>
<span>{{ timePassed }}</span>
</b-tooltip>
</div>
<div class="withdraw-data-item">
{{ $t('subsequentDeposits') }}
<b-tooltip
:active="notEnoughDeposits"
:label="$t('subsequentDepositsTooltip')"
position="is-left"
multilined
size="is-large"
:class="{ 'has-low-anonymity': notEnoughDeposits }"
>
<span>{{ this.$tc('userDeposit', depositsPast) }}</span>
</b-tooltip>
</div>
</div>
</div>
<fieldset>
<div class="field withdraw-address">
<div class="label-with-buttons">
<div class="label">
<span class="name">
{{ $t('recipientAddress') }}
</span>
</div>
<button class="button is-primary-text" @click="insertDonate">
{{ $t('donate') }}
</button>
</div>
<b-input
v-model="withdrawAddress"
:placeholder="$t('pleasePasteAddressHere')"
:size="!withdrawAddress ? '' : isValidAddress ? 'is-primary' : 'is-warning'"
></b-input>
<p class="help">
<span class="has-text-warning">{{ error.type }}</span> {{ error.message }}
</p>
</div>
<RelayerTotal v-show="shouldShowRelayerTotal" />
<WithdrawTotal
v-show="shouldShowTotal"
:currency="selectedStatisticCurrency"
:withdraw-type="withdrawType"
:eth-to-receive="ethToReceive"
/>
<b-tooltip
class="is-block"
:label="`${$t(tooltipText)}`"
position="is-top"
:active="shouldTooltipShow"
multilined
>
<b-button
type="is-primary is-fullwidth"
class="slide-animation"
:outlined="isLoading"
:expanded="isLoading"
:class="{ 'slide-animation-active': isLoading }"
:disabled="isWithdrawalButtonDisable"
:loading="isLoadingRelayers || isLoading"
@click="onWithdraw"
>
{{ $t('withdrawButton') }}
</b-button>
</b-tooltip>
</fieldset>
</b-tab-item>
</template>
<script>
/* eslint-disable no-console */
import { mapState, mapGetters } from 'vuex'
import { getTornadoKeys } from '@/store/snark'
import { parseNote } from '@/utils'
import { DONATIONS_ADDRESS } from '@/constants'
import { LinkIcon, SettingsIcon } from '@/components/icons'
import RelayerTotal from '@/components/withdraw/RelayerTotal'
import WithdrawTotal from '@/components/withdraw/WithdrawTotal'
import WithdrawModalBox from '@/components/withdraw/WithdrawModalBox'
import SettingsModalBox from '@/components/settings/SettingsModalBox'
const { toChecksumAddress, isHexStrict, isAddress } = require('web3-utils')
export default {
components: {
LinkIcon,
RelayerTotal,
SettingsIcon,
WithdrawTotal
},
props: {
activeTab: {
required: true,
type: Number
}
},
data() {
return {
withdrawAddress: '',
withdrawNote: '',
depositsPast: null,
depositTxHash: null,
depositTimestamp: null,
isSpent: false,
isLoading: false,
isFileError: false,
error: {
type: null,
message: ''
},
timePassed: ''
}
},
computed: {
...mapState('application', ['note', 'errors', 'withdrawType', 'ethToReceive']),
...mapState('relayer', ['isLoadingRelayers']),
...mapGetters('txHashKeeper', ['txExplorerUrl']),
...mapGetters('application', ['isNotEnoughTokens', 'selectedStatisticCurrency']),
...mapGetters('metamask', ['networkConfig', 'netId', 'isLoggedIn', 'nativeCurrency']),
notEnoughDeposits() {
if (this.depositsPast < 5) {
return true
}
return false
},
shouldSettingsShow() {
return !this.hasErrorNote && !this.error.message
},
hasErrorNote() {
const note = this.withdrawNote.split('-')[4]
if (typeof WebAssembly === 'undefined') {
return {
type: 'is-warning',
msg: this.$t('turnOnWasm')
}
}
if (!this.withdrawNote) {
return { type: '', msg: '' }
}
if (!note || note.length < 126 || !isHexStrict(note)) {
return { type: 'is-warning', msg: this.$t('noteIsInvalid') }
}
if (this.isSpent) {
return { type: 'is-warning', msg: this.$t('noteHasBeenSpent') }
}
return false
},
withDisconnectedWallet() {
return this.withdrawType === 'wallet' && !this.isLoggedIn
},
shouldTooltipShow() {
return (!this.isWithdrawDisabled && this.isNotEnoughTokens) || this.withDisconnectedWallet
},
tooltipText() {
if (this.withDisconnectedWallet) {
return 'withDisconnectedWallet'
}
return 'notEnoughTokens'
},
isValidAddress() {
return isAddress(this.withdrawAddress)
},
notEnoughPassedTime() {
return this.$moment().unix() - Number(this.depositTimestamp) < 86400 // less than 24 hours
},
hasWarning() {
return this.depositsPast < 5 || this.notEnoughPassedTime
},
isWithdrawDisabled() {
return (
this.isLoading ||
!!this.error.type ||
this.hasErrorNote ||
(this.withdrawType === 'relayer' && !this.selectedRelayer) ||
!this.isValidAddress
)
},
isWithdrawalButtonDisable() {
return (
this.isWithdrawDisabled || this.isNotEnoughTokens || this.isFileError || this.withDisconnectedWallet
)
},
selectedRelayer() {
return this.$store.state.relayer.selectedRelayer.name
},
selectedAmount() {
return this.$store.state.application.selectedStatistic.amount
},
tokens() {
return this.networkConfig.tokens
},
shouldShowTotal() {
return this.isValidAddress && !this.isWithdrawDisabled
},
shouldShowRelayerTotal() {
return this.withdrawType === 'relayer' && this.shouldShowTotal
}
},
watch: {
netId(netId, oldNetId) {
if (netId !== oldNetId) {
const [, , , noteNetId] = this.withdrawNote.split('-')
if (Number(noteNetId) !== netId && noteNetId) {
this.error = {
type: this.$t('error'),
message: this.$t('changeNetworkNote')
}
} else {
this.error = {
type: '',
message: ''
}
}
}
},
errors: {
handler(errors) {
console.log('error', errors)
this.error = {
type: errors.length ? this.$t('error') : null,
message: errors[errors.length - 1]
}
if (this.error.message) {
this.$store.dispatch('notice/addNoticeWithInterval', {
notice: {
untranslatedTitle: this.error.message,
type: 'warning'
}
})
}
},
deep: true
},
withdrawNote: {
async handler(withdrawNote) {
this.error = {
type: '',
message: ''
}
try {
this.$store.dispatch('loading/enable', { message: this.$t('gettingTheNoteData') })
this.isSpent = false
this.depositsPast = null
this.depositTxHash = null
this.depositTimestamp = null
if (!this.hasErrorNote) {
const [tornadoPrefix, currency, amount, noteNetId, note] = this.withdrawNote.split('-')
if (tornadoPrefix !== 'tornado') {
this.$store.dispatch('loading/disable')
this.withdrawNote = `tornado-${currency}-${amount}-${noteNetId}-${note}`
return
}
this.getLogs(withdrawNote)
this.$store.commit('application/SET_WITHDRAW_NOTE', withdrawNote)
const netIdMissmatch = Number(noteNetId) !== Number(this.netId)
if (netIdMissmatch) {
throw new Error(this.$t('changeNetworkNote'))
}
const event = await this.$store.dispatch('application/loadDepositEvent', { withdrawNote })
if (!event) {
throw new Error(this.$t('thereIsNoRelatedDeposit'))
}
const { timestamp, txHash, isSpent, depositsPast = 0 } = event
if (isSpent) {
this.$store.dispatch('notice/addNoticeWithInterval', {
notice: {
title: 'noteWasAlreadySpent',
type: 'warning'
},
interval: 5000
})
}
this.$store.dispatch('application/setAndUpdateStatistic', { currency, amount: Number(amount) })
if (currency !== this.nativeCurrency) {
this.$store.dispatch('application/setDefaultEthToReceive', { currency })
}
this.depositsPast = Number(depositsPast) <= 0 ? 0 : depositsPast
this.depositTxHash = txHash
this.depositTimestamp = timestamp
this.isSpent = isSpent
}
} catch (err) {
this.error = {
type: this.$t('error'),
message: err.message
}
} finally {
this.$store.dispatch('loading/disable')
}
}
},
'$i18n.locale'() {
this.timePastToRender()
},
depositTimestamp() {
this.timePastToRender()
},
activeTab(newTab, oldTab) {
if (newTab !== oldTab && newTab === 1) {
this.error = {
type: '',
message: ''
}
}
}
},
created() {
this.$emit('get-key', this.getKeys)
},
mounted() {
if (this.$route.query.note) {
this.withdrawNote = this.$route.query.note
}
this.$root.$on('resetWithdraw', () => {
this.withdrawAddress = ''
this.withdrawNote = ''
})
},
methods: {
async getKeys() {
try {
this.isFileError = false
this.isLoading = true
this.getProgress(0)
await getTornadoKeys(this.getProgress)
return true
} catch (err) {
console.error('getKeys has error:', err.message)
this.$store.dispatch(
'notice/addNoticeWithInterval',
{
notice: {
title: 'fetchFile',
type: 'warning'
}
},
{ root: true }
)
this.error = {
type: this.$t('downloadError'),
message: this.$t('fetchFile')
}
this.isFileError = true
} finally {
this.isLoading = false
}
},
getProgress(value) {
document.documentElement.style.setProperty('--width-animation', `${value}%`)
},
getLogs(note) {
try {
if (!note) {
return
}
const { commitmentHex, nullifierHex } = parseNote(note)
console.log('\n\nYOUR NOTE DATA:')
console.log('note:', note)
console.log('commitment:', commitmentHex)
console.log('nullifierHash:', nullifierHex)
} catch (err) {
console.log(`Get logs: ${err.message}`)
}
},
onWithdraw() {
const note = this.withdrawNote.split('-')[4]
if (note.length !== 126) {
this.error = {
type: this.$t('validationError'),
message: this.$t('noteIsInvalid')
}
return
}
try {
this.withdrawAddress = toChecksumAddress(this.withdrawAddress)
this.$store.dispatch('application/prepareWithdraw', {
note: this.withdrawNote,
recipient: this.withdrawAddress
})
this.error.type = null
this.currentModal = this.$buefy.modal.open({
parent: this,
component: WithdrawModalBox,
hasModalCard: true,
width: 440,
props: {
note: this.withdrawNote,
withdrawType: this.withdrawType
}
})
} catch (e) {
this.error = {
type: this.$t('validationError'),
message: this.$t('recipientAddressIsInvalid')
}
console.error('error', e)
}
},
onSettings() {
this.$buefy.modal.open({
parent: this,
component: SettingsModalBox,
hasModalCard: true,
width: 440,
props: {
currency: this.selectedStatisticCurrency
},
customClass: 'is-pinned'
})
},
timePastToRender() {
this.timePassed = this.$moment.unix(this.depositTimestamp).fromNow(true)
},
insertDonate() {
this.withdrawAddress = DONATIONS_ADDRESS
}
}
}
</script>
<style lang="scss" scoped>
:root {
--width-animation: 0;
}
.slide-animation {
position: relative;
&:before {
content: '';
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: var(--width-animation);
height: 100%;
background-color: #94febf;
animation-fill-mode: backwards;
}
::v-deep span {
position: relative;
z-index: 2;
}
&-active {
::v-deep span,
&:after {
filter: invert(0.5);
}
}
}
</style>

View file

@ -0,0 +1,88 @@
<template>
<div class="modal-card box box-modal">
<header class="box-modal-header is-spaced">
<div class="box-modal-title">{{ $t('withdrawalConfirmation') }}</div>
<button type="button" class="delete" @click="$emit('close')" />
</header>
<div class="note">
{{ message }}
</div>
<b-button type="is-primary is-fullwidth" @click="_sendWithdraw">
{{ $t('confirm') }}
</b-button>
</div>
</template>
<script>
/* eslint-disable no-console */
import { mapState } from 'vuex'
export default {
props: {
note: {
type: String,
required: true
},
withdrawType: {
type: String,
required: true
}
},
data() {
return {
message: ''
}
},
computed: {
...mapState('application', ['notes', 'errors']),
withdrawalMethod() {
if (this.withdrawType === 'wallet') {
return 'application/withdraw'
}
return 'relayer/relayTornadoWithdraw'
}
},
watch: {
notes(newNotes) {
if (newNotes[this.note]) {
this.$store.dispatch('loading/disable')
this.message = this.$t('yourZkSnarkProofHasBeenSuccesfullyGenerated')
}
},
errors: {
handler(type) {
this.$store.dispatch('loading/disable')
this.$parent.close()
},
deep: true
}
},
mounted() {
this.$store.dispatch('loading/enable', { message: this.$t('generatingProof') })
},
methods: {
async _sendWithdraw() {
this.$store.dispatch('loading/enable', { message: this.$t('preparingTransactionData') })
try {
await this.$store.dispatch(this.withdrawalMethod, {
note: this.note
})
this.$root.$emit('resetWithdraw')
} catch (e) {
console.error(e)
this.$store.dispatch('notice/addNoticeWithInterval', {
notice: {
untranslatedTitle: e.message,
type: 'danger'
},
interval: 3000
})
} finally {
this.$store.dispatch('loading/disable')
this.$parent.close()
}
}
}
}
</script>

View file

@ -0,0 +1,139 @@
<template>
<div class="field">
<div class="label">
{{ $t('total') }}
</div>
<div class="withdraw-data">
<div v-if="isTokenSelected" class="withdraw-data-item">
{{ $t('noteBalance') }}
<span>
{{ selectedStatistic.amount }}
{{ currency }}
</span>
</div>
<div v-if="withdrawType === 'relayer'" class="withdraw-data-item">
{{ $t('gasPrice') }}
<span>{{ gasPrices.fast }} Gwei</span>
</div>
<div v-if="withdrawType === 'relayer'" class="withdraw-data-item">
{{ $t('networkFee') }}
<span>{{ networkFeeInEth }} {{ networkCurrency }}</span>
</div>
<div v-if="withdrawType === 'relayer'" class="withdraw-data-item">
{{ $t('relayerFee') }}
<span>{{ toDecimals(relayerFee, null, 6) }} {{ currency }}</span>
</div>
<div v-if="withdrawType === 'relayer'" class="withdraw-data-item">
{{ $t('totalFee') }}
<span>{{ toDecimals(totalRelayerFee, null, 6) }} {{ currency }}</span>
</div>
<div v-if="isTokenSelected" class="withdraw-data-item">
{{ $t('ethPurchase', { currency: networkCurrency }) }}
<span>{{ toDecimals(ethToReceiveInToken, null, 6) }} {{ currency }}</span>
</div>
<hr v-if="withdrawType === 'relayer'" />
<div class="withdraw-data-item">
{{ $t('tokensToReceive') }}
<span>{{ total }} {{ currency }}</span>
</div>
<div v-if="isTokenSelected" class="withdraw-data-item">
<span class="is-alone">{{ ethToReceiveFromWei }} {{ networkCurrency }}</span>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import { decimalPlaces } from '@/utils'
const { fromWei, toBN } = require('web3-utils')
export default {
props: {
currency: {
type: String,
default: 'ETH'
},
withdrawType: {
type: String,
default: 'wallet'
},
ethToReceive: {
type: String,
default: '20000000000000000'
},
serviceFee: {
type: Number,
default: null
}
},
computed: {
...mapState('application', ['selectedStatistic']),
...mapGetters('metamask', ['networkConfig', 'nativeCurrency']),
...mapGetters('metamask', {
networkCurrency: 'currency'
}),
...mapGetters('gasPrices', ['gasPrices']),
...mapGetters('token', ['toDecimals', 'fromDecimals']),
...mapGetters('application', ['networkFee']),
...mapGetters('price', ['tokenRate']),
networkFeeInEth() {
return fromWei(this.networkFee)
},
relayerFee() {
const { amount } = this.selectedStatistic
const total = toBN(this.fromDecimals(amount.toString()))
const fee = this.serviceFee || this.$store.state.relayer.selectedRelayer.tornadoServiceFee
const decimalsPoint = decimalPlaces(fee)
const roundDecimal = 10 ** decimalsPoint
const aroundFee = toBN(parseInt(fee * roundDecimal, 10))
const tornadoServiceFee = total.mul(toBN(aroundFee)).div(toBN(roundDecimal * 100))
return tornadoServiceFee
},
totalRelayerFee() {
const tornadoServiceFee = this.relayerFee
const { currency } = this.selectedStatistic
const { decimals } = this.networkConfig.tokens[currency]
const ethFee = this.networkFee
if (currency === this.nativeCurrency) {
return ethFee.add(tornadoServiceFee)
}
const tokenFee = ethFee.mul(toBN(10 ** decimals)).div(toBN(this.tokenRate))
return tokenFee.add(tornadoServiceFee)
},
isTokenSelected() {
return (
this.withdrawType === 'relayer' &&
this.selectedStatistic.currency !== this.nativeCurrency &&
this.currency !== 'TORN'
)
},
ethToReceiveInToken() {
const { currency } = this.selectedStatistic
const { decimals } = this.networkConfig.tokens[currency]
const price = this.tokenRate
return toBN(this.ethToReceive)
.mul(toBN(10 ** decimals))
.div(toBN(price))
},
ethToReceiveFromWei() {
return fromWei(this.ethToReceive)
},
total() {
const { amount, currency } = this.selectedStatistic
let total = toBN(this.fromDecimals(amount.toString()))
if (this.withdrawType === 'relayer') {
const relayerFee = this.totalRelayerFee
if (currency === this.nativeCurrency) {
total = total.sub(relayerFee)
} else {
total = total.sub(relayerFee).sub(this.ethToReceiveInToken)
}
}
return this.toDecimals(total, null, 6)
}
}
}
</script>