init
This commit is contained in:
commit
44f31f8b9f
402 changed files with 47865 additions and 0 deletions
94
components/ApproveModalBox.vue
Normal file
94
components/ApproveModalBox.vue
Normal file
|
@ -0,0 +1,94 @@
|
|||
<template>
|
||||
<div class="modal-card box box-modal">
|
||||
<header class="box-modal-header is-spaced">
|
||||
<div class="box-modal-title">{{ $t('approvalIsRequired') }}</div>
|
||||
<button type="button" class="delete" @click="$emit('close')" />
|
||||
</header>
|
||||
<div class="note">
|
||||
{{ $t('inOrderToUse', { currency: selectedCurrency }) }}
|
||||
</div>
|
||||
<b-field class="withdraw-radio">
|
||||
<b-radio v-model="approvalAmount" :native-value="defaultApprovalAmount" class="radio-relayer">
|
||||
{{ defaultApprovalAmount }} {{ selectedCurrency }}
|
||||
</b-radio>
|
||||
<b-radio v-model="approvalAmount" :native-value="unlimitedValue" class="radio-metamask">
|
||||
{{ $t('unlimited') }}
|
||||
<b-tooltip :label="$t('unlimitedTooltip')" position="is-top" multilined>
|
||||
<button class="button is-primary has-icon">
|
||||
<span class="icon icon-info"></span>
|
||||
</button>
|
||||
</b-tooltip>
|
||||
</b-radio>
|
||||
</b-field>
|
||||
<b-button type="is-primary is-fullwidth" :disabled="isApproveBtnAlreadyPressed" @click="_approve">
|
||||
{{ $t('enable') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint-disable no-console */
|
||||
import { mapActions, mapState, mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
success: false,
|
||||
isApproveBtnAlreadyPressed: false,
|
||||
unlimitedValue: 'unlimited'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('application', ['selectedInstance']),
|
||||
...mapState('token', ['allowance']),
|
||||
...mapGetters('txHashKeeper', ['txExplorerUrl']),
|
||||
...mapGetters('token', ['fromDecimals']),
|
||||
...mapGetters('application', ['selectedCurrency']),
|
||||
approvalAmount: {
|
||||
get() {
|
||||
return this.$store.state.token.approvalAmount
|
||||
},
|
||||
set(approvalAmount) {
|
||||
this.$store.commit('token/SET_APPROVAL_AMOUNT', { approvalAmount })
|
||||
}
|
||||
},
|
||||
defaultApprovalAmount() {
|
||||
return this.selectedInstance.amount.toString()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.approvalAmount = this.unlimitedValue
|
||||
},
|
||||
methods: {
|
||||
...mapActions('token', ['approve']),
|
||||
async _approve() {
|
||||
this.isApproveBtnAlreadyPressed = true
|
||||
this.$store.dispatch('loading/enable', { message: this.$t('preparingTransactionData') })
|
||||
try {
|
||||
await this.approve()
|
||||
await this.$store.dispatch('token/fetchTokenAllowance')
|
||||
this.$store.dispatch('notice/addNoticeWithInterval', {
|
||||
notice: {
|
||||
title: 'transactionWasSuccessfullySent',
|
||||
type: 'success'
|
||||
},
|
||||
interval: 3000
|
||||
})
|
||||
this.success = true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.$store.dispatch('notice/addNoticeWithInterval', {
|
||||
notice: {
|
||||
untranslatedTitle: e.message,
|
||||
type: 'danger'
|
||||
},
|
||||
interval: 5000
|
||||
})
|
||||
this.success = false
|
||||
}
|
||||
this.isApproveBtnAlreadyPressed = false
|
||||
this.$store.dispatch('loading/disable')
|
||||
this.$parent.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
41
components/BalanceModalBox.vue
Normal file
41
components/BalanceModalBox.vue
Normal file
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<div class="modal-card box box-modal">
|
||||
<header class="box-modal-header is-spaced">
|
||||
<div class="box-modal-title">{{ $t('insufficientBalance') }}</div>
|
||||
<button type="button" class="delete" @click="$emit('close')" />
|
||||
</header>
|
||||
<i18n path="youDontHaveEnoughTokens" tag="div" class="note">
|
||||
<template v-slot:currency>{{ selectedCurrency }}</template>
|
||||
<template v-slot:balance><number-format :value="currentBalance"/></template>
|
||||
</i18n>
|
||||
<b-button type="is-primary is-fullwidth" @click="close">{{ $t('close') }}</b-button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint-disable no-console */
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import NumberFormat from '@/components/NumberFormat'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NumberFormat
|
||||
},
|
||||
computed: {
|
||||
...mapState('application', ['selectedInstance']),
|
||||
...mapState('token', ['balance']),
|
||||
...mapGetters('token', ['toDecimals']),
|
||||
...mapState('metamask', ['ethBalance']),
|
||||
...mapGetters('metamask', ['nativeCurrency']),
|
||||
...mapGetters('application', ['selectedCurrency']),
|
||||
currentBalance() {
|
||||
const balance = this.selectedInstance.currency === this.nativeCurrency ? this.ethBalance : this.balance
|
||||
return this.toDecimals(balance, null, 6)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$parent.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
176
components/Deposit.vue
Normal file
176
components/Deposit.vue
Normal file
|
@ -0,0 +1,176 @@
|
|||
<template>
|
||||
<b-tab-item :label="$t('deposit')">
|
||||
<fieldset>
|
||||
<b-field :label="$t('token')">
|
||||
<b-dropdown v-model="selectedToken" expanded aria-role="list">
|
||||
<div slot="trigger" class="control">
|
||||
<div class="input">
|
||||
<span>{{ selectedCurrency }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<b-dropdown-item v-for="(token, key) in tokens" :key="key" aria-role="listitem" :value="key">
|
||||
{{ token.symbol }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<template slot="label">
|
||||
{{ $t('amount') }}
|
||||
<b-tooltip :label="$t('amountTooltip')" size="is-small" position="is-right" multilined>
|
||||
<button class="button is-primary has-icon">
|
||||
<span class="icon icon-info"></span>
|
||||
</button>
|
||||
</b-tooltip>
|
||||
</template>
|
||||
<b-steps
|
||||
v-model="currentStep"
|
||||
size="is-small"
|
||||
:has-navigation="false"
|
||||
:mobile-mode="null"
|
||||
@input="changeAmount"
|
||||
>
|
||||
<template v-for="({ amount, address }, key) in amounts">
|
||||
<b-step-item :key="key" :label="shortenAmount(amount)" :clickable="address !== ''"></b-step-item>
|
||||
</template>
|
||||
</b-steps>
|
||||
</b-field>
|
||||
</fieldset>
|
||||
<connect-button v-if="!isLoggedIn" type="is-primary is-fullwidth" />
|
||||
<b-button v-else type="is-primary is-fullwidth" :loading="isDepositBtnClicked" @click="onDeposit">
|
||||
{{ $t('depositButton') }}
|
||||
</b-button>
|
||||
</b-tab-item>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint-disable no-console */
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import ApproveModalBox from '@/components/ApproveModalBox'
|
||||
import BalanceModalBox from '@/components/BalanceModalBox'
|
||||
import DepositModalBox from '@/components/DepositModalBox'
|
||||
|
||||
import { ConnectButton } from '@/components/web3Connect'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConnectButton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentStep: 0,
|
||||
amounts: [],
|
||||
isDepositBtnClicked: false,
|
||||
isDepositModalOpened: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('token', ['isSufficientAllowance', 'isSufficientBalance']),
|
||||
...mapGetters('metamask', ['networkConfig', 'netId', 'isLoggedIn', 'nativeCurrency']),
|
||||
...mapGetters('application', ['selectedCurrency']),
|
||||
selectedAmount: {
|
||||
get() {
|
||||
return this.$store.state.application.selectedInstance.amount
|
||||
},
|
||||
set(selectedAmount) {
|
||||
const currency = this.selectedToken
|
||||
const amount = selectedAmount
|
||||
this.$store.commit('application/SET_SELECTED_INSTANCE', { currency, amount })
|
||||
this.$store.dispatch('application/setAndUpdateStatistic', { currency, amount })
|
||||
}
|
||||
},
|
||||
tokens() {
|
||||
return this.networkConfig.tokens
|
||||
},
|
||||
selectedToken: {
|
||||
get() {
|
||||
return this.$store.state.application.selectedInstance.currency
|
||||
},
|
||||
set(selectedToken) {
|
||||
this.currentStep = 0
|
||||
const currency = selectedToken
|
||||
const amount = this.sortAmounts(currency)[0].amount
|
||||
this.$store.commit('application/SET_SELECTED_INSTANCE', { currency, amount })
|
||||
this.$store.dispatch('application/setAndUpdateStatistic', { currency, amount })
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
netId: {
|
||||
handler(netId) {
|
||||
this.sortAmounts()
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.sortAmounts()
|
||||
},
|
||||
methods: {
|
||||
shortenAmount(n) {
|
||||
return `${this.$n(n, 'compact')} ${this.tokens[this.selectedToken].symbol}`
|
||||
},
|
||||
changeAmount(i) {
|
||||
this.selectedAmount = Number(this.amounts[i].amount)
|
||||
},
|
||||
sortAmounts(currency = this.selectedToken) {
|
||||
this.amounts = Object.entries(this.tokens[currency].instanceAddress)
|
||||
.sort((a, b) => {
|
||||
return a[0] - b[0]
|
||||
})
|
||||
.map(([amount, address]) => {
|
||||
return { amount: Number(amount), address }
|
||||
})
|
||||
return this.amounts
|
||||
},
|
||||
openDepositModal() {
|
||||
this.$store.dispatch('application/prepareDeposit', {
|
||||
prefix: `tornado-${this.selectedToken}-${this.selectedAmount}-${this.netId}`
|
||||
})
|
||||
const depositModal = this.$buefy.modal.open({
|
||||
parent: this,
|
||||
component: DepositModalBox,
|
||||
hasModalCard: true,
|
||||
width: 440,
|
||||
customClass: 'is-pinned',
|
||||
canCancel: false
|
||||
})
|
||||
depositModal.$on('close', () => {
|
||||
this.isDepositModalOpened = false
|
||||
})
|
||||
},
|
||||
async onDeposit() {
|
||||
const onApproval = () => {
|
||||
if (this.isSufficientAllowance) {
|
||||
if (!this.isDepositModalOpened) {
|
||||
this.isDepositModalOpened = true
|
||||
this.openDepositModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
this.isDepositBtnClicked = true
|
||||
await this.$store.dispatch('token/fetchTokenAllowance', {}, { root: true })
|
||||
await this.$store.dispatch('token/fetchTokenBalance', {}, { root: true })
|
||||
await this.$store.dispatch('metamask/updateAccountBalance')
|
||||
if (!this.isSufficientBalance) {
|
||||
this.$buefy.modal.open({
|
||||
parent: this,
|
||||
component: BalanceModalBox,
|
||||
hasModalCard: true,
|
||||
width: 440
|
||||
})
|
||||
} else if (this.isSufficientAllowance || this.selectedToken === this.nativeCurrency) {
|
||||
this.openDepositModal()
|
||||
} else {
|
||||
const parent = this
|
||||
const approveModal = this.$buefy.modal.open({
|
||||
parent,
|
||||
component: ApproveModalBox,
|
||||
hasModalCard: true,
|
||||
width: 440
|
||||
})
|
||||
approveModal.$on('close', onApproval)
|
||||
}
|
||||
this.isDepositBtnClicked = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
148
components/DepositModalBox.vue
Normal file
148
components/DepositModalBox.vue
Normal file
|
@ -0,0 +1,148 @@
|
|||
<template>
|
||||
<div class="modal-card box box-modal">
|
||||
<header class="box-modal-header is-spaced">
|
||||
<div class="box-modal-title">{{ $t('yourNote') }}</div>
|
||||
<button type="button" class="delete" @click="$emit('close')" />
|
||||
</header>
|
||||
<div class="note">
|
||||
<div>{{ $t('pleaseBackupYourNote') }}</div>
|
||||
<div>{{ $t('treatYourNote') }}</div>
|
||||
</div>
|
||||
<div class="znote">
|
||||
{{ prefix }}-{{ note }}
|
||||
<b-tooltip :label="tooltipCopy" position="is-top">
|
||||
<button
|
||||
v-clipboard:copy="`${prefix}-${note}`"
|
||||
v-clipboard:success="onCopy"
|
||||
class="button is-primary has-icon"
|
||||
>
|
||||
<span class="icon icon-copy"></span>
|
||||
</button>
|
||||
</b-tooltip>
|
||||
<b-tooltip :label="$t('saveNote')" position="is-top">
|
||||
<button class="button is-primary has-icon" @click="onSave">
|
||||
<span class="icon icon-save"></span>
|
||||
</button>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
<div v-show="isEnabledSaveFile" class="note">
|
||||
{{ $t('saveAsFile') }} <span class="has-text-primary">{{ filename }}</span>
|
||||
</div>
|
||||
<gas-price-slider v-if="!eipSupported" v-model="gasPrice" @validate="onGasPriceValidate" />
|
||||
<template v-if="!isSetupAccount">
|
||||
<i18n tag="div" path="yourDontHaveAccount" class="notice">
|
||||
<template v-slot:account>
|
||||
<a @click="_redirectToAccount">{{ $t('account.button') }}</a>
|
||||
</template>
|
||||
</i18n>
|
||||
</template>
|
||||
<b-checkbox v-if="isSetupAccount" v-model="isEncrypted">
|
||||
<i18n v-show="isSetupAccount" tag="div" path="iEncryptedTheNote">
|
||||
<template v-slot:address>
|
||||
<b-tooltip :label="tooltipCopy" position="is-top">
|
||||
<a class="has-text-primary" @click.prevent.stop="copyNoteAccount">{{ getEncryptAccount }}</a>
|
||||
</b-tooltip>
|
||||
</template>
|
||||
</i18n>
|
||||
</b-checkbox>
|
||||
<template v-if="!isSetupAccount || !isEncrypted">
|
||||
<b-checkbox v-model="isBackuped">{{ $t('iBackedUpTheNote') }}</b-checkbox>
|
||||
<div v-if="isBackuped && isIPFS" class="notice is-warning">
|
||||
<div class="notice__p">{{ $t('yourNoteWontBeSaved') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<connect-button v-if="!isLoggedIn" type="is-primary is-fullwidth" />
|
||||
<b-button
|
||||
v-else
|
||||
type="is-primary is-fullwidth"
|
||||
:disabled="disableButton || (!isValidGasPrice && !eipSupported)"
|
||||
@click="_sendDeposit"
|
||||
>
|
||||
{{ $t('sendDeposit') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint-disable no-console */
|
||||
import { mapActions, mapState, mapGetters } from 'vuex'
|
||||
|
||||
import { sliceAddress } from '@/utils'
|
||||
import GasPriceSlider from '@/components/GasPriceSlider'
|
||||
import { ConnectButton } from '@/components/web3Connect'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConnectButton,
|
||||
GasPriceSlider
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isBackuped: false,
|
||||
tooltipCopy: this.$t('clickToCopy'),
|
||||
gasPrice: undefined,
|
||||
isValidGasPrice: false,
|
||||
isEncrypted: false,
|
||||
copyTimer: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('metamask', ['isLoggedIn']),
|
||||
...mapGetters('gasPrices', ['eipSupported']),
|
||||
...mapGetters('txHashKeeper', ['addressExplorerUrl']),
|
||||
...mapGetters('encryptedNote', ['isSetupAccount', 'accounts', 'isEnabledSaveFile']),
|
||||
...mapState('application', ['note', 'prefix']),
|
||||
isIPFS() {
|
||||
return this.$isLoadedFromIPFS()
|
||||
},
|
||||
filename() {
|
||||
return `backup-${this.prefix}-${this.note.slice(0, 10)}.txt`
|
||||
},
|
||||
getEncryptAccount() {
|
||||
return sliceAddress(this.accounts.encrypt)
|
||||
},
|
||||
disableButton() {
|
||||
if (this.isBackuped) {
|
||||
return !this.isBackuped
|
||||
} else {
|
||||
return !this.isEncrypted || !this.isSetupAccount
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.isEncrypted = this.isSetupAccount
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearTimeout(this.copyTimer)
|
||||
},
|
||||
methods: {
|
||||
...mapActions('application', ['sendDeposit', 'saveFile']),
|
||||
...mapActions('encryptedNote', ['redirectToAccount']),
|
||||
onCopy() {
|
||||
this.tooltipCopy = this.$t('copied')
|
||||
this.copyTimer = setTimeout(() => {
|
||||
this.tooltipCopy = this.$t('clickToCopy')
|
||||
}, 1500)
|
||||
},
|
||||
onSave() {
|
||||
this.saveFile({ note: this.note, prefix: this.prefix })
|
||||
},
|
||||
_redirectToAccount() {
|
||||
this.redirectToAccount()
|
||||
this.$emit('close')
|
||||
},
|
||||
async _sendDeposit() {
|
||||
this.$store.dispatch('loading/enable', { message: this.$t('preparingTransactionData') })
|
||||
await this.sendDeposit({ gasPrice: this.gasPrice, isEncrypted: this.isEncrypted })
|
||||
this.$store.dispatch('loading/disable')
|
||||
this.$parent.close()
|
||||
},
|
||||
onGasPriceValidate(value) {
|
||||
this.isValidGasPrice = value
|
||||
},
|
||||
async copyNoteAccount() {
|
||||
await this.$copyText(this.accounts.encrypt)
|
||||
this.onCopy()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
256
components/EncryptedTx.vue
Normal file
256
components/EncryptedTx.vue
Normal file
|
@ -0,0 +1,256 @@
|
|||
<template>
|
||||
<div
|
||||
class="box box-tx is-encrypted"
|
||||
:class="{
|
||||
'is-waiting': isWaiting,
|
||||
'is-danger': isFailed,
|
||||
'is-spent': tx.isSpent
|
||||
}"
|
||||
>
|
||||
<b-tooltip
|
||||
:label="$t('lockTooltip', { address: getOwner })"
|
||||
position="is-right"
|
||||
class="lock-tooltip"
|
||||
multilined
|
||||
>
|
||||
<div class="lock"></div>
|
||||
</b-tooltip>
|
||||
<div class="columns is-vcentered">
|
||||
<div class="column is-time" :data-label="$t('timePassed')">{{ time }}</div>
|
||||
<div class="column is-amount" :data-label="$t('amount')">
|
||||
<NumberFormat :value="amount" />
|
||||
{{ currency }}
|
||||
</div>
|
||||
<div class="column is-deposit" :data-label="$t('subsequentDeposits')">
|
||||
<b-skeleton v-if="mixingPower === 'loading'" width="80" />
|
||||
<template v-else>
|
||||
{{ mixingPower }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="column is-hash" :data-label="$t('txHash')">
|
||||
<div class="details">
|
||||
<p class="detail">
|
||||
<a class="detail-description" :href="txExplorerUrl(tx.txHash)" target="_blank">
|
||||
{{ tx.txHash }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-status" :data-label="$t('status')">{{ status }}</div>
|
||||
|
||||
<div class="column column-buttons">
|
||||
<b-tooltip :active="activeCopyTooltip" :label="tooltipCopy" position="is-left" multilined>
|
||||
<b-button
|
||||
type="is-primary hide-icon-desktop"
|
||||
size="is-small"
|
||||
icon-left="decrypt"
|
||||
:disabled="disableCopyButton"
|
||||
@click="onCopyAndDecrypt"
|
||||
>
|
||||
{{ $t('decrypt') }}
|
||||
</b-button>
|
||||
</b-tooltip>
|
||||
<b-button type="is-dark" size="is-small" icon-right="remove" @click="onClose" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint-disable no-console */
|
||||
import { decrypt } from 'eth-sig-util'
|
||||
import { mapGetters, mapState, mapActions } from 'vuex'
|
||||
import { toChecksumAddress, isAddress } from 'web3-utils'
|
||||
|
||||
import txStatus from '@/store/txStatus'
|
||||
import NumberFormat from '@/components/NumberFormat'
|
||||
import { sliceAddress, unpackEncryptedMessage } from '@/utils'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NumberFormat
|
||||
},
|
||||
props: {
|
||||
tx: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
time: '',
|
||||
isSpent: false,
|
||||
timer: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('txHashKeeper', ['txExplorerUrl']),
|
||||
...mapGetters('metamask', ['networkConfig', 'nativeCurrency']),
|
||||
...mapGetters('token', ['toDecimals', 'getSymbol']),
|
||||
...mapGetters('encryptedNote', ['isSetupAccount', 'accounts']),
|
||||
...mapState('application', ['statistic']),
|
||||
...mapState('metamask', ['ethAccount']),
|
||||
prefix() {
|
||||
let prefix = this.tx.prefix || ''
|
||||
prefix = prefix.split('-')
|
||||
return { currency: prefix[1], amount: prefix[2] }
|
||||
},
|
||||
activeCopyTooltip() {
|
||||
return !!this.tx.note || !this.isSetupAccount || !this.checkDecryptNote
|
||||
},
|
||||
disableCopyButton() {
|
||||
return !this.tx.note || !this.isSetupAccount || !this.checkDecryptNote
|
||||
},
|
||||
tooltipCopy() {
|
||||
if (!this.checkDecryptNote) {
|
||||
return this.getBackupAccount
|
||||
? this.$t('notDecryptedWithBackup', { address: this.getOwner, backup: this.getBackupAccount })
|
||||
: this.$t('notDecrypted', { address: this.getOwner })
|
||||
}
|
||||
if (!this.isSetupAccount) {
|
||||
return this.$t('pleaseSetUpAccount')
|
||||
}
|
||||
return this.$t('decryptCopyNote')
|
||||
},
|
||||
checkDecryptNote() {
|
||||
if (isAddress(this.ethAccount)) {
|
||||
if (
|
||||
this.checkSumAddress(this.ethAccount) === this.checkSumAddress(this.accounts.backup) &&
|
||||
this.checkSumAddress(this.tx.backupAccount) === this.checkSumAddress(this.ethAccount)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const unpackedMessage = unpackEncryptedMessage(this.tx.note)
|
||||
const result = this.$sessionStorage.getItem(this.accounts.encrypt)
|
||||
|
||||
if (!result) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
decrypt(unpackedMessage, result.data).split('-')
|
||||
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
amount() {
|
||||
if (this.tx.amount === '100000000000000000') {
|
||||
return this.toDecimals(this.tx.amount, 18)
|
||||
}
|
||||
return this.tx.amount
|
||||
},
|
||||
currency() {
|
||||
const { currency } = this.prefix
|
||||
return this.getSymbol(currency || this.tx.currency)
|
||||
},
|
||||
mixingPower() {
|
||||
if (!this.tx.index) {
|
||||
return '-'
|
||||
}
|
||||
if (this.tx.index === 'v1') {
|
||||
return this.$t('v1Deposit')
|
||||
}
|
||||
const { currency, amount } = this.prefix
|
||||
const nextDepositIndex = this.statistic[currency][amount].nextDepositIndex
|
||||
if (this.tx.status === txStatus.waitingForReciept || !nextDepositIndex) {
|
||||
return 'loading'
|
||||
} else if (this.tx.status === txStatus.fail) {
|
||||
return '-'
|
||||
} else {
|
||||
const depositsPast = this.statistic[currency][amount].nextDepositIndex - this.tx.index - 1
|
||||
return this.depositsPastToRender(depositsPast)
|
||||
}
|
||||
},
|
||||
status() {
|
||||
if (this.isWaiting) {
|
||||
return this.$t('waitingForReceipt')
|
||||
}
|
||||
if (this.isFailed) {
|
||||
return this.$t('failed')
|
||||
}
|
||||
if (this.tx.isSpent) {
|
||||
return this.$t('spent')
|
||||
}
|
||||
return this.$t('deposited')
|
||||
},
|
||||
isWaiting() {
|
||||
return this.tx.status === txStatus.waitingForReciept
|
||||
},
|
||||
isFailed() {
|
||||
return this.tx.status === txStatus.fail
|
||||
},
|
||||
getOwner() {
|
||||
return this.tx.owner ? sliceAddress(this.tx.owner) : ''
|
||||
},
|
||||
getBackupAccount() {
|
||||
return this.tx.backupAccount ? sliceAddress(this.tx.backupAccount) : ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.update()
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearTimeout(this.timer)
|
||||
},
|
||||
methods: {
|
||||
...mapActions('encryptedNote', ['decryptNote']),
|
||||
checkSumAddress(address) {
|
||||
return isAddress(address) ? toChecksumAddress(address) : ''
|
||||
},
|
||||
async onCopyAndDecrypt() {
|
||||
const note = await this.decryptNote(this.tx.note)
|
||||
|
||||
if (note) {
|
||||
this.$copyText(`${this.tx.prefix}-${note}`)
|
||||
|
||||
this.$store.dispatch('notice/addNoticeWithInterval', {
|
||||
notice: {
|
||||
title: 'copied',
|
||||
type: 'info'
|
||||
},
|
||||
interval: 2000
|
||||
})
|
||||
}
|
||||
},
|
||||
update() {
|
||||
this.updateTime()
|
||||
|
||||
this.timer = setTimeout(() => {
|
||||
this.update()
|
||||
}, 10000)
|
||||
},
|
||||
onClose() {
|
||||
this.$buefy.dialog.confirm({
|
||||
title: this.$t('removeFromCache'),
|
||||
type: 'is-primary is-outlined',
|
||||
message: this.$t('pleaseMakeSureYouHaveBackedUpYourNote'),
|
||||
cancelText: this.$t('cancelButton'),
|
||||
confirmText: this.$t('remove'),
|
||||
onConfirm: () => {
|
||||
this.$store.dispatch('notice/addNoticeWithInterval', {
|
||||
notice: {
|
||||
title: 'noteHasBeenDeleted',
|
||||
type: 'info'
|
||||
},
|
||||
interval: 2000
|
||||
})
|
||||
this.$store.commit('txHashKeeper/DELETE_TX', { storeType: 'encryptedTxs', txHash: this.tx.txHash })
|
||||
}
|
||||
})
|
||||
},
|
||||
updateTime(t = this.tx.timestamp) {
|
||||
this.time = this.$moment.unix(t).fromNow()
|
||||
},
|
||||
depositsPastToRender(depositsPast) {
|
||||
if (depositsPast < 0) {
|
||||
return 'loading'
|
||||
}
|
||||
|
||||
return this.$tc('userDeposit', depositsPast)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
170
components/Footer.vue
Normal file
170
components/Footer.vue
Normal file
|
@ -0,0 +1,170 @@
|
|||
<template>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item is-column">
|
||||
<div class="level-subitem footer-address">
|
||||
<div class="footer-address__name">
|
||||
{{ $t('donationsAddress') }}
|
||||
</div>
|
||||
<a
|
||||
class="footer-address__value"
|
||||
target="_blank"
|
||||
:href="addressExplorerUrl(donationsAddress)"
|
||||
rel="noreferrer"
|
||||
>{{ donationsAddress }}</a
|
||||
>
|
||||
</div>
|
||||
<div class="level-subitem">
|
||||
Tornado.cash version:
|
||||
<span class="footer-version__value">{{ commit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item is-column">
|
||||
<div class="level-subitem">
|
||||
<div class="buttons">
|
||||
<b-button
|
||||
tag="a"
|
||||
type="is-icon"
|
||||
:href="duneLink"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
icon-right="stats"
|
||||
></b-button>
|
||||
<b-button
|
||||
tag="a"
|
||||
type="is-icon"
|
||||
href="https://torn.community"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
icon-right="discourse"
|
||||
></b-button>
|
||||
<b-button
|
||||
tag="a"
|
||||
type="is-icon"
|
||||
href="https://discord.com/invite/TFDrM8K42j"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
icon-right="discord"
|
||||
></b-button>
|
||||
<b-button
|
||||
tag="a"
|
||||
type="is-icon"
|
||||
href="https://tornado-cash.medium.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
icon-right="medium"
|
||||
></b-button>
|
||||
<b-button
|
||||
tag="a"
|
||||
type="is-icon"
|
||||
href="https://twitter.com/TornadoCash"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
icon-right="twitter"
|
||||
></b-button>
|
||||
<b-button
|
||||
tag="a"
|
||||
type="is-icon"
|
||||
href="https://t.me/TornadoCashOfficial"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
icon-right="telegram"
|
||||
></b-button>
|
||||
<b-button
|
||||
tag="a"
|
||||
type="is-icon"
|
||||
href="https://github.com/tornadocash"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
icon-right="github"
|
||||
></b-button>
|
||||
<div class="break"></div>
|
||||
<b-dropdown
|
||||
v-model="$i18n.locale"
|
||||
class="dropdown-langs"
|
||||
position="is-top-left"
|
||||
aria-role="list"
|
||||
@change="langChange"
|
||||
>
|
||||
<b-button slot="trigger" type="is-icon">
|
||||
<FlagIcon :code="$i18n.locale" :class="'is-active-locale-' + $i18n.locale" />
|
||||
</b-button>
|
||||
|
||||
<b-dropdown-item
|
||||
v-for="locale in locales"
|
||||
:key="locale"
|
||||
:value="locale"
|
||||
aria-role="listitem"
|
||||
>
|
||||
<FlagIcon :code="locale" />
|
||||
{{ printLang(locale) }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import { FlagIcon } from '@/components/icons'
|
||||
import { LOCALES_NAMES, DONATIONS_ADDRESS } from '@/constants'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FlagIcon
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
commit: process.env.commit,
|
||||
donationsAddress: DONATIONS_ADDRESS
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('metamask', ['networkConfig', 'netId']),
|
||||
...mapGetters('txHashKeeper', ['addressExplorerUrl']),
|
||||
duneLink() {
|
||||
const mainnetNetworks = [1, 5]
|
||||
|
||||
if (mainnetNetworks.includes(Number(this.netId))) {
|
||||
return 'https://dune.xyz/poma/tornado-cash_1'
|
||||
}
|
||||
|
||||
return 'https://dune.xyz/fennec/Tornado-Cash-Cross-chain-Dashboard'
|
||||
},
|
||||
locales() {
|
||||
return this.$i18n.availableLocales
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
langChange(lang) {
|
||||
localStorage.setItem('lang', lang)
|
||||
|
||||
if (lang === 'zh') {
|
||||
lang += '-cn'
|
||||
}
|
||||
|
||||
this.$moment.locale(lang)
|
||||
this.$numbro.setLanguage(LOCALES_NAMES[lang])
|
||||
},
|
||||
printLang(lang) {
|
||||
let code = lang
|
||||
switch (code) {
|
||||
case 'zh':
|
||||
code = 'cn'
|
||||
break
|
||||
}
|
||||
return code.toUpperCase()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
132
components/GasPriceSlider.vue
Normal file
132
components/GasPriceSlider.vue
Normal file
|
@ -0,0 +1,132 @@
|
|||
<template>
|
||||
<div class="field field-slider">
|
||||
<div class="label">
|
||||
Gas Price
|
||||
<b-field :class="hasError ? 'is-warning' : ''">
|
||||
<b-input
|
||||
ref="gasPriceInput"
|
||||
v-model.number="input"
|
||||
type="number"
|
||||
:min="0"
|
||||
custom-class="hide-spinner"
|
||||
:use-html5-validation="false"
|
||||
expanded
|
||||
:style="{ width: reactiveWidth }"
|
||||
></b-input>
|
||||
<div class="control has-text" @click="$refs.gasPriceInput.focus()">
|
||||
<span>Gwei</span>
|
||||
</div>
|
||||
</b-field>
|
||||
</div>
|
||||
<b-slider
|
||||
v-model="slider"
|
||||
:min="0"
|
||||
:max="2"
|
||||
:custom-formatter="tooltipFormat"
|
||||
bigger-slider-focus
|
||||
lazy
|
||||
rounded
|
||||
@change="onChange"
|
||||
>
|
||||
<template v-for="(val, index) of Object.keys(gasPrices).filter((k) => k !== 'instant')">
|
||||
<b-slider-tick :key="val" :value="index">{{ $t(`gasPriceSlider.${val}`) }}</b-slider-tick>
|
||||
</template>
|
||||
</b-slider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { debounce } from '@/utils'
|
||||
const { toWei, toHex } = require('web3-utils')
|
||||
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
data() {
|
||||
return {
|
||||
input: null,
|
||||
slider: 2,
|
||||
hasError: false,
|
||||
reactiveWidth: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('gasPrices', ['gasPrices'])
|
||||
},
|
||||
watch: {
|
||||
input: {
|
||||
handler(gasPrice) {
|
||||
this.setWidth(gasPrice)
|
||||
debounce(this.validateGasPrice, gasPrice)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.input = this.gasPrices.fast
|
||||
this.setWidth(this.input)
|
||||
},
|
||||
methods: {
|
||||
onChange(index) {
|
||||
this.input = this.getValueFromIndex(index)
|
||||
},
|
||||
tooltipFormat(index) {
|
||||
return this.getValueFromIndex(index)
|
||||
},
|
||||
getValueFromIndex(index) {
|
||||
switch (Number(index)) {
|
||||
case 2:
|
||||
return this.gasPrices.fast
|
||||
case 1:
|
||||
return this.gasPrices.standard
|
||||
case 0:
|
||||
return this.gasPrices.low
|
||||
}
|
||||
},
|
||||
validateGasPrice(gasPrice) {
|
||||
try {
|
||||
let speed = ''
|
||||
this.hasError = false
|
||||
|
||||
if (gasPrice === '') {
|
||||
throw new Error('must not be an empty')
|
||||
}
|
||||
if (gasPrice <= 0) {
|
||||
throw new Error('must be greater than zero')
|
||||
}
|
||||
|
||||
if (gasPrice < this.gasPrices.standard) {
|
||||
speed = 'low'
|
||||
this.slider = 0
|
||||
} else if (gasPrice === this.gasPrices.standard) {
|
||||
this.slider = 1
|
||||
speed = 'standard'
|
||||
} else {
|
||||
this.slider = 2
|
||||
speed = 'fast'
|
||||
}
|
||||
|
||||
if (gasPrice) {
|
||||
gasPrice = {
|
||||
speed,
|
||||
value: toHex(toWei(gasPrice.toString(), 'gwei'))
|
||||
}
|
||||
}
|
||||
this.$emit('input', gasPrice)
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Invalid gas price:', e.message)
|
||||
this.hasError = true
|
||||
} finally {
|
||||
this.$emit('validate', !this.hasError)
|
||||
}
|
||||
},
|
||||
setWidth(value) {
|
||||
if (!value) {
|
||||
value = 0
|
||||
}
|
||||
|
||||
this.reactiveWidth = `${Math.min(75, Math.max(35, 19 + value && value.toString().length * 12))}px`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
113
components/Job.vue
Normal file
113
components/Job.vue
Normal file
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<div class="box box-tx is-waiting" :class="{ 'is-danger': isFailed }">
|
||||
<div class="columns is-vcentered">
|
||||
<div class="column is-time" :data-label="$t('timePassed')">{{ time }}</div>
|
||||
<div class="column is-amount" :data-label="$t('amount')">
|
||||
{{ job.amount }}
|
||||
{{ currency }}
|
||||
</div>
|
||||
<div class="column is-deposit" :data-label="$t('subsequentDeposits')">
|
||||
<b-skeleton v-if="!isFailed" width="80" />
|
||||
<template v-else>-</template>
|
||||
</div>
|
||||
<div class="column is-hash" :data-label="$t('txHash')">
|
||||
<b-skeleton v-if="!job.txHash && job.status !== 'FAILED'" />
|
||||
<div v-else class="details">
|
||||
<p class="detail">
|
||||
<a class="detail-description" :href="txExplorerUrl(job.txHash)" target="_blank">
|
||||
{{ job.txHash }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-status" :data-label="$t('status')">
|
||||
<b-skeleton v-if="!job.status" width="60" />
|
||||
<template v-else>
|
||||
<div class="status-with-loading">
|
||||
{{ status }}
|
||||
<b-icon v-show="!isFailed" icon="loading" size="is-small" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="column column-buttons">
|
||||
<b-button type="is-primary" size="is-small" icon-left="copy" disabled>
|
||||
{{ $t('note') }}
|
||||
</b-button>
|
||||
<b-button
|
||||
type="is-dark"
|
||||
size="is-small"
|
||||
icon-right="remove"
|
||||
:disabled="!isFailed"
|
||||
@click="onDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
export default {
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
timer: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
time: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('txHashKeeper', ['txExplorerUrl']),
|
||||
...mapGetters('metamask', ['netId']),
|
||||
...mapGetters('token', ['getSymbol']),
|
||||
isFailed() {
|
||||
return this.job.status === 'FAILED'
|
||||
},
|
||||
status() {
|
||||
switch (this.job.status) {
|
||||
case 'ACCEPTED':
|
||||
return this.$t('accepted')
|
||||
case 'SENT':
|
||||
return this.$t('sent')
|
||||
case 'MINED':
|
||||
return this.$t('mined')
|
||||
case 'CONFIRMED':
|
||||
return this.$t('confirmed')
|
||||
case 'FAILED':
|
||||
return this.$t('failed')
|
||||
case 'QUEUED':
|
||||
return this.$t('queued')
|
||||
default:
|
||||
return this.job.status
|
||||
}
|
||||
},
|
||||
currency() {
|
||||
return this.getSymbol(this.job.currency)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.update()
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearTimeout(this.timer)
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
this.updateTime()
|
||||
|
||||
this.timer = setTimeout(() => {
|
||||
this.update()
|
||||
}, 10000)
|
||||
},
|
||||
onDelete() {
|
||||
this.$store.commit('relayer/DELETE_JOB', { id: this.job.id, type: 'tornado', netId: this.netId })
|
||||
},
|
||||
updateTime(t = this.job.timestamp) {
|
||||
this.time = this.$moment.unix(t).fromNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
48
components/Loaders/ApproveLoader.vue
Normal file
48
components/Loaders/ApproveLoader.vue
Normal file
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<div v-if="isReconnectButtonShow" class="loading-alert">
|
||||
{{ $t('mobileWallet.loading.alert') }}
|
||||
<b-button type="is-primary" size="small" class="max-content is-outlined" @click="onReconnect">
|
||||
{{ $t('mobileWallet.loading.action') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapState, mapGetters, mapActions } from 'vuex'
|
||||
import { SECOND } from '@/constants'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isReconnectButtonShow: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('metamask', ['isWalletConnect', 'netId']),
|
||||
...mapState('loading', ['enabled'])
|
||||
},
|
||||
mounted() {
|
||||
this.onClearData()
|
||||
|
||||
if (this.isWalletConnect) {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.isReconnectButtonShow = true
|
||||
}, SECOND * 20)
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.onClearData()
|
||||
},
|
||||
methods: {
|
||||
...mapActions('metamask', ['mobileWalletReconnect']),
|
||||
async onReconnect() {
|
||||
await this.mobileWalletReconnect(this.netId)
|
||||
},
|
||||
onClearData() {
|
||||
if (this.timeout) {
|
||||
this.isReconnectButtonShow = false
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
28
components/Loaders/Loader.vue
Normal file
28
components/Loaders/Loader.vue
Normal file
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<b-loading v-model="enabled">
|
||||
<div class="loading-container">
|
||||
<div class="loading-tornado"></div>
|
||||
<div class="loading-message">{{ message }}...</div>
|
||||
<approve-loader v-if="isApprove" />
|
||||
</div>
|
||||
</b-loading>
|
||||
</template>
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
|
||||
import ApproveLoader from './ApproveLoader'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ApproveLoader
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('metamask', ['isWalletConnect']),
|
||||
...mapState('metamask', ['providerName']),
|
||||
...mapState('loading', ['enabled', 'message', 'type']),
|
||||
isApprove() {
|
||||
return this.type === 'approve'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
9
components/Logo.vue
Normal file
9
components/Logo.vue
Normal file
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="logo" viewBox="0 0 155 40">
|
||||
<path
|
||||
fill="#94febf"
|
||||
fill-rule="evenodd"
|
||||
d="M29.4,8.6A11.5,11.5,0,0,1,39.9,19.4,17.1,17.1,0,0,0,8.5,10.5,11.5,11.5,0,0,1,19.3,0a17.2,17.2,0,0,0-8.9,31.4A11.5,11.5,0,0,1,0,20.6a17.1,17.1,0,0,0,31.3,9.1A11.4,11.4,0,0,1,20.6,40,17.1,17.1,0,0,0,29.4,8.6ZM19.9,27.2a6.9,6.9,0,0,1-5-2.1,7.1,7.1,0,1,1,5,2.1Zm36.6-9.7v7.4c0,1.3.5,1.6,1.8,1.6a4.8,4.8,0,0,0,1.8-.3v2.9a6.4,6.4,0,0,1-2.4.5c-3,0-4.8-1.9-4.8-5V17.5H50.4V14.4h2.5V9.9h3.6v4.5h3.6v3.1ZM77,21.9c0,5-2.5,7.7-7,7.7s-7.1-2.7-7.1-7.7,2.5-7.7,7.1-7.7S77,16.9,77,21.9Zm-3.6,0c0-4-1.3-4.8-3.4-4.8s-3.5.8-3.5,4.8,1.3,4.7,3.5,4.7S73.4,25.8,73.4,21.9ZM88,14.1v3.2a11.5,11.5,0,0,0-3.8.5V29.4H80.6V15.7A20.4,20.4,0,0,1,88,14.1Zm16.9,6.3v9h-3.6V20.1c0-1.4-.2-2.8-3.5-2.8a12,12,0,0,0-2.8.3V29.4H91.4V15.8a15,15,0,0,1,6.9-1.7C102.4,14.1,104.9,16.5,104.9,20.4Zm16.2.1v4.1c0,2.4-1.6,5-6.3,5s-6.4-2.6-6.4-5.1.5-4.7,6.5-4.7a11,11,0,0,1,2.6.2c0-1.7-.8-2.9-3.2-2.9a11.2,11.2,0,0,0-4.4.8V15a12.5,12.5,0,0,1,4.6-.9C118.8,14.1,121.1,16.4,121.1,20.5Zm-3.6,1.7h-2.1c-2.5,0-3.4.7-3.4,1.9s.9,2.4,2.8,2.4,2.7-.9,2.7-2.3Zm13.9-8.1,2.5.3V8.7h3.6V24.6c0,2.4-1.7,5-6.4,5-2,0-6.7-.8-6.7-7.9C124.4,16.8,126.9,14.1,131.4,14.1Zm-.3,12.4c1.6,0,2.8-.4,2.8-1.6V17.2l-2.4-.2c-2.1,0-3.5,1-3.5,4.5S128.8,26.5,131.1,26.5ZM155,21.9c0,5-2.5,7.7-7.1,7.7s-7.1-2.7-7.1-7.7,2.5-7.7,7.1-7.7S155,16.9,155,21.9Zm-3.6,0c0-4-1.4-4.8-3.5-4.8s-3.5.8-3.5,4.8,1.4,4.7,3.5,4.7S151.4,25.8,151.4,21.9Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
64
components/MetamaskNavbarIcon.vue
Normal file
64
components/MetamaskNavbarIcon.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<b-tooltip position="is-bottom" type="is-dark-tooltip" :triggers="[]">
|
||||
<template v-slot:content>
|
||||
<template v-if="isLoggedIn">
|
||||
<p>{{ $t('web3connected') }}</p>
|
||||
<a :href="addressExplorerUrl(ethAccount)" target="_blank">{{ shortAddress(ethAccount) }}</a>
|
||||
<p><NumberFormat :value="balance" /> {{ currency }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>{{ $t('notConnected') }}</p>
|
||||
<connect-button type="is-primary-link mb-0" />
|
||||
</template>
|
||||
</template>
|
||||
<b-button type="is-nav-icon" :icon-left="wallet" :class="{ [wallet]: isLoggedIn }"></b-button>
|
||||
</b-tooltip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
|
||||
import { sliceAddress } from '@/utils'
|
||||
import NumberFormat from '@/components/NumberFormat'
|
||||
import { ConnectButton } from '@/components/web3Connect'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NumberFormat,
|
||||
ConnectButton
|
||||
},
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isActive: false,
|
||||
timer: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('metamask', ['ethAccount', 'ethBalance', 'providerName']),
|
||||
...mapGetters('metamask', ['isLoggedIn', 'currency']),
|
||||
...mapGetters('token', ['toDecimals']),
|
||||
...mapGetters('txHashKeeper', ['addressExplorerUrl']),
|
||||
balance() {
|
||||
return this.toDecimals(this.ethBalance)
|
||||
},
|
||||
wallet() {
|
||||
const supportedWallets = ['metamask', 'walletConnect']
|
||||
|
||||
if (supportedWallets.includes(this.providerName)) {
|
||||
return this.providerName
|
||||
}
|
||||
return 'metamask'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
shortAddress(address) {
|
||||
return sliceAddress(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
71
components/Navbar.vue
Normal file
71
components/Navbar.vue
Normal file
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<b-navbar wrapper-class="container" class="header">
|
||||
<template slot="brand">
|
||||
<b-navbar-item tag="router-link" to="/" active-class="">
|
||||
<Logo />
|
||||
</b-navbar-item>
|
||||
</template>
|
||||
<template slot="start">
|
||||
<b-navbar-item
|
||||
v-if="isEnabledGovernance"
|
||||
tag="router-link"
|
||||
to="/governance"
|
||||
:active="$route.path.includes('governance')"
|
||||
class="has-tag"
|
||||
>
|
||||
{{ $t('governance') }} <span v-if="hasActiveProposals" class="navbar-item--tag"></span>
|
||||
</b-navbar-item>
|
||||
<b-navbar-item tag="router-link" to="/compliance">
|
||||
{{ $t('compliance') }}
|
||||
</b-navbar-item>
|
||||
<b-navbar-item href="http://docs.tornado.cash" target="_blank" rel="noreferrer" class="has-tag">
|
||||
<b-icon icon="open-book" size="is-small" class="mr-1" />
|
||||
<span>{{ $t('docs') }}</span>
|
||||
</b-navbar-item>
|
||||
</template>
|
||||
<template slot="end">
|
||||
<b-navbar-item tag="div">
|
||||
<div class="buttons">
|
||||
<network-navbar-icon />
|
||||
<metamask-navbar-icon />
|
||||
<indicator />
|
||||
<b-button icon-left="settings" type="is-primary" outlined @mousedown.prevent @click="onAccount">
|
||||
{{ $t('settings') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-navbar-item>
|
||||
</template>
|
||||
</b-navbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import Logo from '@/components/Logo'
|
||||
import { Indicator } from '@/modules/account'
|
||||
import MetamaskNavbarIcon from '@/components/MetamaskNavbarIcon'
|
||||
import NetworkNavbarIcon from '@/components/NetworkNavbarIcon'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Logo,
|
||||
Indicator,
|
||||
NetworkNavbarIcon,
|
||||
MetamaskNavbarIcon
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isActive: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('metamask', ['netId', 'isLoggedIn']),
|
||||
...mapGetters('governance/gov', ['isEnabledGovernance']),
|
||||
...mapState('governance/gov', ['hasActiveProposals'])
|
||||
},
|
||||
methods: {
|
||||
onAccount() {
|
||||
this.$router.push('/account')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
147
components/NetworkModal.vue
Normal file
147
components/NetworkModal.vue
Normal file
|
@ -0,0 +1,147 @@
|
|||
<template>
|
||||
<div class="modal-card box box-modal is-wallet-modal">
|
||||
<header class="box-modal-header is-spaced">
|
||||
<p class="box-modal-title has-text-centered">{{ $t('changeNetwork') }}</p>
|
||||
<button class="delete" type="button" @click="$emit('close')" />
|
||||
</header>
|
||||
<div class="networks">
|
||||
<div
|
||||
v-for="{ name, chainId } in networks"
|
||||
:key="chainId"
|
||||
class="item"
|
||||
:class="{ 'is-active': chainId === netId }"
|
||||
@click="setNetwork(chainId)"
|
||||
>
|
||||
<b-icon class="network-icon" :icon="`${name}`.replace(/\)?\s\(?/g, '-').toLowerCase()" />
|
||||
<b>{{ name }}</b>
|
||||
<span class="network-checkbox"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapState, mapActions, mapGetters } from 'vuex'
|
||||
import config from '@/networkConfig'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
networkConfig: config
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('metamask', ['netId', 'isLoggedIn']),
|
||||
...mapState('metamask', ['isInitialized']),
|
||||
networks() {
|
||||
return Object.keys(this.networkConfig).map((key) => {
|
||||
return {
|
||||
name: this.networkConfig[key].networkName,
|
||||
chainId: Number(key.replace('netId', ''))
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('metamask', ['networkChangeHandler']),
|
||||
...mapActions('loading', ['enable', 'disable']),
|
||||
async setNetwork(netId) {
|
||||
this.enable({ message: this.$t('changingNetwork') })
|
||||
|
||||
await this.sleep()
|
||||
|
||||
try {
|
||||
const providerName = window.localStorage.getItem('provider')
|
||||
const isSupport = this.checkSupportNetwork(netId)
|
||||
|
||||
if (isSupport) {
|
||||
await this.networkChangeHandler({ netId })
|
||||
|
||||
if (!providerName) {
|
||||
this.$router.go(0)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`setNetwork has error ${err.message}`)
|
||||
} finally {
|
||||
this.$emit('close')
|
||||
this.disable()
|
||||
}
|
||||
},
|
||||
sleep() {
|
||||
return new Promise((resolve) =>
|
||||
setTimeout(() => {
|
||||
resolve()
|
||||
}, 800)
|
||||
)
|
||||
},
|
||||
checkSupportNetwork(netId) {
|
||||
const isSupport = Object.keys(this.networkConfig).includes(`netId${netId}`)
|
||||
|
||||
if (!isSupport) {
|
||||
this.$buefy.snackbar.open({
|
||||
message: this.$i18n.t('currentNetworkIsNotSupported'),
|
||||
type: 'is-primary',
|
||||
position: 'is-top',
|
||||
indefinite: true,
|
||||
actionText: 'Ok'
|
||||
})
|
||||
}
|
||||
|
||||
return isSupport
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.box-modal {
|
||||
max-width: 300px;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
.item {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
margin: 14px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.929rem;
|
||||
|
||||
b {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.network-icon {
|
||||
margin-right: 12px;
|
||||
|
||||
::v-deep .trnd {
|
||||
height: 1.571rem;
|
||||
width: 1.571rem;
|
||||
}
|
||||
}
|
||||
|
||||
.network-checkbox {
|
||||
margin-left: auto;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
border-radius: 100%;
|
||||
border: 1px solid #6b6b6b;
|
||||
transition: border-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
.network-checkbox {
|
||||
border-color: #94febf;
|
||||
background-color: #94febf;
|
||||
background-image: url('../assets/img/icons/checkbox.svg');
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.network-checkbox {
|
||||
border-color: #94febf;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
73
components/NetworkNavbarIcon.vue
Normal file
73
components/NetworkNavbarIcon.vue
Normal file
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<b-button :icon-left="iconName" class="network-button" @click="onClick">{{ shortNetworkName }}</b-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import NetworkModal from '@/components/NetworkModal'
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters('metamask', ['networkName', 'netId']),
|
||||
iconName() {
|
||||
return `${this.networkName}`.replace(/\)?\s\(?/g, '-').toLowerCase()
|
||||
},
|
||||
shortNetworkName() {
|
||||
switch (this.netId) {
|
||||
case 1:
|
||||
return 'Ethereum'
|
||||
case 5:
|
||||
return 'Goerli'
|
||||
case 56:
|
||||
return 'BSC Mainnet'
|
||||
case 137:
|
||||
return 'Polygon Network'
|
||||
case 42161:
|
||||
return 'Arbitrum'
|
||||
case 43114:
|
||||
return 'Avalanche'
|
||||
default:
|
||||
return this.networkName
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$buefy.modal.open({
|
||||
parent: this,
|
||||
component: NetworkModal,
|
||||
hasModalCard: true,
|
||||
width: 440
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.network-button {
|
||||
padding-left: 0;
|
||||
border: 0;
|
||||
background-color: #242424;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: #393939;
|
||||
}
|
||||
|
||||
::v-deep .icon {
|
||||
height: 2.857em;
|
||||
width: 2.857em;
|
||||
background-color: #0f1f19;
|
||||
border: 1px solid #94febf;
|
||||
border-radius: 4px;
|
||||
|
||||
&:first-child:not(:last-child) {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.trnd {
|
||||
background-color: #94febf;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
45
components/NetworkSelect.vue
Normal file
45
components/NetworkSelect.vue
Normal file
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<b-select
|
||||
v-model="selectedNetwork"
|
||||
class="network-select"
|
||||
placeholder="Select a network"
|
||||
size="is-small"
|
||||
expanded
|
||||
@input="updateNetwork()"
|
||||
>
|
||||
<option v-for="network in networks" :key="network.networkName" :value="network.networkName.toLowerCase()">
|
||||
{{ network.networkName }}
|
||||
</option>
|
||||
</b-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import networkConfig from '@/networkConfig'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedNetwork: this.value,
|
||||
networkConfig
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
networks() {
|
||||
const networkConfig = Object.assign({}, this.networkConfig)
|
||||
delete networkConfig.netId333
|
||||
return networkConfig
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateNetwork() {
|
||||
this.$emit('input', this.selectedNetwork)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
54
components/Notices.vue
Normal file
54
components/Notices.vue
Normal file
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<div class="notices is-top">
|
||||
<b-notification
|
||||
v-for="notice in notices"
|
||||
v-show="notice.isShowed"
|
||||
:key="notice.id"
|
||||
class="is-top-right"
|
||||
has-icon
|
||||
:icon="notice.type"
|
||||
:aria-close-label="$t('closeNotification')"
|
||||
role="alert"
|
||||
@close="close(notice.id)"
|
||||
>
|
||||
<span v-if="notice.untranslatedTitle">{{ notice.untranslatedTitle }}</span>
|
||||
<i18n v-else :path="notice.title.path || notice.title" tag="span">
|
||||
<template v-slot:value>
|
||||
<b><number-format :value="notice.title.amount" /> {{ getSymbol(notice.title.currency) }}</b>
|
||||
</template>
|
||||
<template v-slot:description>{{ notice.description }}</template>
|
||||
</i18n>
|
||||
<a v-if="notice.nova" href="https://nova.tornadocash.eth.link" target="_blank">
|
||||
Tornado Cash Nova
|
||||
</a>
|
||||
<a v-if="notice.txHash" :href="txExplorerUrl(notice.txHash)" target="_blank">
|
||||
{{ $t('viewOnEtherscan') }}
|
||||
</a>
|
||||
<n-link v-else-if="notice.routerLink" v-bind="notice.routerLink.params" @onClick="$forceUpdate()">
|
||||
{{ $t(notice.routerLink.title) }}
|
||||
</n-link>
|
||||
</b-notification>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters, mapActions } from 'vuex'
|
||||
import NumberFormat from '@/components/NumberFormat'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NumberFormat
|
||||
},
|
||||
computed: {
|
||||
...mapState('notice', ['notices']),
|
||||
...mapGetters('txHashKeeper', ['txExplorerUrl']),
|
||||
...mapGetters('token', ['getSymbol'])
|
||||
},
|
||||
methods: {
|
||||
...mapActions('notice', ['showNotice']),
|
||||
close(id) {
|
||||
this.showNotice({ id, isShowed: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
57
components/NumberFormat.vue
Normal file
57
components/NumberFormat.vue
Normal file
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<span class="is-uppercase">{{ num }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ROUNDING_PRECISION } from '@/constants'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
},
|
||||
format: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
average: true,
|
||||
mantissa: 5,
|
||||
trimMantissa: true,
|
||||
totalLength: 5,
|
||||
lowPrecision: false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
num: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$i18n.locale'(lang, oldLang) {
|
||||
if (lang !== oldLang) {
|
||||
this.render()
|
||||
}
|
||||
},
|
||||
value(value, oldvalue) {
|
||||
if (value !== oldvalue) {
|
||||
this.render()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.render()
|
||||
},
|
||||
methods: {
|
||||
render() {
|
||||
if (Number(this.value) <= Number(ROUNDING_PRECISION) && Number(this.value) > 0) {
|
||||
this.num = '~0.0001'
|
||||
return
|
||||
}
|
||||
this.num = this.$numbro(this.value).format(this.format)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
158
components/Settings.vue
Normal file
158
components/Settings.vue
Normal file
|
@ -0,0 +1,158 @@
|
|||
<template>
|
||||
<div class="modal-card box box-modal">
|
||||
<header class="box-modal-header">
|
||||
<div class="box-modal-title">{{ $t('settings') }}</div>
|
||||
<button type="button" class="delete" @click="$parent.cancel('escape')" />
|
||||
</header>
|
||||
<div class="field">
|
||||
<b-field :label="$t('rpc')" class="has-custom-field">
|
||||
<b-dropdown v-model="selectedRpc" expanded aria-role="list">
|
||||
<div slot="trigger" class="control" :class="{ 'is-loading': checkingRpc && !isCustomRpc }">
|
||||
<div class="input">
|
||||
<span>{{ isCustomRpc ? $t('customRpc') : selectedRpc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<b-dropdown-item
|
||||
v-for="{ name, url } in Object.values(networkConfig.rpcUrls)"
|
||||
:key="name"
|
||||
:value="name"
|
||||
aria-role="listitem"
|
||||
@click="checkRpc({ name, url })"
|
||||
>
|
||||
{{ name }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item value="custom" aria-role="listitem" @click="checkRpc({ name: 'custom' })">
|
||||
{{ $t('customRpc') }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</b-field>
|
||||
<div v-if="isCustomRpc" class="field has-custom-field">
|
||||
<b-input
|
||||
ref="customInput"
|
||||
v-model="customRpcUrl"
|
||||
type="url"
|
||||
:placeholder="$t('customRpcPlaceholder')"
|
||||
:custom-class="hasErrorRpc.type"
|
||||
:use-html5-validation="false"
|
||||
@input="checkCustomRpc"
|
||||
></b-input>
|
||||
</div>
|
||||
<p v-if="hasErrorRpc.msg" class="help" :class="hasErrorRpc.type">
|
||||
{{ hasErrorRpc.msg }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="buttons buttons__halfwidth">
|
||||
<b-button type="is-primary" outlined @mousedown.prevent @click="onReset">
|
||||
{{ $t('reset') }}
|
||||
</b-button>
|
||||
<b-button type="is-primary" :disabled="isDisabledSave" @click="onSave">
|
||||
{{ $t('save') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint-disable no-console */
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
|
||||
import { debounce } from '@/utils'
|
||||
import networkConfig from '@/networkConfig'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
netId: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
checkingRpc: false,
|
||||
hasErrorRpc: { type: '', msg: '' },
|
||||
customRpcUrl: '',
|
||||
selectedRpc: 'custom',
|
||||
rpc: { name: 'custom', url: '' }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('settings', ['getRpc']),
|
||||
networkConfig() {
|
||||
return networkConfig[`netId${this.netId}`]
|
||||
},
|
||||
isCustomRpc() {
|
||||
return this.selectedRpc === 'custom'
|
||||
},
|
||||
isDisabledSave() {
|
||||
return (
|
||||
this.hasErrorRpc.type === 'is-warning' || this.checkingRpc || (this.isCustomRpc && !this.customRpcUrl)
|
||||
)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.rpc = this.getRpc(this.netId)
|
||||
this.selectedRpc = this.rpc.name
|
||||
|
||||
if (this.selectedRpc === 'custom') {
|
||||
this.customRpcUrl = this.rpc.url
|
||||
}
|
||||
|
||||
this.checkRpc(this.rpc)
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('settings', ['SAVE_RPC']),
|
||||
onReset() {
|
||||
this.checkingRpc = false
|
||||
this.hasErrorRpc = { type: '', msg: '' }
|
||||
|
||||
this.rpc = Object.entries(this.networkConfig.rpcUrls)[0][1]
|
||||
this.selectedRpc = this.rpc.name
|
||||
this.checkRpc(this.rpc)
|
||||
},
|
||||
onSave() {
|
||||
this.SAVE_RPC({ ...this.rpc, netId: this.netId })
|
||||
this.$emit('close')
|
||||
},
|
||||
onCancel() {
|
||||
this.$emit('cancel')
|
||||
},
|
||||
checkRpc({ name, url = '' }) {
|
||||
if (name === 'custom') {
|
||||
this.customRpcUrl = ''
|
||||
this.hasErrorRpc = { type: '', msg: '' }
|
||||
this.checkingRpc = true
|
||||
return
|
||||
}
|
||||
|
||||
this._checkRpc({ name, url })
|
||||
},
|
||||
checkCustomRpc(url) {
|
||||
const trimmedUrl = url.trim()
|
||||
if (!trimmedUrl) {
|
||||
this.hasErrorRpc = { type: '', msg: '' }
|
||||
return
|
||||
}
|
||||
debounce(this._checkRpc, { name: 'custom', url: trimmedUrl })
|
||||
},
|
||||
async _checkRpc({ name, url }) {
|
||||
this.checkingRpc = true
|
||||
this.hasErrorRpc = { type: '', msg: '' }
|
||||
|
||||
const { isValid, error } = await this.$store.dispatch('settings/checkRpc', {
|
||||
url,
|
||||
netId: this.netId
|
||||
})
|
||||
|
||||
if (isValid) {
|
||||
this.hasErrorRpc.type = 'is-primary'
|
||||
this.hasErrorRpc.msg = this.$t('rpcStatusOk')
|
||||
this.rpc = { name, url }
|
||||
} else {
|
||||
this.hasErrorRpc.type = 'is-warning'
|
||||
this.hasErrorRpc.msg = error
|
||||
}
|
||||
|
||||
this.checkingRpc = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
113
components/Statistics.vue
Normal file
113
components/Statistics.vue
Normal file
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<div class="column is-half">
|
||||
<div class="box-stats">
|
||||
<div class="tab-with-corner is-left-top">
|
||||
{{ $t('statistics') }}
|
||||
<span class="selected"
|
||||
><NumberFormat :value="selectedStatistic.amount" /> {{ selectedStatisticCurrency }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="label">
|
||||
{{ $t('anonymitySet') }}
|
||||
<b-tooltip :label="$t('anonymitySetTooltip')" size="is-medium" position="is-top" multilined>
|
||||
<button class="button is-primary has-icon">
|
||||
<span class="icon icon-info"></span>
|
||||
</button>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
<div class="field">
|
||||
<i18n v-if="anonimitySet" path="equalUserDeposit">
|
||||
<span v-if="anonimitySet > 1 && anonimitySet < 5" slot="only">{{ $t('only') }}</span>
|
||||
<b v-if="anonimitySet > 1" slot="n">{{ Number(anonimitySet) }}</b>
|
||||
<span slot="equalUserDepositText">{{ $tc('equalUserDepositPlural', Number(anonimitySet)) }}</span>
|
||||
</i18n>
|
||||
<b-skeleton size="is-large" :active="!anonimitySet" width="200"></b-skeleton>
|
||||
</div>
|
||||
<template v-if="anonimitySet != 0">
|
||||
<div class="label">{{ $t('latestDeposits') }}</div>
|
||||
<div v-if="latestDeposits && latestDeposits.length" class="columns is-small is-multiline">
|
||||
<div class="column is-half-small">
|
||||
<div class="deposits">
|
||||
<div v-for="{ index, depositTime } in latestDeposits.slice(0, 5)" :key="index" class="row">
|
||||
<div class="value">{{ Number(index) + 1 }}.</div>
|
||||
<div class="data">{{ depositTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half-small">
|
||||
<div class="deposits">
|
||||
<div v-for="{ index, depositTime } in latestDeposits.slice(5, 10)" :key="index" class="row">
|
||||
<div class="value">{{ Number(index) + 1 }}.</div>
|
||||
<div class="data">{{ depositTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="columns is-small is-multiline">
|
||||
<div class="column is-half-small">
|
||||
<div class="deposits">
|
||||
<b-skeleton size="is-large"></b-skeleton>
|
||||
<b-skeleton size="is-large"></b-skeleton>
|
||||
<b-skeleton size="is-large"></b-skeleton>
|
||||
<b-skeleton size="is-large"></b-skeleton>
|
||||
<b-skeleton size="is-large"></b-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half-small">
|
||||
<div class="deposits">
|
||||
<b-skeleton size="is-large"></b-skeleton>
|
||||
<b-skeleton size="is-large"></b-skeleton>
|
||||
<b-skeleton size="is-large"></b-skeleton>
|
||||
<b-skeleton size="is-large"></b-skeleton>
|
||||
<b-skeleton size="is-large"></b-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint-disable no-console */
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import NumberFormat from '@/components/NumberFormat'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NumberFormat
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
timer: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('application', ['ip', 'statistic', 'selectedStatistic']),
|
||||
...mapGetters('metamask', ['networkConfig']),
|
||||
...mapGetters('application', ['selectedStatisticCurrency', 'latestDeposits']),
|
||||
anonimitySet() {
|
||||
const currency = this.selectedStatistic.currency.toLowerCase()
|
||||
return this.statistic[currency][this.selectedStatistic.amount].anonymitySet
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.timer) {
|
||||
this.updateEvents()
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearTimeout(this.timer)
|
||||
},
|
||||
methods: {
|
||||
updateEvents() {
|
||||
this.$store.dispatch('application/updateSelectEvents')
|
||||
|
||||
this.timer = setTimeout(() => {
|
||||
this.updateEvents()
|
||||
}, 60 * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
184
components/Tx.vue
Normal file
184
components/Tx.vue
Normal file
|
@ -0,0 +1,184 @@
|
|||
<template>
|
||||
<div
|
||||
class="box box-tx"
|
||||
:class="{
|
||||
'is-waiting': isWaiting,
|
||||
'is-danger': isFailed,
|
||||
'is-spent': tx.isSpent
|
||||
}"
|
||||
>
|
||||
<div class="columns is-vcentered">
|
||||
<div class="column is-time" :data-label="$t('timePassed')">{{ time }}</div>
|
||||
<div class="column is-amount" :data-label="$t('amount')">
|
||||
<NumberFormat :value="amount" />
|
||||
{{ currency }}
|
||||
</div>
|
||||
<div class="column is-deposit" :data-label="$t('subsequentDeposits')">
|
||||
<b-skeleton v-if="mixingPower === 'loading'" width="80" />
|
||||
<template v-else>
|
||||
{{ mixingPower }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="column is-hash" :data-label="$t('txHash')">
|
||||
<div class="details">
|
||||
<p class="detail">
|
||||
<a class="detail-description" :href="txExplorerUrl(tx.txHash)" target="_blank">
|
||||
{{ tx.txHash }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-status" :data-label="$t('status')">{{ status }}</div>
|
||||
|
||||
<div class="column column-buttons">
|
||||
<b-tooltip :active="!!tx.note" :label="tooltipShareUrl" position="is-left">
|
||||
<b-button
|
||||
v-clipboard:copy="`${tx.prefix}-${tx.note}`"
|
||||
v-clipboard:success="onCopyLink"
|
||||
type="is-primary hide-text-touch"
|
||||
size="is-small"
|
||||
:disabled="!tx.note"
|
||||
icon-left="copy"
|
||||
>
|
||||
{{ $t('note') }}
|
||||
</b-button>
|
||||
</b-tooltip>
|
||||
<b-button type="is-dark" size="is-small" icon-right="remove" @click="onClose" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint-disable no-console */
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import txStatus from '../store/txStatus'
|
||||
import NumberFormat from '@/components/NumberFormat'
|
||||
export default {
|
||||
components: {
|
||||
NumberFormat
|
||||
},
|
||||
props: {
|
||||
tx: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tooltipShareUrl: this.$t('copyNote'),
|
||||
time: '',
|
||||
isSpent: false,
|
||||
timer: null,
|
||||
copyTimer: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('txHashKeeper', ['txExplorerUrl']),
|
||||
...mapGetters('metamask', ['networkConfig', 'nativeCurrency']),
|
||||
...mapGetters('token', ['toDecimals', 'getSymbol']),
|
||||
...mapState('application', ['statistic']),
|
||||
prefix() {
|
||||
let prefix = this.tx.prefix || ''
|
||||
prefix = prefix.split('-')
|
||||
return { currency: prefix[1], amount: prefix[2] }
|
||||
},
|
||||
amount() {
|
||||
if (this.tx.amount === '100000000000000000') {
|
||||
return this.toDecimals(this.tx.amount, 18)
|
||||
}
|
||||
return this.tx.amount
|
||||
},
|
||||
currency() {
|
||||
const { currency } = this.prefix
|
||||
return this.getSymbol(currency || this.tx.currency)
|
||||
},
|
||||
mixingPower() {
|
||||
if (!this.tx.index) {
|
||||
return '-'
|
||||
}
|
||||
if (this.tx.index === 'v1') {
|
||||
return this.$t('v1Deposit')
|
||||
}
|
||||
const { currency, amount } = this.prefix
|
||||
const nextDepositIndex = this.statistic[currency][amount].nextDepositIndex
|
||||
if (this.tx.status === txStatus.waitingForReciept || !nextDepositIndex) {
|
||||
return 'loading'
|
||||
} else if (this.tx.status === txStatus.fail) {
|
||||
return '-'
|
||||
} else {
|
||||
const depositsPast = this.statistic[currency][amount].nextDepositIndex - this.tx.index - 1
|
||||
return this.depositsPastToRender(depositsPast)
|
||||
}
|
||||
},
|
||||
status() {
|
||||
if (this.isWaiting) {
|
||||
return this.$t('waitingForReceipt')
|
||||
}
|
||||
if (this.isFailed) {
|
||||
return this.$t('failed')
|
||||
}
|
||||
if (this.tx.isSpent) {
|
||||
return this.$t('spent')
|
||||
}
|
||||
return this.$t('deposited')
|
||||
},
|
||||
isWaiting() {
|
||||
return this.tx.status === txStatus.waitingForReciept
|
||||
},
|
||||
isFailed() {
|
||||
return this.tx.status === txStatus.fail
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.update()
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearTimeout(this.timer)
|
||||
clearTimeout(this.copyTimer)
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
this.updateTime()
|
||||
|
||||
this.timer = setTimeout(() => {
|
||||
this.update()
|
||||
}, 10000)
|
||||
},
|
||||
onCopyLink() {
|
||||
this.tooltipShareUrl = this.$t('copied')
|
||||
this.copyTimer = setTimeout(() => {
|
||||
this.tooltipShareUrl = this.$t('copyNote')
|
||||
}, 1500)
|
||||
},
|
||||
onClose() {
|
||||
this.$buefy.dialog.confirm({
|
||||
title: this.$t('removeFromCache'),
|
||||
type: 'is-primary is-outlined',
|
||||
message: this.$t('pleaseMakeSureYouHaveBackedUpYourNote'),
|
||||
cancelText: this.$t('cancelButton'),
|
||||
confirmText: this.$t('remove'),
|
||||
onConfirm: () => {
|
||||
this.$store.dispatch('notice/addNoticeWithInterval', {
|
||||
notice: {
|
||||
title: 'noteHasBeenDeleted',
|
||||
type: 'info'
|
||||
},
|
||||
interval: 2000
|
||||
})
|
||||
this.$store.commit('txHashKeeper/DELETE_TX', { txHash: this.tx.txHash })
|
||||
}
|
||||
})
|
||||
},
|
||||
updateTime(t = this.tx.timestamp) {
|
||||
this.time = this.$moment.unix(t).fromNow()
|
||||
},
|
||||
depositsPastToRender(depositsPast) {
|
||||
if (depositsPast < 0) {
|
||||
return 'loading'
|
||||
}
|
||||
|
||||
return this.$tc('userDeposit', depositsPast)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
299
components/Txs.vue
Normal file
299
components/Txs.vue
Normal file
|
@ -0,0 +1,299 @@
|
|||
<template>
|
||||
<div v-show="Object.keys(allTxs).length > 0 || Object.keys(jobs('tornado')).length > 0" class="txs">
|
||||
<div class="tx-filters buttons">
|
||||
<div class="tx-filters-title">{{ $t('filterBy') }}</div>
|
||||
<b-button
|
||||
v-for="(token, key) in tokens"
|
||||
:key="key"
|
||||
type="is-primary"
|
||||
size="is-small"
|
||||
:class="{ 'is-hovered': filters.currency === key }"
|
||||
outlined
|
||||
:disabled="!activeTokensFilters.has(key) && filters.currency !== key"
|
||||
@click="setFilter('currency', key)"
|
||||
>
|
||||
{{ token.symbol }}
|
||||
</b-button>
|
||||
<div class="break"></div>
|
||||
<b-field>
|
||||
<p class="control">
|
||||
<b-button
|
||||
type="is-primary"
|
||||
size="is-small"
|
||||
:class="{ 'is-hovered': filters.isSpent === true }"
|
||||
outlined
|
||||
@click="setSpent(true)"
|
||||
>
|
||||
{{ $t('spent') }}
|
||||
</b-button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<b-button
|
||||
type="is-primary"
|
||||
size="is-small"
|
||||
:class="{ 'is-hovered': filters.isSpent === false }"
|
||||
outlined
|
||||
@click="setSpent(false)"
|
||||
>
|
||||
{{ $t('unspent') }}
|
||||
</b-button>
|
||||
</p>
|
||||
</b-field>
|
||||
<div class="break"></div>
|
||||
<b-field>
|
||||
<p class="control">
|
||||
<b-button
|
||||
type="is-primary"
|
||||
size="is-small"
|
||||
:class="{ 'is-hovered': transactions === 'regular' }"
|
||||
outlined
|
||||
@click="setTransactionFilter('regular')"
|
||||
>
|
||||
{{ $t('regular') }}
|
||||
</b-button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<b-button
|
||||
type="is-primary"
|
||||
size="is-small"
|
||||
:class="{ 'is-hovered': transactions === 'encrypted' }"
|
||||
outlined
|
||||
@click="setTransactionFilter('encrypted')"
|
||||
>
|
||||
{{ $t('encrypted') }}
|
||||
</b-button>
|
||||
</p>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="tx-head">
|
||||
<div class="columns">
|
||||
<div class="column is-time is-sortable" @click="setSort('timestamp')">
|
||||
{{ $t('timePassed') }}
|
||||
|
||||
<span
|
||||
v-show="currentSort === 'timestamp'"
|
||||
class="icon icon-chevron-up"
|
||||
:class="{ 'is-desc': !isAsc }"
|
||||
/>
|
||||
</div>
|
||||
<div class="column is-amount is-sortable" @click="setSort('amount')">
|
||||
{{ $t('amount') }}
|
||||
<span
|
||||
v-show="currentSort === 'amount'"
|
||||
class="icon icon-chevron-up"
|
||||
:class="{ 'is-desc': !isAsc }"
|
||||
/>
|
||||
</div>
|
||||
<div class="column is-deposit is-sortable" @click="setSort('deposits')">
|
||||
{{ $t('subsequentDeposits') }}
|
||||
<span
|
||||
v-show="currentSort === 'deposits'"
|
||||
class="icon icon-chevron-up"
|
||||
:class="{ 'is-desc': !isAsc }"
|
||||
/>
|
||||
</div>
|
||||
<div class="column is-hash">{{ $t('txHash') }}</div>
|
||||
<div class="column is-status">{{ $t('status') }}</div>
|
||||
<div class="column column-buttons"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tx-head-mobile field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<b-dropdown v-model="mobileSort" expanded aria-role="list">
|
||||
<div slot="trigger" class="control">
|
||||
<div class="input">
|
||||
<span>{{
|
||||
mobileSort === 'timestamp'
|
||||
? $t('timePassed')
|
||||
: mobileSort === 'amount'
|
||||
? $t('amount')
|
||||
: $t('subsequentDeposits')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<b-dropdown-item value="timestamp" aria-role="listitem">
|
||||
{{ $t('timePassed') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item value="amount" aria-role="listitem">{{ $t('amount') }}</b-dropdown-item>
|
||||
<b-dropdown-item value="deposits" aria-role="listitem">
|
||||
{{ $t('subsequentDeposits') }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-primary" @click="setSort(mobileSort)">
|
||||
<span
|
||||
v-show="currentSort === mobileSort"
|
||||
class="icon icon-arrow-up"
|
||||
:class="{ 'is-desc': !isAsc }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Job v-for="job in jobs('tornado')" :key="job.id" :job="job" />
|
||||
<template v-for="tx in filteredTxs">
|
||||
<EncryptedTx v-if="tx.isEncrypted" :key="tx.txHash" :tx="tx" />
|
||||
<Tx v-else :key="tx.txHash" :tx="tx" />
|
||||
</template>
|
||||
<div v-show="filteredTxs.length === 0 && jobs('tornado').length === 0" class="box box-tx is-white">
|
||||
<div class="columns is-vcentered is-centered">
|
||||
<div class="column">{{ $t('thereAreNoElements') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint-disable no-console */
|
||||
import { mapActions, mapGetters, mapState } from 'vuex'
|
||||
import Tx from '@/components/Tx'
|
||||
import EncryptedTx from '@/components/EncryptedTx'
|
||||
import Job from '@/components/Job'
|
||||
const { toWei, toBN } = require('web3-utils')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tx,
|
||||
Job,
|
||||
EncryptedTx
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filters: {
|
||||
currency: '',
|
||||
ifAfter24hrs: false,
|
||||
isSpent: undefined,
|
||||
timer: null
|
||||
},
|
||||
transactions: 'all',
|
||||
currentSort: 'timestamp',
|
||||
isAsc: false,
|
||||
mobileSort: 'timestamp'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('metamask', ['networkConfig', 'nativeCurrency']),
|
||||
...mapGetters('txHashKeeper', ['allTxs', 'txs', 'txExplorerUrl', 'encryptedTxs']),
|
||||
...mapGetters('relayer', ['jobs']),
|
||||
...mapGetters('price', ['getTokenPrice']),
|
||||
...mapState('application', ['statistic']),
|
||||
tokens() {
|
||||
return this.networkConfig.tokens
|
||||
},
|
||||
activeTokensFilters() {
|
||||
const filters = new Set()
|
||||
this.allTxs.forEach((tx) => {
|
||||
filters.add(tx.currency)
|
||||
})
|
||||
return filters
|
||||
},
|
||||
filteredTxs() {
|
||||
if (this.transactions === 'regular') {
|
||||
return this.filterTxs(this.txs)
|
||||
}
|
||||
if (this.transactions === 'encrypted') {
|
||||
return this.filterTxs(this.encryptedTxs)
|
||||
}
|
||||
return this.filterTxs(this.allTxs)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
mobileSort(sort) {
|
||||
if (this.currentSort === sort) {
|
||||
return
|
||||
}
|
||||
this.setSort(sort)
|
||||
},
|
||||
currentSort(sort) {
|
||||
this.mobileSort = sort
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.timer = setTimeout(() => {
|
||||
this.cleanTxs()
|
||||
this.cleanEncryptedTxs()
|
||||
this.checkPendingTransaction()
|
||||
this.checkPendingEncryptedTransaction()
|
||||
}, 2500)
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearTimeout(this.timer)
|
||||
},
|
||||
methods: {
|
||||
...mapActions('txHashKeeper', [
|
||||
'cleanTxs',
|
||||
'cleanEncryptedTxs',
|
||||
'checkPendingTransaction',
|
||||
'checkPendingEncryptedTransaction'
|
||||
]),
|
||||
setFilter(filter, value) {
|
||||
this.filters[filter] = this.filters[filter] === value ? '' : value
|
||||
},
|
||||
setSort(sort) {
|
||||
this.isAsc = sort === this.currentSort ? !this.isAsc : true
|
||||
this.currentSort = sort
|
||||
},
|
||||
filterTxs(txs) {
|
||||
return txs
|
||||
.filter((tx) => {
|
||||
const isMatched = []
|
||||
if (this.filters.currency !== '') {
|
||||
isMatched.push(this.filters.currency === tx.currency)
|
||||
}
|
||||
if (this.filters.ifAfter24hrs) {
|
||||
isMatched.push(this.$moment().unix() - Number(tx.timestamp) > 86400)
|
||||
}
|
||||
if (this.filters.isSpent !== undefined) {
|
||||
isMatched.push(this.filters.isSpent === Boolean(tx.isSpent))
|
||||
}
|
||||
|
||||
return isMatched.every(Boolean)
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (this.currentSort === 'deposits') {
|
||||
const depositsA = this.getDepositsPast(a)
|
||||
const depositsB = this.getDepositsPast(b)
|
||||
return this.isAsc ? depositsA - depositsB : depositsB - depositsA
|
||||
}
|
||||
if (this.currentSort === 'amount') {
|
||||
return this.isAsc ? this.compareAmounts(a, b) : this.compareAmounts(b, a)
|
||||
}
|
||||
return this.isAsc
|
||||
? this.compareNumbers(a[this.currentSort], b[this.currentSort])
|
||||
: this.compareNumbers(b[this.currentSort], a[this.currentSort])
|
||||
})
|
||||
},
|
||||
compareNumbers(a, b) {
|
||||
const a1 = isNaN(Number(a)) ? 0 : Number(a)
|
||||
const b1 = isNaN(Number(b)) ? 0 : Number(b)
|
||||
|
||||
return a1 - b1
|
||||
},
|
||||
compareAmounts(a, b) {
|
||||
let amountA = toBN(toWei(a.amount))
|
||||
if (a.currency !== this.nativeCurrency) {
|
||||
const priceA = toBN(this.getTokenPrice(a.currency))
|
||||
amountA = amountA.mul(priceA)
|
||||
}
|
||||
|
||||
let amountB = toBN(toWei(b.amount))
|
||||
if (b.currency !== this.nativeCurrency) {
|
||||
const priceB = toBN(this.getTokenPrice(b.currency))
|
||||
amountB = amountB.mul(priceB)
|
||||
}
|
||||
|
||||
return amountA.cmp(amountB)
|
||||
},
|
||||
getDepositsPast(tx) {
|
||||
const [, currency, amount] = tx.prefix.split('-')
|
||||
return this.statistic[currency][amount].nextDepositIndex - tx.index - 1
|
||||
},
|
||||
setTransactionFilter(value) {
|
||||
this.transactions = this.transactions === value ? 'all' : value
|
||||
},
|
||||
setSpent(value) {
|
||||
this.filters.isSpent = this.filters.isSpent === value ? undefined : value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
195
components/governance/CreateProposal.vue
Normal file
195
components/governance/CreateProposal.vue
Normal file
|
@ -0,0 +1,195 @@
|
|||
<template>
|
||||
<div class="proposal">
|
||||
<h1 class="title">{{ $t('createProposal') }}</h1>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-6">
|
||||
<b-field
|
||||
:label="$t('proposalTitle')"
|
||||
:message="isValidTitle ? '' : $t('proposal.error.title')"
|
||||
:type="{ 'is-warning': !isValidTitle }"
|
||||
>
|
||||
<b-input v-model="validTitle" :placeholder="$t('title')"></b-input>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<b-field
|
||||
:label="$t('proposalAddress')"
|
||||
:type="{ 'is-warning': !hasValidAddress }"
|
||||
:message="hasValidAddress ? '' : addressErrorMessage"
|
||||
>
|
||||
<b-input
|
||||
v-model="address"
|
||||
:placeholder="$t('proposalAddress')"
|
||||
:size="!address ? '' : hasValidAddress ? '' : 'is-warning'"
|
||||
></b-input>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<b-field
|
||||
:message="isValidDescription ? '' : $t('proposal.error.description')"
|
||||
:type="{ 'is-warning': !isValidDescription }"
|
||||
:label="$t('proposalDescription')"
|
||||
>
|
||||
<b-input v-model="validDescription" maxlength="2000" type="textarea"></b-input>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
<b-tooltip :label="`${$t('onlyOneProposalErr')}`" position="is-top" :active="cannotCreate" multilined>
|
||||
<b-button
|
||||
:disabled="cannotCreate"
|
||||
type="is-primary"
|
||||
:icon-left="isFetchingBalances ? '' : 'plus'"
|
||||
outlined
|
||||
:loading="isFetchingBalances"
|
||||
@click="onCreateProposal"
|
||||
>
|
||||
{{ $t('createProposal') }}
|
||||
</b-button>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapActions, mapState, mapGetters } from 'vuex'
|
||||
import { debounce } from '@/utils'
|
||||
|
||||
const { isAddress } = require('web3-utils')
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
proposalAddress: '',
|
||||
description: '',
|
||||
title: '',
|
||||
isValidAddress: true,
|
||||
isValidContract: true,
|
||||
isValidTitle: true,
|
||||
isValidDescription: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('governance/gov', ['latestProposalId']),
|
||||
...mapGetters('governance/gov', ['isFetchingBalances']),
|
||||
address: {
|
||||
get() {
|
||||
return this.proposalAddress
|
||||
},
|
||||
set(address) {
|
||||
this.setInitialState()
|
||||
this.proposalAddress = address
|
||||
|
||||
debounce(this.validateAddress, address)
|
||||
}
|
||||
},
|
||||
validTitle: {
|
||||
get() {
|
||||
return this.title
|
||||
},
|
||||
set(title) {
|
||||
this.isValidTitle = true
|
||||
this.title = title
|
||||
}
|
||||
},
|
||||
addressErrorMessage() {
|
||||
if (!this.isValidAddress) {
|
||||
return this.$t('proposal.error.address')
|
||||
}
|
||||
|
||||
if (!this.isValidContract) {
|
||||
return this.$t('proposal.error.contract')
|
||||
}
|
||||
|
||||
return this.$t('proposal.error.address')
|
||||
},
|
||||
validDescription: {
|
||||
get() {
|
||||
return this.description
|
||||
},
|
||||
set(description) {
|
||||
this.isValidDescription = true
|
||||
this.description = description
|
||||
}
|
||||
},
|
||||
hasValidAddress() {
|
||||
return this.isValidAddress && this.isValidContract
|
||||
},
|
||||
cannotCreate() {
|
||||
return (
|
||||
this.latestProposalId.value !== 0 &&
|
||||
(this.latestProposalId.status === 'active' || this.latestProposalId.status === 'pending')
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('governance/gov', ['createProposal']),
|
||||
async addressIsContract(address) {
|
||||
if (!address) {
|
||||
return false
|
||||
}
|
||||
|
||||
const code = await this.$provider.web3.eth.getCode(address)
|
||||
|
||||
return code !== '0x'
|
||||
},
|
||||
isAddress(address) {
|
||||
const isCorrect = isAddress(address)
|
||||
|
||||
if (!isCorrect && address) {
|
||||
this.isValidAddress = isCorrect
|
||||
}
|
||||
|
||||
return isCorrect
|
||||
},
|
||||
async isContract(address) {
|
||||
const isContract = await this.addressIsContract(address)
|
||||
|
||||
if (!isContract && address) {
|
||||
this.isValidContract = isContract
|
||||
}
|
||||
|
||||
return isContract
|
||||
},
|
||||
setInitialState() {
|
||||
this.isValidAddress = true
|
||||
this.isValidContract = true
|
||||
},
|
||||
async validateAddress(address) {
|
||||
const isCorrect = this.isAddress(address)
|
||||
|
||||
if (!isCorrect) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isContract = await this.isContract(address)
|
||||
|
||||
return isContract
|
||||
},
|
||||
async validationForms() {
|
||||
this.isValidTitle = this.title
|
||||
this.isValidDescription = this.description
|
||||
this.isValidAddress = this.proposalAddress
|
||||
|
||||
const isCorrect = await this.validateAddress(this.proposalAddress)
|
||||
|
||||
return isCorrect && this.isValidAddress && this.isValidTitle && this.isValidDescription
|
||||
},
|
||||
async onCreateProposal() {
|
||||
const isValidForms = await this.validationForms()
|
||||
|
||||
if (!isValidForms) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.dispatch('loading/enable', { message: this.$t('preparingTransactionData') })
|
||||
|
||||
await this.createProposal({
|
||||
proposalAddress: this.proposalAddress,
|
||||
title: this.title,
|
||||
description: this.description
|
||||
})
|
||||
this.$store.dispatch('loading/disable')
|
||||
// this.$parent.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
108
components/governance/Metrics.vue
Normal file
108
components/governance/Metrics.vue
Normal file
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<div class="governance-head">
|
||||
<div class="columns is-mobile is-multiline is-centered is-vcentered">
|
||||
<div class="column is-12-mobile is-6-tablet is-3-desktop">
|
||||
<div class="info-name">{{ $t('availableBalance') }}</div>
|
||||
<div class="info-value"><number-format :value="balance" /> TORN</div>
|
||||
</div>
|
||||
<div class="column is-12-mobile is-6-tablet is-3-desktop">
|
||||
<div class="info-name">{{ $t('stakingReward.title') }}</div>
|
||||
<div class="info-value"><number-format :value="reward" /> TORN</div>
|
||||
</div>
|
||||
<div class="column is-12-mobile is-6-tablet is-3-desktop">
|
||||
<div class="info-name">{{ $t('votingPower') }}</div>
|
||||
<div class="info-value has-tooltip">
|
||||
<span><number-format :value="votingPower" /> TORN</span>
|
||||
<b-tooltip
|
||||
:label="
|
||||
`${$n(toDecimals(lockedBalance, 18))} ${$t('locked')} TORN + ${$n(
|
||||
toDecimals(delegatedBalance, 18)
|
||||
)} ${$t('delegated')} TORN`
|
||||
"
|
||||
size="is-medium"
|
||||
position="is-top"
|
||||
multilined
|
||||
>
|
||||
<button class="button is-primary has-icon">
|
||||
<span class="icon icon-info"></span>
|
||||
</button>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-mobile is-6-tablet is-3-desktop">
|
||||
<div class="info-value without-label has-text-right-desktop">
|
||||
<b-button
|
||||
type="is-text"
|
||||
:icon-left="isDataLoading ? '' : 'settings'"
|
||||
:loading="isDataLoading"
|
||||
@click.native="onManage"
|
||||
>
|
||||
{{ $t('manage') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import ManageBox from './manage/ManageBox'
|
||||
import NumberFormat from '@/components/NumberFormat'
|
||||
const { fromWei } = require('web3-utils')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NumberFormat
|
||||
},
|
||||
computed: {
|
||||
...mapState('torn', {
|
||||
balance: (state) => fromWei(state.balance)
|
||||
}),
|
||||
...mapState('governance/gov', ['lockedBalance', 'delegatedBalance']),
|
||||
...mapGetters('governance/gov', ['isFetchingBalances']),
|
||||
...mapGetters('governance/staking', ['reward', 'isCheckingReward']),
|
||||
...mapGetters('token', ['toDecimals']),
|
||||
...mapState('metamask', ['isInitialized']),
|
||||
votingPower() {
|
||||
return fromWei(this.$store.getters['governance/gov/votingPower'])
|
||||
},
|
||||
isDataLoading() {
|
||||
return this.isCheckingReward || this.isFetchingBalances
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isInitialized: {
|
||||
handler(isInitialized) {
|
||||
if (isInitialized) {
|
||||
this.checkReward()
|
||||
this.fetchTokenBalance()
|
||||
this.fetchTokenAllowance()
|
||||
this.REMOVE_SIGNATURE()
|
||||
this.fetchUserData()
|
||||
this.fetchDelegatedBalance()
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('governance/staking', ['checkReward']),
|
||||
...mapActions('torn', ['fetchTokenBalance', 'fetchTokenAllowance']),
|
||||
...mapActions('governance/gov', ['fetchUserData', 'fetchDelegatedBalance']),
|
||||
...mapMutations('torn', ['REMOVE_SIGNATURE']),
|
||||
onManage() {
|
||||
const manageBox = this.$buefy.modal.open({
|
||||
parent: this,
|
||||
component: ManageBox,
|
||||
hasModalCard: true,
|
||||
width: 480,
|
||||
customClass: 'is-pinned is-manage-box'
|
||||
})
|
||||
manageBox.$on('close', () => {
|
||||
this.isManageBoxOpened = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
351
components/governance/Proposal.vue
Normal file
351
components/governance/Proposal.vue
Normal file
|
@ -0,0 +1,351 @@
|
|||
<template>
|
||||
<div class="proposal">
|
||||
<div class="columns">
|
||||
<div class="column is-7-tablet is-8-desktop">
|
||||
<h1 class="title">{{ data.title }}</h1>
|
||||
<div class="description">
|
||||
<p>
|
||||
{{ data.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5-tablet is-4-desktop">
|
||||
<div v-if="data.status === 'active'" class="proposal-block">
|
||||
<div class="title">{{ $t('castYourVote') }}</div>
|
||||
<b-tooltip
|
||||
class="fit-content"
|
||||
:label="tooltipMessage"
|
||||
position="is-top"
|
||||
:active="readyForAction"
|
||||
multilined
|
||||
>
|
||||
<div class="buttons buttons__halfwidth">
|
||||
<b-button
|
||||
:disabled="readyForAction"
|
||||
type="is-primary"
|
||||
:icon-left="isFetchingBalances ? '' : 'check'"
|
||||
outlined
|
||||
:loading="isFetchingBalances"
|
||||
@click="onCastVote(true)"
|
||||
>{{ $t('for') }}</b-button
|
||||
>
|
||||
<b-button
|
||||
:disabled="readyForAction"
|
||||
type="is-danger"
|
||||
:icon-left="isFetchingBalances ? '' : 'close'"
|
||||
outlined
|
||||
:loading="isFetchingBalances"
|
||||
@click="onCastVote(false)"
|
||||
>{{ $t('against') }}</b-button
|
||||
>
|
||||
</div>
|
||||
</b-tooltip>
|
||||
<i18n
|
||||
v-if="voterReceipts[data.id] && voterReceipts[data.id].hasVoted"
|
||||
tag="div"
|
||||
path="yourCurrentVote"
|
||||
>
|
||||
<template v-slot:vote>
|
||||
<span
|
||||
:class="{
|
||||
'has-text-primary': voterReceipts[data.id].support,
|
||||
'has-text-danger': !voterReceipts[data.id].support
|
||||
}"
|
||||
>{{ $n(fromWeiToTorn(voterReceipts[data.id].balance)) }} TORN</span
|
||||
>
|
||||
</template>
|
||||
</i18n>
|
||||
</div>
|
||||
<div v-else-if="data.status === 'awaitingExecution'" class="proposal-block">
|
||||
<div class="title">{{ $t('executeProposal') }}</div>
|
||||
<b-tooltip
|
||||
class="fit-content"
|
||||
:label="$t('connectYourWalletFirst')"
|
||||
position="is-top"
|
||||
:active="!ethAccount"
|
||||
multilined
|
||||
>
|
||||
<b-button
|
||||
type="is-primary"
|
||||
icon-left="check"
|
||||
outlined
|
||||
:disabled="!ethAccount"
|
||||
expanded
|
||||
@click="onExecute"
|
||||
>{{ $t('execute') }}</b-button
|
||||
>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
<div class="proposal-block">
|
||||
<div class="title">{{ $t('currentResults') }}</div>
|
||||
<div class="label">
|
||||
{{ $t('for') }}
|
||||
<span class="percent"
|
||||
><number-format :value="data.results.for" /> TORN / {{ calculatePercent('for') }}%</span
|
||||
>
|
||||
</div>
|
||||
<b-progress :value="calculatePercent('for')" type="is-primary"></b-progress>
|
||||
<div class="label">
|
||||
{{ $t('against') }}
|
||||
<span class="percent"
|
||||
><number-format :value="data.results.against" class="value" /> TORN /
|
||||
{{ calculatePercent('against') }}%</span
|
||||
>
|
||||
</div>
|
||||
<b-progress :value="calculatePercent('against')" type="is-danger"></b-progress>
|
||||
<div class="label">
|
||||
{{ $t('quorum') }}
|
||||
<b-tooltip
|
||||
:label="
|
||||
$t('quorumTooltip', {
|
||||
days: $tc('dayPlural', votingPeriod),
|
||||
votes: $n(quorumVotes, 'compact')
|
||||
})
|
||||
"
|
||||
size="is-medium"
|
||||
position="is-top"
|
||||
multilined
|
||||
>
|
||||
<button class="button is-primary has-icon">
|
||||
<span class="icon icon-info"></span>
|
||||
</button>
|
||||
</b-tooltip>
|
||||
<span class="percent"
|
||||
><number-format :value="isQuorumCompleted ? quorumVotes : quorumResult" class="value" /> TORN /
|
||||
{{ quorumPercent }}%</span
|
||||
>
|
||||
</div>
|
||||
<b-progress :value="quorumPercent" type="is-violet"></b-progress>
|
||||
</div>
|
||||
<div class="proposal-block">
|
||||
<div class="title">{{ $t('information') }}</div>
|
||||
<div class="columns is-multiline is-small" :class="{ 'has-countdown': countdown }">
|
||||
<div class="column is-full-small">
|
||||
<strong>{{ $t('proposalAddress') }}</strong>
|
||||
<div class="value">
|
||||
<a :href="contractUrl" class="address" target="_blank">
|
||||
{{ data.target }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half-small">
|
||||
<strong>{{ $t('id') }}</strong>
|
||||
<div class="value">{{ data.id }}</div>
|
||||
</div>
|
||||
<div class="column is-half-small">
|
||||
<strong>{{ $t('status') }}</strong>
|
||||
<div class="value">
|
||||
<b-tag :type="getStatusType(data.status)">{{ $t(data.status) }}</b-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half-small">
|
||||
<strong>{{ $t('startDate') }}</strong>
|
||||
<div class="value">{{ $moment.unix(data.startTime).format('llll') }}</div>
|
||||
</div>
|
||||
<div class="column is-half-small">
|
||||
<strong>{{ $t('endDate') }}</strong>
|
||||
<div class="value">{{ $moment.unix(data.endTime).format('llll') }}</div>
|
||||
</div>
|
||||
<div v-if="countdown" class="column is-full-small">
|
||||
<strong>{{ $t(timerLabel) }}</strong>
|
||||
<div class="value">
|
||||
{{ countdown }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions, mapGetters } from 'vuex'
|
||||
import quorum from './mixins/quorum'
|
||||
import NumberFormat from '@/components/NumberFormat'
|
||||
const { toBN, fromWei, toWei } = require('web3-utils')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NumberFormat
|
||||
},
|
||||
mixins: [quorum],
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
timeId: null,
|
||||
countdown: false,
|
||||
timerLabel: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('governance/gov', ['proposals', 'voterReceipts']),
|
||||
...mapState('metamask', ['ethAccount', 'isInitialized']),
|
||||
...mapGetters('txHashKeeper', ['addressExplorerUrl']),
|
||||
...mapGetters('metamask', ['networkConfig']),
|
||||
...mapGetters('governance/gov', ['votingPower', 'constants', 'votingPeriod', 'isFetchingBalances']),
|
||||
readyForAction() {
|
||||
return (
|
||||
this.data.status !== 'active' ||
|
||||
!this.ethAccount ||
|
||||
!this.votingPower ||
|
||||
toBN(this.votingPower).isZero()
|
||||
)
|
||||
},
|
||||
tooltipMessage() {
|
||||
if (!this.ethAccount) {
|
||||
return this.$t('connectYourWalletFirst')
|
||||
}
|
||||
|
||||
if (this.data.status !== 'active') {
|
||||
return this.$t('proposalIsActive')
|
||||
}
|
||||
|
||||
if (!this.votingPower || toBN(this.votingPower).isZero()) {
|
||||
return this.$t('lockedBalanceError')
|
||||
}
|
||||
|
||||
return ''
|
||||
},
|
||||
contractUrl() {
|
||||
return this.addressExplorerUrl(this.data.target) + '#code'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isInitialized: {
|
||||
handler(isInitialized) {
|
||||
if (isInitialized) {
|
||||
this.fetchReceipt({ id: this.data.id })
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
data: {
|
||||
handler(data) {
|
||||
const statusesWithNoTimer = ['failed', 'defeated', 'expired', 'executed']
|
||||
if (statusesWithNoTimer.includes(data.status)) {
|
||||
return
|
||||
}
|
||||
|
||||
const { MINING_BLOCK_TIME } = this.networkConfig.constants
|
||||
const { EXECUTION_DELAY, EXECUTION_EXPIRATION } = this.constants
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const startTime = data.startTime + MINING_BLOCK_TIME
|
||||
const endTime = data.endTime + MINING_BLOCK_TIME
|
||||
const executionStartTime = endTime + EXECUTION_DELAY
|
||||
const expirationEndTime = executionStartTime + EXECUTION_EXPIRATION
|
||||
|
||||
if (now <= startTime) {
|
||||
this.timerLabel = 'timerRemainingForPending'
|
||||
this.startTimer(startTime)
|
||||
} else if (now <= endTime) {
|
||||
this.timerLabel = 'timerRemainingForVoting'
|
||||
this.startTimer(endTime)
|
||||
} else if (now <= executionStartTime) {
|
||||
this.timerLabel = 'timerRemainingForAwaitingExecution'
|
||||
this.startTimer(executionStartTime)
|
||||
} else if (now <= expirationEndTime) {
|
||||
this.timerLabel = 'timerRemainingForExecution'
|
||||
this.startTimer(expirationEndTime)
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearTimeout(this.timeId)
|
||||
},
|
||||
methods: {
|
||||
...mapActions('governance/gov', ['castVote', 'executeProposal', 'fetchReceipt', 'fetchProposals']),
|
||||
getStatusType(status) {
|
||||
let statusType = ''
|
||||
switch (status) {
|
||||
case 'awaitingExecution':
|
||||
case 'active':
|
||||
statusType = 'is-primary'
|
||||
break
|
||||
case 'expired':
|
||||
statusType = 'is-gray'
|
||||
break
|
||||
case 'failed':
|
||||
case 'defeated':
|
||||
statusType = 'is-danger'
|
||||
break
|
||||
case 'pending':
|
||||
case 'timeLocked':
|
||||
statusType = 'is-warning'
|
||||
break
|
||||
case 'executed':
|
||||
statusType = 'is-violet'
|
||||
break
|
||||
}
|
||||
return statusType
|
||||
},
|
||||
calculatePercent(result) {
|
||||
return this.results.isZero()
|
||||
? 0
|
||||
: toBN(toWei(this.data.results[result]))
|
||||
.mul(toBN(100))
|
||||
.divRound(this.results)
|
||||
.toNumber()
|
||||
},
|
||||
onCastVote(support) {
|
||||
this.castVote({ id: this.data.id, support })
|
||||
},
|
||||
onExecute() {
|
||||
this.executeProposal({ id: this.data.id })
|
||||
},
|
||||
fromWeiToTorn(v) {
|
||||
return fromWei(v)
|
||||
},
|
||||
accurateHumanize(duration, accuracy = 4) {
|
||||
const units = [
|
||||
{ unit: 'y', key: 'yy' },
|
||||
{ unit: 'M', key: 'MM' },
|
||||
{ unit: 'd', key: 'dd' },
|
||||
{ unit: 'h', key: 'hh' },
|
||||
{ unit: 'm', key: 'mm' },
|
||||
{ unit: 's', key: 'ss' }
|
||||
]
|
||||
let beginFilter = false
|
||||
let componentCount = 0
|
||||
|
||||
return units
|
||||
.map(({ unit, key }) => ({ value: duration.get(unit), key }))
|
||||
.filter(({ value, key }) => {
|
||||
if (beginFilter === false) {
|
||||
if (value === 0) {
|
||||
return false
|
||||
}
|
||||
beginFilter = true
|
||||
}
|
||||
componentCount++
|
||||
return value !== 0 && componentCount <= accuracy
|
||||
})
|
||||
.map(({ value, key }) => ({ value, key: value === 1 ? key[0] : key }))
|
||||
.map(({ value, key }) => this.$moment.localeData().relativeTime(value, true, key, true))
|
||||
.join(' ')
|
||||
},
|
||||
startTimer(time) {
|
||||
this.timeId = setTimeout(() => {
|
||||
const diffTime = this.$moment.unix(time).diff(this.$moment())
|
||||
|
||||
if (diffTime > 0) {
|
||||
this.countdown = this.accurateHumanize(this.$moment.duration(diffTime, 'millisecond'))
|
||||
|
||||
this.startTimer(time)
|
||||
} else {
|
||||
this.countdown = false
|
||||
|
||||
this.fetchProposals({})
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
79
components/governance/ProposalSkeleton.vue
Normal file
79
components/governance/ProposalSkeleton.vue
Normal file
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<div class="proposal">
|
||||
<div class="columns">
|
||||
<div class="column is-7-tablet is-8-desktop">
|
||||
<h1 class="title"><b-skeleton height="24" width="200"></b-skeleton></h1>
|
||||
<div class="description">
|
||||
<b-skeleton width="60%"></b-skeleton>
|
||||
<b-skeleton width="60%"></b-skeleton>
|
||||
<b-skeleton width="60%"></b-skeleton>
|
||||
<b-skeleton width="60%"></b-skeleton>
|
||||
<b-skeleton width="60%"></b-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5-tablet is-4-desktop">
|
||||
<div class="proposal-block">
|
||||
<div class="title">{{ $t('castYourVote') }}</div>
|
||||
<div class="buttons buttons__halfwidth">
|
||||
<b-skeleton height="2.857em"></b-skeleton>
|
||||
<b-skeleton height="2.857em"></b-skeleton>
|
||||
</div>
|
||||
<div><b-skeleton height="21"></b-skeleton></div>
|
||||
</div>
|
||||
<div class="proposal-block">
|
||||
<div class="title">{{ $t('currentResults') }}</div>
|
||||
<div class="label">
|
||||
<b-skeleton width="26" height="21"></b-skeleton>
|
||||
<span class="percent"><b-skeleton width="90" height="21"></b-skeleton></span>
|
||||
</div>
|
||||
<b-progress :value="0" type="is-primary"></b-progress>
|
||||
<div class="label">
|
||||
<b-skeleton width="58" height="21"></b-skeleton>
|
||||
<span class="percent"><b-skeleton width="90" height="21"></b-skeleton></span>
|
||||
</div>
|
||||
<b-progress :value="0" type="is-danger"></b-progress>
|
||||
<div class="label">
|
||||
<b-skeleton width="50" height="21"></b-skeleton>
|
||||
<span class="percent"><b-skeleton width="90" height="21"></b-skeleton></span>
|
||||
</div>
|
||||
<b-progress :value="0" type="is-danger"></b-progress>
|
||||
</div>
|
||||
<div class="proposal-block">
|
||||
<div class="title">{{ $t('information') }}</div>
|
||||
<div class="columns is-multiline is-small">
|
||||
<div class="column is-full-small">
|
||||
<strong>{{ $t('proposalAddress') }}</strong>
|
||||
<div class="value">
|
||||
<b-skeleton height="21"></b-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half-small">
|
||||
<strong>{{ $t('id') }}</strong>
|
||||
<div class="value">
|
||||
<b-skeleton width="20" height="21"></b-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half-small">
|
||||
<strong>{{ $t('status') }}</strong>
|
||||
<div class="value">
|
||||
<b-skeleton width="70" height="21"></b-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half-small">
|
||||
<strong>{{ $t('startDate') }}</strong>
|
||||
<div class="value">
|
||||
<b-skeleton width="70" height="21"></b-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half-small">
|
||||
<strong>{{ $t('endDate') }}</strong>
|
||||
<div class="value">
|
||||
<b-skeleton width="70" height="21"></b-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
89
components/governance/ProposalsList.vue
Normal file
89
components/governance/ProposalsList.vue
Normal file
|
@ -0,0 +1,89 @@
|
|||
<template>
|
||||
<div class="proposals-list">
|
||||
<div class="proposals-list--header">
|
||||
<div class="title">{{ $t('proposals') }}</div>
|
||||
<b-field class="field-tabs">
|
||||
<b-radio-button v-model="proposalStatusFilter" native-value="" type="is-primary">
|
||||
<span>{{ $t('all') }}</span>
|
||||
</b-radio-button>
|
||||
|
||||
<b-radio-button v-model="proposalStatusFilter" native-value="active" type="is-primary">
|
||||
<span>{{ $t('active') }}</span>
|
||||
</b-radio-button>
|
||||
</b-field>
|
||||
<b-field class="field-btn">
|
||||
<b-tooltip
|
||||
:label="
|
||||
$t('proposalThresholdError', {
|
||||
PROPOSAL_THRESHOLD: $n(proposalThreshold)
|
||||
})
|
||||
"
|
||||
:active="!hasProposalThreshold"
|
||||
multilined
|
||||
>
|
||||
<b-button
|
||||
type="is-primary"
|
||||
:icon-left="isFetchingBalances ? '' : 'plus'"
|
||||
outlined
|
||||
:disabled="!hasProposalThreshold"
|
||||
:loading="isFetchingBalances"
|
||||
@click="onCreateProposal"
|
||||
>
|
||||
{{ $t('createProposal') }}
|
||||
</b-button>
|
||||
</b-tooltip>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="proposals-list--container">
|
||||
<ProposalsListSkeleton v-if="isFetchingProposals" />
|
||||
<ProposalsListItem v-for="proposal in filteredProposals" v-else :key="proposal.id" :data="proposal" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import ProposalsListItem from '@/components/governance/ProposalsListItem'
|
||||
import ProposalsListSkeleton from '@/components/governance/ProposalsListSkeleton'
|
||||
|
||||
const { toBN } = require('web3-utils')
|
||||
export default {
|
||||
components: {
|
||||
ProposalsListItem,
|
||||
ProposalsListSkeleton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
proposalStatusFilter: '',
|
||||
timer: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('governance/gov', ['lockedBalance', 'proposals']),
|
||||
...mapGetters('governance/gov', ['isFetchingProposals', 'constants', 'isFetchingBalances']),
|
||||
...mapGetters('token', ['toDecimals']),
|
||||
filteredProposals() {
|
||||
return this.proposals
|
||||
.filter((proposal) => {
|
||||
if (this.proposalStatusFilter) {
|
||||
return proposal.status === this.proposalStatusFilter || proposal.status === 'awaitingExecution'
|
||||
}
|
||||
return true
|
||||
})
|
||||
.reverse()
|
||||
},
|
||||
hasProposalThreshold() {
|
||||
const PROPOSAL_THRESHOLD = toBN(this.constants.PROPOSAL_THRESHOLD)
|
||||
return toBN(this.lockedBalance).gte(PROPOSAL_THRESHOLD)
|
||||
},
|
||||
proposalThreshold() {
|
||||
return this.toDecimals(this.constants.PROPOSAL_THRESHOLD, 18)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onCreateProposal() {
|
||||
this.$emit('onCreateProposal')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
89
components/governance/ProposalsListItem.vue
Normal file
89
components/governance/ProposalsListItem.vue
Normal file
|
@ -0,0 +1,89 @@
|
|||
<template>
|
||||
<div class="proposals-box" @click="onClick">
|
||||
<div class="columns is-gapless">
|
||||
<div class="column is-8-tablet is-9-desktop">
|
||||
<div class="title">
|
||||
{{ data.title }}
|
||||
</div>
|
||||
<div class="proposals-box--info">
|
||||
<div class="proposals-box--id">
|
||||
<span class="tag">{{ data.id }}</span>
|
||||
</div>
|
||||
<b-tag :type="getStatusType(data.status)">
|
||||
{{ $t(data.status) }}
|
||||
</b-tag>
|
||||
<div class="date">
|
||||
<span>{{ $t('startDate') }}:</span> {{ this.$moment.unix(data.startTime).format('l') }}
|
||||
</div>
|
||||
<div class="date">
|
||||
<span>{{ $t('endDate') }}:</span> {{ this.$moment.unix(data.endTime).format('l') }}
|
||||
</div>
|
||||
<div class="date">
|
||||
<span>{{ $t('quorum') }}:</span> {{ quorumPercent }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4-tablet is-3-desktop">
|
||||
<div class="results">
|
||||
<div class="result">
|
||||
<span class="has-text-primary"><b-icon icon="check" /> {{ $t('for') }}</span>
|
||||
<span><number-format :value="data.results.for" /> TORN</span>
|
||||
</div>
|
||||
<div class="result is-danger">
|
||||
<span class="has-text-danger"><b-icon icon="close" /> {{ $t('against') }}</span>
|
||||
<span><number-format :value="data.results.against" /> TORN</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import quorum from './mixins/quorum'
|
||||
import NumberFormat from '@/components/NumberFormat'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NumberFormat
|
||||
},
|
||||
mixins: [quorum],
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getStatusType(status) {
|
||||
let statusType = ''
|
||||
switch (status) {
|
||||
case 'awaitingExecution':
|
||||
case 'active':
|
||||
statusType = 'is-primary'
|
||||
break
|
||||
case 'expired':
|
||||
statusType = 'is-gray'
|
||||
break
|
||||
case 'failed':
|
||||
case 'defeated':
|
||||
statusType = 'is-danger'
|
||||
break
|
||||
case 'pending':
|
||||
case 'timeLocked':
|
||||
statusType = 'is-warning'
|
||||
break
|
||||
case 'executed':
|
||||
statusType = 'is-violet'
|
||||
break
|
||||
}
|
||||
return statusType
|
||||
},
|
||||
onClick() {
|
||||
if (this.data.status !== 'loading') {
|
||||
this.$router.push({ path: `/governance/${this.data.id}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
55
components/governance/ProposalsListSkeleton.vue
Normal file
55
components/governance/ProposalsListSkeleton.vue
Normal file
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-for="(item, index) in emptyArray" :key="index" class="proposals-box">
|
||||
<div class="columns is-gapless">
|
||||
<div class="column is-8-tablet is-9-desktop">
|
||||
<div class="title">
|
||||
<b-skeleton height="28" width="210"></b-skeleton>
|
||||
</div>
|
||||
<div class="proposals-box--info">
|
||||
<div class="proposals-box--id"><b-skeleton height="28" width="30"></b-skeleton></div>
|
||||
<b-skeleton height="28" width="70"></b-skeleton>
|
||||
<div class="date">
|
||||
<b-skeleton height="21" width="130"></b-skeleton>
|
||||
</div>
|
||||
<div class="date">
|
||||
<b-skeleton height="21" width="130"></b-skeleton>
|
||||
</div>
|
||||
<div class="date">
|
||||
<b-skeleton height="21" width="130"></b-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4-tablet is-3-desktop">
|
||||
<div class="results">
|
||||
<div class="result">
|
||||
<span class="has-text-primary"><b-icon icon="check" /> {{ $t('for') }}</span>
|
||||
<b-skeleton height="15" width="50"></b-skeleton>
|
||||
</div>
|
||||
<div class="result is-danger">
|
||||
<span class="has-text-danger"><b-icon icon="close" /> {{ $t('against') }}</span>
|
||||
<b-skeleton height="15" width="50"></b-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
size: {
|
||||
type: Number,
|
||||
default: 5
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
emptyArray: Array(this.size).fill('')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
60
components/governance/manage/ManageBox.vue
Normal file
60
components/governance/manage/ManageBox.vue
Normal file
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<div class="modal-card box box-modal has-delete">
|
||||
<button type="button" class="delete" @click="$emit('close')" />
|
||||
<b-tabs v-model="activeTab" :animated="false" class="is-modal">
|
||||
<LockTab />
|
||||
<UnlockTab />
|
||||
<DelegateTab />
|
||||
<UndelegateTab />
|
||||
<RewardTab />
|
||||
</b-tabs>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint-disable no-console */
|
||||
import { mapActions } from 'vuex'
|
||||
import { BigNumber as BN } from 'bignumber.js'
|
||||
|
||||
import { LockTab, UnlockTab, DelegateTab, UndelegateTab, RewardTab } from './tabs'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LockTab,
|
||||
UnlockTab,
|
||||
DelegateTab,
|
||||
UndelegateTab,
|
||||
RewardTab
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
close: this.$parent.close,
|
||||
formatNumber: this.formatNumber
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 0
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchTokenAllowance()
|
||||
},
|
||||
methods: {
|
||||
...mapActions('torn', ['fetchTokenAllowance']),
|
||||
formatNumber(value) {
|
||||
value = String(value).replace(',', '.')
|
||||
|
||||
let [amount, decimals] = value.split('.')
|
||||
|
||||
if (decimals && decimals.length > 18) {
|
||||
decimals = decimals.slice(0, 17)
|
||||
amount = new BN(`${amount}.${decimals}`)
|
||||
} else {
|
||||
amount = new BN(value)
|
||||
}
|
||||
|
||||
return isNaN(amount) ? '0' : amount.toString(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
82
components/governance/manage/tabs/DelegateTab.vue
Normal file
82
components/governance/manage/tabs/DelegateTab.vue
Normal file
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<b-tab-item :label="$t('delegate')">
|
||||
<div class="p">
|
||||
{{ $t('delegateTabDesc') }}
|
||||
</div>
|
||||
<b-field :label="$t('recipient')">
|
||||
<b-input
|
||||
v-model="delegatee"
|
||||
:placeholder="$t('address')"
|
||||
:size="!delegatee ? '' : isValidAddress ? 'is-primary' : 'is-warning'"
|
||||
></b-input>
|
||||
</b-field>
|
||||
<div class="label-with-value">
|
||||
{{ $t('currentDelegate') }}:
|
||||
<a target="_blank" :href="addressExplorerUrl(currentDelegate)">{{ delegateMsg }}</a>
|
||||
</div>
|
||||
<div>
|
||||
<b-tooltip
|
||||
class="is-block"
|
||||
:label="`${$t('pleaseLockBalance')}`"
|
||||
position="is-top"
|
||||
:active="!canDelegate"
|
||||
multilined
|
||||
>
|
||||
<b-button
|
||||
:disabled="!canDelegate || !isValidAddress"
|
||||
type="is-primary is-fullwidth"
|
||||
outlined
|
||||
@click="onDelegate"
|
||||
>
|
||||
{{ $t('delegate') }}
|
||||
</b-button>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
</b-tab-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isAddress } from 'web3-utils'
|
||||
import { BigNumber as BN } from 'bignumber.js'
|
||||
import { mapActions, mapState, mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
delegatee: ''
|
||||
}
|
||||
},
|
||||
inject: ['close'],
|
||||
computed: {
|
||||
...mapState('torn', ['signature', 'balance', 'allowance']),
|
||||
...mapState('governance/gov', ['lockedBalance', 'timestamp', 'currentDelegate']),
|
||||
...mapGetters('token', ['toDecimals']),
|
||||
...mapGetters('txHashKeeper', ['addressExplorerUrl']),
|
||||
isValidAddress() {
|
||||
return isAddress(this.delegatee)
|
||||
},
|
||||
canDelegate() {
|
||||
return new BN(this.lockedBalance).gt(new BN('0'))
|
||||
},
|
||||
canUndelegate() {
|
||||
return this.currentDelegate !== '0x0000000000000000000000000000000000000000'
|
||||
},
|
||||
delegateMsg() {
|
||||
if (!this.canUndelegate) {
|
||||
return this.$t('none')
|
||||
} else {
|
||||
return this.currentDelegate
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('governance/gov', ['delegate']),
|
||||
async onDelegate() {
|
||||
this.$store.dispatch('loading/enable', { message: this.$t('preparingTransactionData') })
|
||||
await this.delegate({ delegatee: this.delegatee })
|
||||
this.$store.dispatch('loading/disable')
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
147
components/governance/manage/tabs/LockTab.vue
Normal file
147
components/governance/manage/tabs/LockTab.vue
Normal file
|
@ -0,0 +1,147 @@
|
|||
<template>
|
||||
<b-tab-item :label="$t('lock')">
|
||||
<div class="p">
|
||||
{{ $t('lockTabDesc') }}
|
||||
</div>
|
||||
<b-field :label="$t('amountToLock')" expanded>
|
||||
<b-field :class="hasErrorAmount ? 'is-warning' : ''">
|
||||
<b-input
|
||||
v-model="computedAmountToLock"
|
||||
step="0.01"
|
||||
:min="minAmount"
|
||||
:max="maxAmountToLock"
|
||||
custom-class="hide-spinner"
|
||||
:use-html5-validation="false"
|
||||
:placeholder="$t('amount')"
|
||||
expanded
|
||||
></b-input>
|
||||
<div class="control has-button">
|
||||
<button
|
||||
class="button is-primary is-small is-outlined"
|
||||
@mousedown.prevent
|
||||
@click="setMaxAmountToLock"
|
||||
>
|
||||
{{ $t('max') }}
|
||||
</button>
|
||||
</div>
|
||||
</b-field>
|
||||
</b-field>
|
||||
<div class="label-with-value">
|
||||
{{ $t('availableBalance') }}: <span><number-format :value="maxAmountToLock" /> TORN</span>
|
||||
</div>
|
||||
<div class="buttons buttons__halfwidth">
|
||||
<b-button type="is-primary is-fullwidth" outlined :disabled="disabledApprove" @click="onApprove">
|
||||
{{ $t('approve') }}
|
||||
</b-button>
|
||||
<b-button type="is-primary is-fullwidth" outlined :disabled="disabledLock" @click="onLock">
|
||||
{{ $t('lock') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-tab-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'vuex'
|
||||
import { toWei, fromWei } from 'web3-utils'
|
||||
import { BigNumber as BN } from 'bignumber.js'
|
||||
|
||||
import { debounce } from '@/utils'
|
||||
import NumberFormat from '@/components/NumberFormat'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NumberFormat
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
amountToLock: '',
|
||||
minAmount: '0',
|
||||
hasErrorAmount: false
|
||||
}
|
||||
},
|
||||
inject: ['close', 'formatNumber'],
|
||||
computed: {
|
||||
...mapState('torn', ['signature', 'balance', 'allowance']),
|
||||
maxAmountToLock() {
|
||||
return fromWei(this.balance)
|
||||
},
|
||||
hasEnoughApproval() {
|
||||
if (Number(this.amountToLock) && new BN(this.allowance).gte(new BN(toWei(this.amountToLock)))) {
|
||||
return true
|
||||
}
|
||||
|
||||
return Boolean(this.signature.v) && this.signature.amount === this.amountToLock
|
||||
},
|
||||
disabledApprove() {
|
||||
if (!Number(this.amountToLock) || this.signature.amount === this.amountToLock) {
|
||||
return true
|
||||
}
|
||||
|
||||
const allowance = new BN(String(this.allowance))
|
||||
const amount = new BN(toWei(this.amountToLock))
|
||||
|
||||
if (allowance.isZero()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return allowance.gte(amount)
|
||||
},
|
||||
disabledLock() {
|
||||
return Number(this.amountToLock) && !this.hasEnoughApproval
|
||||
},
|
||||
computedAmountToLock: {
|
||||
get() {
|
||||
return this.amountToLock
|
||||
},
|
||||
set(value) {
|
||||
this.amountToLock = this.formatNumber(value)
|
||||
|
||||
debounce(this.validateLock, this.amountToLock)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('torn', ['signApprove']),
|
||||
...mapActions('governance/gov', ['lock', 'lockWithApproval']),
|
||||
async onApprove() {
|
||||
this.$store.dispatch('loading/enable', { message: this.$t('preparingTransactionData') })
|
||||
await this.signApprove({ amount: this.amountToLock })
|
||||
this.$store.dispatch('loading/disable')
|
||||
},
|
||||
async onLock() {
|
||||
this.$store.dispatch('loading/enable', { message: this.$t('preparingTransactionData') })
|
||||
if (this.signature.v) {
|
||||
await this.lock()
|
||||
} else {
|
||||
await this.lockWithApproval({ amount: this.amountToLock })
|
||||
}
|
||||
this.$store.dispatch('loading/disable')
|
||||
this.close()
|
||||
},
|
||||
setMaxAmountToLock() {
|
||||
this.computedAmountToLock = this.maxAmountToLock
|
||||
},
|
||||
validateLock(value) {
|
||||
this.amountToLock = this.validateAmount(value, this.maxAmountToLock)
|
||||
},
|
||||
validateAmount(value, maxAmount) {
|
||||
this.hasErrorAmount = false
|
||||
|
||||
let amount = new BN(value)
|
||||
|
||||
if (amount.isZero()) {
|
||||
amount = this.minAmount
|
||||
this.hasErrorAmount = true
|
||||
} else if (amount.lt(this.minAmount)) {
|
||||
amount = this.minAmount
|
||||
this.hasErrorAmount = true
|
||||
} else if (amount.gt(maxAmount)) {
|
||||
amount = maxAmount
|
||||
this.hasErrorAmount = true
|
||||
}
|
||||
|
||||
return amount.toString(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
43
components/governance/manage/tabs/RewardTab.vue
Normal file
43
components/governance/manage/tabs/RewardTab.vue
Normal file
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<b-tab-item :label="$t('stakingReward.label.tab')">
|
||||
<div class="p">
|
||||
{{ $t('stakingReward.description') }}
|
||||
</div>
|
||||
<div class="label-with-value">
|
||||
{{ $t('stakingReward.label.input') }}:
|
||||
<span><number-format :value="reward" /> TORN</span>
|
||||
</div>
|
||||
<b-button :disabled="notAvailableClaim" type="is-primary is-fullwidth" outlined @click="onClaim">
|
||||
{{ $t('stakingReward.action') }}
|
||||
</b-button>
|
||||
</b-tab-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import { BigNumber as BN } from 'bignumber.js'
|
||||
|
||||
import NumberFormat from '@/components/NumberFormat'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NumberFormat
|
||||
},
|
||||
inject: ['close'],
|
||||
computed: {
|
||||
...mapGetters('governance/staking', ['reward']),
|
||||
notAvailableClaim() {
|
||||
return BN(this.reward).isZero()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('governance/staking', ['claimReward']),
|
||||
async onClaim() {
|
||||
this.$store.dispatch('loading/enable', { message: this.$t('preparingTransactionData') })
|
||||
await this.claimReward()
|
||||
this.$store.dispatch('loading/disable')
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
53
components/governance/manage/tabs/UndelegateTab.vue
Normal file
53
components/governance/manage/tabs/UndelegateTab.vue
Normal file
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<b-tab-item :label="$t('undelegate')">
|
||||
<div class="p">
|
||||
{{ $t('undelegateTabDesc') }}
|
||||
</div>
|
||||
<div class="label-with-value">
|
||||
{{ $t('currentDelegate') }}:
|
||||
<a target="_blank" :href="addressExplorerUrl(currentDelegate)">{{ delegateMsg }}</a>
|
||||
</div>
|
||||
<b-tooltip
|
||||
class="is-block"
|
||||
:label="`${$t('pleaseDelegate')}`"
|
||||
position="is-top"
|
||||
:active="!canUndelegate"
|
||||
multilined
|
||||
>
|
||||
<b-button :disabled="!canUndelegate" type="is-primary is-fullwidth" outlined @click="onUndelegate">
|
||||
{{ $t('undelegate') }}
|
||||
</b-button>
|
||||
</b-tooltip>
|
||||
</b-tab-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState, mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
inject: ['close'],
|
||||
computed: {
|
||||
...mapState('governance/gov', ['currentDelegate']),
|
||||
...mapGetters('txHashKeeper', ['addressExplorerUrl']),
|
||||
canUndelegate() {
|
||||
return this.currentDelegate !== '0x0000000000000000000000000000000000000000'
|
||||
},
|
||||
delegateMsg() {
|
||||
if (!this.canUndelegate) {
|
||||
return this.$t('none')
|
||||
} else {
|
||||
return this.currentDelegate
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('governance/gov', ['undelegate']),
|
||||
async onUndelegate() {
|
||||
this.$store.dispatch('loading/enable', { message: this.$t('preparingTransactionData') })
|
||||
await this.undelegate()
|
||||
this.$store.dispatch('loading/disable')
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
137
components/governance/manage/tabs/UnlockTab.vue
Normal file
137
components/governance/manage/tabs/UnlockTab.vue
Normal file
|
@ -0,0 +1,137 @@
|
|||
<template>
|
||||
<b-tab-item :label="$t('unlock')">
|
||||
<div class="p">
|
||||
{{ $t('unlockTabDesc') }}
|
||||
</div>
|
||||
<b-field :label="$t('amountToUnlock')" expanded>
|
||||
<b-field :class="hasErrorAmount ? 'is-warning' : ''">
|
||||
<b-input
|
||||
v-model="computedAmountToUnlock"
|
||||
step="0.01"
|
||||
:min="minAmount"
|
||||
:max="maxAmountToUnlock"
|
||||
custom-class="hide-spinner"
|
||||
:placeholder="$t('amount')"
|
||||
:use-html5-validation="false"
|
||||
expanded
|
||||
></b-input>
|
||||
<div class="control has-button">
|
||||
<button
|
||||
class="button is-primary is-small is-outlined"
|
||||
@mousedown.prevent
|
||||
@click="setMaxAmountToUnlock"
|
||||
>
|
||||
{{ $t('max') }}
|
||||
</button>
|
||||
</div>
|
||||
</b-field>
|
||||
</b-field>
|
||||
<div class="label-with-value">
|
||||
{{ $t('lockedBalance') }}: <span><number-format :value="maxAmountToUnlock" /> TORN</span>
|
||||
</div>
|
||||
<b-tooltip
|
||||
class="is-block"
|
||||
:label="unlockMsgErr"
|
||||
position="is-top"
|
||||
:active="!hasLockedBalance || !canWithdraw"
|
||||
multilined
|
||||
>
|
||||
<b-button :disabled="disableUnlock" type="is-primary is-fullwidth" outlined @click="onUnlock">
|
||||
{{ $t('unlock') }}
|
||||
</b-button>
|
||||
</b-tooltip>
|
||||
</b-tab-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fromWei } from 'web3-utils'
|
||||
import { BigNumber as BN } from 'bignumber.js'
|
||||
import { mapActions, mapState, mapGetters } from 'vuex'
|
||||
|
||||
import { debounce } from '@/utils'
|
||||
import NumberFormat from '@/components/NumberFormat'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NumberFormat
|
||||
},
|
||||
inject: ['close', 'formatNumber'],
|
||||
data() {
|
||||
return {
|
||||
amountToUnlock: '',
|
||||
minAmount: '0',
|
||||
hasErrorAmount: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('torn', ['signature', 'balance', 'allowance']),
|
||||
...mapState('governance/gov', ['lockedBalance', 'timestamp', 'currentDelegate']),
|
||||
...mapGetters('token', ['toDecimals']),
|
||||
...mapGetters('txHashKeeper', ['addressExplorerUrl']),
|
||||
unlockMsgErr() {
|
||||
if (this.hasLockedBalance && !this.canWithdraw) {
|
||||
return this.$t('tokensLockedUntil', {
|
||||
date: this.$moment.unix(this.timestamp).format('llll')
|
||||
})
|
||||
} else {
|
||||
return this.$t('pleaseLockTornFirst')
|
||||
}
|
||||
},
|
||||
maxAmountToUnlock() {
|
||||
return fromWei(this.lockedBalance)
|
||||
},
|
||||
hasLockedBalance() {
|
||||
return !new BN(this.lockedBalance).isZero()
|
||||
},
|
||||
disableUnlock() {
|
||||
return !Number(this.amountToUnlock) || !this.hasLockedBalance || !this.canWithdraw
|
||||
},
|
||||
canWithdraw() {
|
||||
return Date.now() > Number(this.timestamp) * 1000
|
||||
},
|
||||
computedAmountToUnlock: {
|
||||
get() {
|
||||
return this.amountToUnlock
|
||||
},
|
||||
set(value) {
|
||||
this.amountToUnlock = this.formatNumber(value)
|
||||
|
||||
debounce(this.validateUnlock, this.amountToUnlock)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('governance/gov', ['unlock']),
|
||||
async onUnlock() {
|
||||
this.$store.dispatch('loading/enable', { message: this.$t('preparingTransactionData') })
|
||||
await this.unlock({ amount: this.amountToUnlock })
|
||||
this.$store.dispatch('loading/disable')
|
||||
this.close()
|
||||
},
|
||||
setMaxAmountToUnlock() {
|
||||
this.computedAmountToUnlock = this.maxAmountToUnlock
|
||||
},
|
||||
validateUnlock(value) {
|
||||
this.amountToUnlock = this.validateAmount(value, this.maxAmountToUnlock)
|
||||
},
|
||||
validateAmount(value, maxAmount) {
|
||||
this.hasErrorAmount = false
|
||||
|
||||
let amount = new BN(value)
|
||||
|
||||
if (amount.isZero()) {
|
||||
amount = this.minAmount
|
||||
this.hasErrorAmount = true
|
||||
} else if (amount.lt(this.minAmount)) {
|
||||
amount = this.minAmount
|
||||
this.hasErrorAmount = true
|
||||
} else if (amount.gt(maxAmount)) {
|
||||
amount = maxAmount
|
||||
this.hasErrorAmount = true
|
||||
}
|
||||
|
||||
return amount.toString(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
7
components/governance/manage/tabs/index.js
Normal file
7
components/governance/manage/tabs/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import LockTab from './LockTab'
|
||||
import RewardTab from './RewardTab'
|
||||
import UnlockTab from './UnlockTab'
|
||||
import DelegateTab from './DelegateTab'
|
||||
import UndelegateTab from './UndelegateTab'
|
||||
|
||||
export { LockTab, RewardTab, UndelegateTab, UnlockTab, DelegateTab }
|
30
components/governance/mixins/quorum.js
Normal file
30
components/governance/mixins/quorum.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { mapGetters } from 'vuex'
|
||||
const { toBN, fromWei, toWei } = require('web3-utils')
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters('governance/gov', ['quorumVotes']),
|
||||
results() {
|
||||
const resultFor = toBN(toWei(this.data.results.for))
|
||||
const resultAgainst = toBN(toWei(this.data.results.against))
|
||||
return resultFor.add(resultAgainst)
|
||||
},
|
||||
quorumResult() {
|
||||
return fromWei(this.results)
|
||||
},
|
||||
quorumVotesToWei() {
|
||||
return toBN(toWei(this.quorumVotes))
|
||||
},
|
||||
isQuorumCompleted() {
|
||||
return this.results.gte(this.quorumVotesToWei)
|
||||
},
|
||||
quorumPercent() {
|
||||
return this.isQuorumCompleted
|
||||
? 100
|
||||
: toBN('100')
|
||||
.mul(this.results)
|
||||
.div(this.quorumVotesToWei)
|
||||
.toNumber()
|
||||
}
|
||||
}
|
||||
}
|
97
components/icons/Error404Icon.vue
Normal file
97
components/icons/Error404Icon.vue
Normal file
|
@ -0,0 +1,97 @@
|
|||
<template>
|
||||
<svg class="errorIcon" viewBox="0 0 402 196" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M19 89L0.5 114V136H47.5L43.5 110.5L33.5 110L40.5 100.5C40.1667 99 38.3 95.2 33.5 92C28.7 88.8 21.8333 88.6667 19 89Z"
|
||||
fill="#182922"
|
||||
/>
|
||||
<path d="M66 36.5L59.5 34.5L73 17H99.5V75L82 59.5L66 36.5Z" fill="#182922" />
|
||||
<path
|
||||
d="M76.5 168.5L69.5 173.5V181H99.5V136H126.5V110.5H99.5V97L91.5 103L84.5 117L83.5 141.5L81.5 157L76.5 168.5Z"
|
||||
fill="#182922"
|
||||
/>
|
||||
<path d="M320 78.5L309 70L275.5 115V136H320L330 111L308.5 110L326 87L320 78.5Z" fill="#182922" />
|
||||
<path
|
||||
d="M327.5 55.5L320.5 54L348.5 17H374.5V42.5H362L354.5 45L345 51.5L336.5 55.5H327.5Z"
|
||||
fill="#182922"
|
||||
/>
|
||||
<path d="M362.5 150L344 158.5L345 181.5H374.5V148L362.5 150Z" fill="#182922" />
|
||||
<path
|
||||
d="M125.827 135.769H126.327V135.269V111.269V110.769H125.827H98.7913V17.5771V17.0771H98.2913H73.739H73.4888L73.3388 17.2775L0.59975 114.431L0.5 114.565V114.731V135.269V135.769H1H70.0266V180.269V180.769H70.5266H98.2913H98.7913V180.269V135.769H125.827ZM401 135.769H401.5V135.269V111.269V110.769H401H373.965V17.5771V17.0771H373.465H348.912H348.662L348.512 17.2775L275.773 114.431L275.673 114.565V114.731V135.269V135.769H276.173H345.2V180.269V180.769H345.7H373.465H373.965V180.269V135.769H401ZM70.0266 110.769H31.624L70.0266 61.4936V110.769ZM345.2 110.769H306.797L345.2 61.4936V110.769Z"
|
||||
stroke="#44F1A6"
|
||||
/>
|
||||
<path
|
||||
d="M60.481 34C64.0903 35.3589 67.2333 37.4449 69.5 40.5C78.612 52.7815 85.2128 68.202 99.5 74.8228M99.5 96.1312C92.2684 100.818 84.2893 109.613 84 123.5C83.5651 144.376 83.8868 167.521 69.5 173.194M48 136.282C44.2352 128.755 44.4352 119.12 43.4817 110.5M40.9125 99.5C39.2955 95.8589 36.73 92.88 32.5 91.0001C28.0552 89.0246 23.4277 88.6047 19 89.0797M309 69.8078C315.516 74.2604 322.053 79.9477 326.309 86.5M330.15 110C327.96 118.791 322.802 127.295 319.5 136.409M320.5 53.5727C325.17 55.5529 331.611 56.7962 338.5 54.5C349.932 50.6892 353.835 39.7068 374.5 42.2019M374.5 148.114C361.589 148.856 351.877 153.158 344.862 158.5"
|
||||
stroke="#44F1A6"
|
||||
/>
|
||||
<path
|
||||
d="M214.602 71.6778L199.102 192.322L230.693 110.442L251.904 54.124L227.767 67.2893L214.602 71.6778Z"
|
||||
fill="#1D3F30"
|
||||
/>
|
||||
<path d="M157.552 61.4379L137.073 50.4668L196.317 187.24L157.552 61.4379Z" fill="#182922" />
|
||||
<path d="M149.507 57.781L144.387 43.8843L167.061 57.781H149.507Z" fill="#276C4E" />
|
||||
<path
|
||||
d="M135.61 47.5419L198.511 193.823M198.511 193.823L253.367 51.199M198.511 193.823L214.602 71.6784M198.511 193.823L158.284 62.9015M141.461 37.3022C141.461 38.4725 146.825 50.7114 149.507 57.0502"
|
||||
stroke="#44F1A6"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M222.666 36.5972C222.64 43.8351 211.835 49.6982 198.511 49.6982L198.462 49.6981C185.144 49.6836 174.356 43.8117 174.356 36.5703L174.356 36.5455C174.381 29.3066 185.186 23.4425 198.511 23.4425L198.56 23.4426C211.878 23.457 222.666 29.329 222.666 36.5703L222.666 36.5972ZM235.787 56.4237C233.446 63.0515 225.269 68.5516 214.275 71.283L214.187 71.9896C238.975 68.263 257.174 55.916 257.174 41.2589C257.174 31.1572 248.529 22.1528 235.041 16.3114C252.562 18.1395 265.801 26.5147 265.801 36.5703C265.801 18.9625 239.536 4.68851 207.138 4.68851C188.551 4.68851 171.983 9.3866 161.235 16.717C164.598 7.19459 180.009 0 198.511 0C166.113 0 139.849 14.274 139.849 31.8818C139.849 41.9835 148.493 50.9879 161.981 56.8293C149.262 55.5022 138.8 50.725 134.02 44.3594L134 44.4577L135.865 49.0225C144.789 60.4428 165.615 68.4522 189.884 68.4522C208.471 68.4522 225.039 63.7541 235.787 56.4237Z"
|
||||
fill="#44F1A6"
|
||||
/>
|
||||
<path
|
||||
d="M222.496 38.9175C222.496 42.0722 219.962 45.0588 215.568 47.2865C211.203 49.5001 205.137 50.8838 198.409 50.8838C191.682 50.8838 185.616 49.5001 181.25 47.2865C176.857 45.0588 174.323 42.0722 174.323 38.9175C174.323 35.7627 176.857 32.7761 181.25 30.5484C185.616 28.3349 191.682 26.9512 198.409 26.9512C205.137 26.9512 211.203 28.3349 215.568 30.5484C219.962 32.7761 222.496 35.7627 222.496 38.9175Z"
|
||||
stroke="#44F1A6"
|
||||
/>
|
||||
<path
|
||||
d="M219.725 42.7263C219.725 45.6026 217.49 48.328 213.604 50.3638C209.741 52.3872 204.369 53.6537 198.409 53.6537C192.449 53.6537 187.078 52.3872 183.215 50.3638C179.328 48.328 177.093 45.6026 177.093 42.7263C177.093 39.8499 179.328 37.1246 183.215 35.0888C187.078 33.0654 192.449 31.7988 198.409 31.7988C204.369 31.7988 209.741 33.0654 213.604 35.0888C217.49 37.1246 219.725 39.8499 219.725 42.7263Z"
|
||||
stroke="#44F1A6"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M167.61 94.5294L167.267 93.4227C173.991 95.0115 181.491 95.9004 189.406 95.9004C197.416 95.9004 205.002 94.9899 211.788 93.3648L211.647 94.4261C204.871 96.0159 197.338 96.9004 189.406 96.9004C181.647 96.9004 174.27 96.0541 167.61 94.5294ZM171.287 106.37L170.952 105.29C176.691 106.383 182.909 106.982 189.406 106.982C196.822 106.982 203.873 106.202 210.264 104.796L210.124 105.851C203.749 107.224 196.748 107.982 189.406 107.982C183.049 107.982 176.948 107.414 171.287 106.37ZM174.462 116.593L174.133 115.532C178.964 116.276 184.091 116.677 189.406 116.677C196.311 116.677 202.9 116.001 208.934 114.773L208.794 115.821C202.781 117.02 196.241 117.677 189.406 117.677C184.221 117.677 179.206 117.299 174.462 116.593ZM177.155 125.266L176.83 124.219C180.858 124.721 185.07 124.988 189.406 124.988C195.879 124.988 202.075 124.394 207.796 123.308L207.657 124.352C201.959 125.411 195.812 125.988 189.406 125.988C185.192 125.988 181.091 125.739 177.155 125.266ZM179.824 133.861L179.503 132.826C182.709 133.137 186.02 133.299 189.406 133.299C195.453 133.299 201.258 132.78 206.66 131.827L206.521 132.866C201.146 133.796 195.389 134.299 189.406 134.299C186.136 134.299 182.934 134.149 179.824 133.861ZM182.031 140.967L181.713 139.941C184.223 140.128 186.792 140.225 189.406 140.225C195.102 140.225 200.583 139.765 205.715 138.914L205.577 139.95C200.473 140.779 195.04 141.225 189.406 141.225C186.904 141.225 184.442 141.137 182.031 140.967ZM184.004 147.32L183.688 146.302C185.566 146.405 187.473 146.458 189.406 146.458C194.789 146.458 199.98 146.047 204.866 145.284L204.728 146.317C199.872 147.06 194.729 147.458 189.406 147.458C187.583 147.458 185.781 147.411 184.004 147.32ZM198.721 191.368C198.385 191.399 198.048 191.427 197.71 191.454L197.406 190.475C197.891 190.437 198.375 190.396 198.857 190.352L198.721 191.368ZM204.112 150.94C199.447 151.629 194.513 151.999 189.406 151.999C188.069 151.999 186.745 151.974 185.434 151.924L185.748 152.936C186.957 152.978 188.177 152.999 189.406 152.999C194.454 152.999 199.341 152.641 203.974 151.971L204.112 150.94ZM203.453 155.883C198.983 156.511 194.273 156.847 189.406 156.847C188.584 156.847 187.766 156.838 186.954 156.819L187.267 157.826C187.976 157.84 188.689 157.847 189.406 157.847C194.215 157.847 198.879 157.522 203.315 156.911L203.453 155.883ZM202.888 160.115C198.587 160.693 194.068 161.002 189.406 161.002C189.02 161.002 188.635 161 188.251 160.996L188.562 161.999C188.843 162.001 189.124 162.002 189.406 162.002C194.012 162.002 198.484 161.704 202.751 161.142L202.888 160.115ZM202.324 164.345C198.235 164.87 193.954 165.152 189.543 165.158L189.854 166.157C194.1 166.139 198.23 165.868 202.188 165.37L202.324 164.345ZM201.855 167.867C198.242 168.313 194.481 168.569 190.617 168.614L190.926 169.61C194.629 169.557 198.239 169.311 201.718 168.891L201.855 167.867ZM201.479 170.683C198.249 171.068 194.904 171.303 191.473 171.37L191.781 172.364C195.053 172.291 198.248 172.067 201.343 171.706L201.479 170.683ZM201.104 173.497C198.26 173.826 195.327 174.037 192.327 174.121L192.635 175.112C195.477 175.025 198.261 174.825 200.968 174.519L201.104 173.497ZM200.729 176.31C198.273 176.584 195.751 176.771 193.179 176.863L193.486 177.852C195.903 177.759 198.275 177.583 200.593 177.331L200.729 176.31ZM200.448 178.419C198.284 178.654 196.071 178.822 193.816 178.917L194.123 179.905C196.223 179.811 198.288 179.654 200.312 179.44L200.448 178.419ZM200.167 180.527C198.297 180.724 196.39 180.871 194.452 180.965L194.759 181.951C196.543 181.86 198.302 181.725 200.031 181.546L200.167 180.527ZM199.886 182.633C198.311 182.795 196.71 182.922 195.087 183.01L195.393 183.995C196.864 183.911 198.317 183.796 199.75 183.652L199.886 182.633ZM199.605 184.74C198.327 184.868 197.031 184.972 195.721 185.052L196.027 186.035C197.186 185.961 198.334 185.869 199.469 185.758L199.605 184.74ZM199.418 186.143C198.338 186.249 197.246 186.338 196.143 186.41L196.448 187.392C197.401 187.328 198.346 187.251 199.282 187.161L199.418 186.143ZM199.231 187.546C198.35 187.631 197.461 187.705 196.564 187.767L196.869 188.748C197.616 188.694 198.358 188.633 199.095 188.564L199.231 187.546ZM199.044 188.949C198.362 189.013 197.676 189.071 196.985 189.122L197.29 190.102C197.832 190.061 198.371 190.015 198.908 189.966L199.044 188.949ZM213.557 80.1028C206.299 81.9885 198.091 83.049 189.406 83.049C179.78 83.049 170.741 81.7464 162.931 79.4614L162.574 78.312C170.422 80.6835 179.596 82.049 189.406 82.049C198.177 82.049 206.439 80.9575 213.699 79.0314L213.557 80.1028Z"
|
||||
fill="url(#paint0_linear)"
|
||||
/>
|
||||
<path
|
||||
d="M142.29 145.388C132.091 140.383 122.569 134.775 117 130M159.691 152.492C176.741 158.119 197.102 161.071 219 157.333"
|
||||
stroke="#44F1A6"
|
||||
/>
|
||||
<path
|
||||
d="M263 80C260.456 82.4914 254.977 86.9979 248.429 91.605M227 104C231.938 101.951 237.321 98.9194 242.429 95.6425"
|
||||
stroke="#44F1A6"
|
||||
/>
|
||||
<path
|
||||
d="M117.282 21.6163C116.028 29.8285 116.689 42.8176 124.908 54.8036M165.921 79.6946C148.556 75.5121 137.059 68.5158 129.587 60.5941"
|
||||
stroke="#44F1A6"
|
||||
/>
|
||||
<path
|
||||
d="M245 125C237.768 129.445 227.895 133.525 217.03 136.05M206 138C207.986 137.761 209.959 137.461 211.909 137.108"
|
||||
stroke="#44F1A6"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear"
|
||||
x1="188.137"
|
||||
y1="78.312"
|
||||
x2="188.137"
|
||||
y2="191.454"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#44F1A6" />
|
||||
<stop offset="0.458333" stop-color="#44F1A6" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.errorIcon {
|
||||
max-width: 402px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
height: auto;
|
||||
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
</style>
|
10
components/icons/EthPurchaseArrow.vue
Normal file
10
components/icons/EthPurchaseArrow.vue
Normal file
|
@ -0,0 +1,10 @@
|
|||
<template>
|
||||
<svg class="arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30.71 150">
|
||||
<polygon fill="#000403" points="30.71 0.5 30.34 0.5 0.54 75 30.34 149.5 30.71 149.5 30.71 0.5" />
|
||||
<polygon
|
||||
class="arrow-border"
|
||||
fill="#666"
|
||||
points="30.71 149 30.68 149 1.08 75 30.68 1 30.71 1 30.71 0 30 0 0 75 30 150 30.71 150 30.71 149"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
26
components/icons/FlagIcon.vue
Normal file
26
components/icons/FlagIcon.vue
Normal file
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<i v-if="code" class="flag-icon" :class="flagIconClass"></i>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FlagIcon',
|
||||
props: {
|
||||
code: { type: String, default: null }
|
||||
},
|
||||
computed: {
|
||||
flagIconClass() {
|
||||
let code = this.code
|
||||
switch (code) {
|
||||
case 'zh':
|
||||
code = 'cn'
|
||||
break
|
||||
case 'en':
|
||||
code = 'gb'
|
||||
break
|
||||
}
|
||||
return 'flag-icon-' + code
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
9
components/icons/LinkIcon.vue
Normal file
9
components/icons/LinkIcon.vue
Normal file
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 40 40">
|
||||
<path
|
||||
fill="#94FEBF"
|
||||
fill-rule="evenodd"
|
||||
d="M36 40H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4h8a2 2 0 1 1 0 4H6a2 2 0 0 0-2 2v28a2 2 0 0 0 2 2h28a2 2 0 0 0 2-2v-6a2 2 0 1 1 4 0v8a4 4 0 0 1-4 4zm2-22a2 2 0 0 1-2-2V6.801l-3.601 3.602-6.162 6.162-3.834 3.834-7 7a2.004 2.004 0 0 1-2.833-2.833l7-7L33.136 4H24a2 2 0 1 1 0-4h13.897c.083-.004.161.008.243.014.165.012.324.043.476.093.054.018.107.027.159.049.227.096.431.235.606.403.005.005.012.006.018.011A1.993 1.993 0 0 1 40 2v14a2 2 0 0 1-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
9
components/icons/SettingsIcon.vue
Normal file
9
components/icons/SettingsIcon.vue
Normal file
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 40 34">
|
||||
<path
|
||||
fill="#94FEBF"
|
||||
fill-rule="evenodd"
|
||||
d="M38 7H22.859c-.447 1.722-1.997 3-3.859 3h-8c-1.862 0-3.412-1.278-3.859-3H2a2 2 0 1 1 0-4h5.141C7.588 1.278 9.138 0 11 0h8c1.862 0 3.412 1.278 3.859 3H38a2 2 0 1 1 0 4zM18 4h-6a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2zM2 27h15.141c.447-1.722 1.997-3 3.859-3h8c1.862 0 3.412 1.278 3.859 3H38a2 2 0 1 1 0 4h-5.141c-.447 1.722-1.997 3-3.859 3h-8c-1.862 0-3.412-1.278-3.859-3H2a2 2 0 1 1 0-4zm20 3h6a1 1 0 0 0 0-2h-6a1 1 0 0 0 0 2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
34
components/icons/Verified.vue
Normal file
34
components/icons/Verified.vue
Normal file
File diff suppressed because one or more lines are too long
5
components/icons/index.js
Normal file
5
components/icons/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export { default as LinkIcon } from './LinkIcon'
|
||||
export { default as SettingsIcon } from './SettingsIcon'
|
||||
export { default as EthPurchaseArrow } from './EthPurchaseArrow'
|
||||
export { default as FlagIcon } from './FlagIcon'
|
||||
export { default as Error404Icon } from './Error404Icon'
|
148
components/settings/EthPurchase.vue
Normal file
148
components/settings/EthPurchase.vue
Normal file
|
@ -0,0 +1,148 @@
|
|||
<template>
|
||||
<b-field :type="type" :message="error">
|
||||
<template slot="label">
|
||||
{{ $t('ethPurchase', { currency }) }}
|
||||
<b-tooltip
|
||||
:label="$t('ethPurchaseTooltip', { currency: selectedStatisticCurrency, networkCurrency: currency })"
|
||||
size="is-small"
|
||||
position="is-right"
|
||||
multilined
|
||||
>
|
||||
<button class="button is-primary has-icon">
|
||||
<span class="icon icon-info"></span>
|
||||
</button>
|
||||
</b-tooltip>
|
||||
</template>
|
||||
<div
|
||||
class="field has-eth-purchase"
|
||||
:class="[type, { 'is-disabled': disabled }]"
|
||||
@click="onEthPurchaseClick"
|
||||
>
|
||||
<div class="columns is-mobile">
|
||||
<div class="column currency-container is-light">
|
||||
<div class="currency">{{ currency }}</div>
|
||||
<b-input
|
||||
ref="input"
|
||||
v-model.number="newValue"
|
||||
type="number"
|
||||
step="0.01"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:disabled="disabled"
|
||||
:use-html5-validation="false"
|
||||
expanded
|
||||
custom-class="hide-spinner"
|
||||
@input="onInput"
|
||||
@focus="$emit('focus', $event)"
|
||||
@blur="$emit('blur', $event)"
|
||||
/>
|
||||
<div class="withdraw-data">
|
||||
<div class="withdraw-data-item">
|
||||
{{ $t('rate') }}
|
||||
<span> {{ toDecimals(tokenRate, 18, 6) }} {{ currency }}/{{ selectedStatisticCurrency }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column arrow-container">
|
||||
<EthPurchaseArrow />
|
||||
</div>
|
||||
<div class="column currency-container is-inverted">
|
||||
<div class="currency">{{ selectedStatisticCurrency }}</div>
|
||||
<div class="input">{{ ethToReceiveInToken }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-field>
|
||||
</template>
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import { EthPurchaseArrow } from '@/components/icons'
|
||||
import { debounce } from '@/utils'
|
||||
const { toBN, toWei } = require('web3-utils')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EthPurchaseArrow
|
||||
},
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
value: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
defaultEthToReceive: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
type: '',
|
||||
min: 0,
|
||||
error: '',
|
||||
newValue: this.value
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('application', ['selectedStatistic']),
|
||||
...mapGetters('application', ['maxEthToReceive', 'selectedStatisticCurrency']),
|
||||
...mapGetters('token', ['toDecimals']),
|
||||
...mapGetters('metamask', ['networkConfig', 'currency']),
|
||||
...mapGetters('price', ['tokenRate']),
|
||||
max() {
|
||||
return Math.max(0, Number(this.toDecimals(this.maxEthToReceive, 18, 5)))
|
||||
},
|
||||
ethToReceiveInToken() {
|
||||
const { currency } = this.selectedStatistic
|
||||
const { decimals } = this.networkConfig.tokens[currency]
|
||||
const price = this.tokenRate
|
||||
|
||||
const ethToReceive = toBN(toWei(Math.min(Math.max(this.min, this.newValue), this.max).toString()))
|
||||
return this.toDecimals(ethToReceive.mul(toBN(10 ** decimals)).div(toBN(price)), null, 6)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(value) {
|
||||
this.newValue = value
|
||||
},
|
||||
newValue(value) {
|
||||
debounce(this.validateEthToReceive, value)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.validateEthToReceive(this.newValue)
|
||||
},
|
||||
methods: {
|
||||
onEthPurchaseClick() {
|
||||
this.$refs.input.focus()
|
||||
},
|
||||
onInput(value) {
|
||||
const parsedValue = parseFloat(value)
|
||||
if (!Number.isNaN(parsedValue)) {
|
||||
this.$emit('input', parsedValue)
|
||||
}
|
||||
},
|
||||
validateEthToReceive(value) {
|
||||
let type = ''
|
||||
let error = ''
|
||||
|
||||
if (value === '') {
|
||||
type = 'is-warning'
|
||||
error = this.$t('incorrectAmount')
|
||||
} else if (value < 0) {
|
||||
type = 'is-warning'
|
||||
error = this.$t('amountIsLow', { value: this.min })
|
||||
} else if (value > this.max) {
|
||||
type = 'is-warning'
|
||||
error = this.$t('amountIsHigh', { value: this.max })
|
||||
} else if (value === this.defaultEthToReceive) {
|
||||
type = 'is-primary'
|
||||
}
|
||||
|
||||
this.error = error
|
||||
this.type = type
|
||||
this.$emit('isValidEthToReceive', type !== 'is-warning')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
66
components/settings/SettingsModalBox.vue
Normal file
66
components/settings/SettingsModalBox.vue
Normal file
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<div class="modal-card box box-modal">
|
||||
<header class="box-modal-header is-spaced">
|
||||
<div class="box-modal-title">{{ $t('withdrawalSettings') }}</div>
|
||||
<button type="button" class="delete" @click="$parent.cancel('escape')" />
|
||||
</header>
|
||||
<b-tabs v-model="withdrawType" :animated="false" class="is-modal">
|
||||
<RelayerTab />
|
||||
<WalletTab />
|
||||
</b-tabs>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint-disable no-console */
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
|
||||
import { RelayerTab, WalletTab } from '@/components/settings/tabs'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RelayerTab,
|
||||
WalletTab
|
||||
},
|
||||
props: {
|
||||
currency: {
|
||||
type: String,
|
||||
default: 'ETH'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'withdrawalSettings'
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
currency: this.currency,
|
||||
save: this.save,
|
||||
reset: this.reset
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
withdrawType: 'relayer'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('application', {
|
||||
defaultWithdrawType: 'withdrawType'
|
||||
})
|
||||
},
|
||||
created() {
|
||||
this.withdrawType = this.defaultWithdrawType
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('application', ['SET_WITHDRAW_TYPE']),
|
||||
reset() {
|
||||
this.withdrawType = 'relayer'
|
||||
this.$root.$emit('resetSettings')
|
||||
},
|
||||
save() {
|
||||
this.SET_WITHDRAW_TYPE({ withdrawType: this.withdrawType })
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
259
components/settings/tabs/RelayerTab.vue
Normal file
259
components/settings/tabs/RelayerTab.vue
Normal file
|
@ -0,0 +1,259 @@
|
|||
<template>
|
||||
<b-tab-item :label="$t('relayer')" value="relayer">
|
||||
<div class="field">
|
||||
<b-field :label="$t('relayer')">
|
||||
<b-dropdown v-model="selectedRelayer" expanded aria-role="list" @change="onChangeRelayer">
|
||||
<div slot="trigger" class="control" :class="{ 'is-loading': checkingRelayer || isLoadingRelayers }">
|
||||
<div class="input">
|
||||
<span>{{ dropdownValue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<b-dropdown-item
|
||||
v-for="{ name, tornadoServiceFee } in relayers"
|
||||
v-show="!isLoadingRelayers"
|
||||
:key="name"
|
||||
:value="name"
|
||||
aria-role="listitem"
|
||||
>
|
||||
{{ getRelayerName({ name, tornadoServiceFee }) }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item value="custom" aria-role="listitem">
|
||||
{{ $t('custom') }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</b-field>
|
||||
<div v-if="isCustomRelayer" class="field has-custom-field">
|
||||
<b-input
|
||||
ref="customInput"
|
||||
v-model="customRelayerUrl"
|
||||
type="url"
|
||||
:placeholder="$t('pasteYourRelayerUrlorEnsRecord')"
|
||||
:custom-class="hasErrorRelayer.type"
|
||||
:use-html5-validation="false"
|
||||
@input="onInputCustomRelayer"
|
||||
></b-input>
|
||||
</div>
|
||||
<div class="withdraw-data is-spaced">
|
||||
<div class="withdraw-data-item">
|
||||
{{ $t('relayerFee') }}
|
||||
<span> {{ relayer.tornadoServiceFee }}% </span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="hasErrorRelayer.msg" class="help" :class="hasErrorRelayer.type">
|
||||
{{ hasErrorRelayer.msg }}
|
||||
</p>
|
||||
</div>
|
||||
<eth-purchase
|
||||
v-if="isEnabledEthPurchase"
|
||||
v-model="ethToReceive"
|
||||
:default-eth-to-receive="defaultEthToReceive"
|
||||
@isValidEthToReceive="ethToReceiveErrorHandler"
|
||||
/>
|
||||
<WithdrawTotal
|
||||
:currency="currency"
|
||||
withdraw-type="relayer"
|
||||
:eth-to-receive="ethToReceiveToWei"
|
||||
:service-fee="relayer.tornadoServiceFee"
|
||||
/>
|
||||
<div class="buttons buttons__halfwidth mt-5">
|
||||
<b-button type="is-primary" outlined @mousedown.prevent @click="onReset">
|
||||
{{ $t('reset') }}
|
||||
</b-button>
|
||||
<b-button
|
||||
type="is-primary"
|
||||
:disabled="isDisabledSave"
|
||||
:loading="checkingRelayer || isLoadingRelayers"
|
||||
@click="onSave"
|
||||
>
|
||||
{{ $t('save') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-tab-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters, mapMutations } from 'vuex'
|
||||
import EthPurchase from '@/components/settings/EthPurchase'
|
||||
import WithdrawTotal from '@/components/withdraw/WithdrawTotal'
|
||||
import { debounce } from '@/utils'
|
||||
const { fromWei, toWei } = require('web3-utils')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EthPurchase,
|
||||
WithdrawTotal
|
||||
},
|
||||
inject: ['currency', 'save'],
|
||||
data() {
|
||||
return {
|
||||
selectedRelayer: 'custom',
|
||||
checkingRelayer: false,
|
||||
customRelayerUrl: '',
|
||||
hasErrorRelayer: { type: '', msg: '' },
|
||||
isValidEthToReceive: true,
|
||||
isValidRelayer: true,
|
||||
ethToReceive: 0.02,
|
||||
relayer: {
|
||||
name: 'custom',
|
||||
url: '',
|
||||
address: '',
|
||||
tornadoServiceFee: 0.0,
|
||||
miningServiceFee: 0.0,
|
||||
ethPrices: {
|
||||
torn: '1'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('token', ['toDecimals']),
|
||||
...mapGetters('metamask', ['networkConfig', 'nativeCurrency']),
|
||||
...mapGetters('metamask', {
|
||||
networkCurrency: 'currency'
|
||||
}),
|
||||
...mapState('relayer', ['isLoadingRelayers']),
|
||||
...mapState('relayer', {
|
||||
defaultRelayer: (state) => state.selectedRelayer,
|
||||
relayers: (state) => state.validRelayers
|
||||
}),
|
||||
...mapState('application', {
|
||||
ethToReceiveFromStore: (state) => Number(fromWei(state.ethToReceive)),
|
||||
defaultEthToReceive: (state) => Number(fromWei(state.defaultEthToReceive))
|
||||
}),
|
||||
isEnabledEthPurchase() {
|
||||
return this.currency.toLowerCase() !== this.nativeCurrency
|
||||
},
|
||||
ethToReceiveToWei() {
|
||||
return toWei(this.ethToReceive.toString())
|
||||
},
|
||||
isCustomRelayer() {
|
||||
return this.selectedRelayer === 'custom'
|
||||
},
|
||||
dropdownValue() {
|
||||
if (this.isLoadingRelayers) {
|
||||
return this.$t('loading')
|
||||
}
|
||||
if (this.isCustomRelayer) {
|
||||
return this.$t('custom')
|
||||
}
|
||||
return this.selectedRelayer
|
||||
},
|
||||
isDisabledSave() {
|
||||
return !this.isValidRelayer || this.hasErrorRelayer.type === 'is-warning' || !this.isValidEthToReceive
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
defaultRelayer: {
|
||||
handler(relayer) {
|
||||
this.setRelayer(relayer)
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.setEthToReceive(this.ethToReceiveFromStore)
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on('resetSettings', this.onReset)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$root.$off('resetSettings', this.onReset)
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('relayer', ['SET_SELECTED_RELAYER']),
|
||||
...mapMutations('application', ['SAVE_ETH_TO_RECEIVE']),
|
||||
getRelayerName({ name, tornadoServiceFee }) {
|
||||
return `${name} - ${tornadoServiceFee}%`
|
||||
},
|
||||
onInputCustomRelayer(url) {
|
||||
const trimmedUrl = url.toLowerCase().trim()
|
||||
|
||||
if (!trimmedUrl) {
|
||||
this.hasErrorRelayer = { type: '', msg: '' }
|
||||
this.isValidRelayer = false
|
||||
return
|
||||
}
|
||||
if (
|
||||
window.location.protocol !== 'http:' &&
|
||||
trimmedUrl.startsWith('http:') &&
|
||||
!trimmedUrl.includes('.onion')
|
||||
) {
|
||||
this.hasErrorRelayer.type = 'is-warning'
|
||||
this.hasErrorRelayer.msg = this.$t('relayerShouldSupportSSL')
|
||||
this.isValidRelayer = false
|
||||
return
|
||||
}
|
||||
|
||||
debounce(this.checkRelayer, { url, name: 'custom' })
|
||||
},
|
||||
onChangeRelayer(keyName) {
|
||||
if (keyName === 'custom') {
|
||||
this.hasErrorRelayer = { type: '', msg: '' }
|
||||
this.isValidRelayer = false
|
||||
this.customRelayerUrl = ''
|
||||
return
|
||||
}
|
||||
|
||||
const { realUrl: url, name } = this.relayers.find((el) => el.name === keyName)
|
||||
|
||||
debounce(this.checkRelayer, { url, name })
|
||||
},
|
||||
async checkRelayer({ url, name }) {
|
||||
this.hasErrorRelayer = { type: '', msg: '' }
|
||||
this.isValidRelayer = false
|
||||
this.checkingRelayer = true
|
||||
|
||||
const { isValid, error, ...relayer } = await this.$store.dispatch('relayer/setupRelayer', {
|
||||
name,
|
||||
url
|
||||
})
|
||||
|
||||
if (isValid) {
|
||||
this.hasErrorRelayer.type = 'is-primary'
|
||||
this.hasErrorRelayer.msg = this.$t('relayerStatusOk')
|
||||
this.relayer = relayer
|
||||
} else {
|
||||
this.hasErrorRelayer.type = 'is-warning'
|
||||
this.hasErrorRelayer.msg = error
|
||||
}
|
||||
|
||||
this.checkingRelayer = false
|
||||
this.isValidRelayer = isValid
|
||||
},
|
||||
ethToReceiveErrorHandler(value) {
|
||||
this.isValidEthToReceive = value
|
||||
},
|
||||
setEthToReceive(ethToReceive) {
|
||||
if (this.isEnabledEthPurchase) {
|
||||
this.ethToReceive = ethToReceive
|
||||
}
|
||||
},
|
||||
setRelayer(relayer) {
|
||||
this.relayer = relayer
|
||||
this.selectedRelayer = relayer.name
|
||||
if (this.selectedRelayer === 'custom') {
|
||||
this.customRelayerUrl = relayer.url
|
||||
}
|
||||
},
|
||||
onReset() {
|
||||
this.hasErrorRelayer = { type: '', msg: '' }
|
||||
this.checkingRelayer = false
|
||||
this.isValidRelayer = true
|
||||
|
||||
this.setRelayer(this.defaultRelayer)
|
||||
this.setEthToReceive(this.defaultEthToReceive)
|
||||
},
|
||||
onSave() {
|
||||
this.SET_SELECTED_RELAYER(this.relayer)
|
||||
|
||||
if (this.isEnabledEthPurchase) {
|
||||
this.SAVE_ETH_TO_RECEIVE({
|
||||
ethToReceive: this.ethToReceiveToWei
|
||||
})
|
||||
}
|
||||
|
||||
this.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
52
components/settings/tabs/WalletTab.vue
Normal file
52
components/settings/tabs/WalletTab.vue
Normal file
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<b-tab-item :label="$t('wallet')" value="wallet">
|
||||
<fieldset :disabled="isNotLoggedIn">
|
||||
<div class="notice is-warning">
|
||||
<div class="notice__p">{{ $t('withdrawWalletWarning', { currency: networkCurrency }) }}</div>
|
||||
<div v-if="isNotLoggedIn" class="tooltip" :data-label="$t('connectYourWalletFirst')"></div>
|
||||
</div>
|
||||
<WithdrawTotal :currency="currency" withdraw-type="wallet" />
|
||||
</fieldset>
|
||||
<div class="buttons buttons__halfwidth mt-5">
|
||||
<b-button type="is-primary" outlined @mousedown.prevent @click="onReset">
|
||||
{{ $t('reset') }}
|
||||
</b-button>
|
||||
<connect-button v-if="isNotLoggedIn" />
|
||||
<b-button v-else type="is-primary" @click="onSave">
|
||||
{{ $t('save') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-tab-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import { ConnectButton } from '@/components/web3Connect'
|
||||
import WithdrawTotal from '@/components/withdraw/WithdrawTotal'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConnectButton,
|
||||
WithdrawTotal
|
||||
},
|
||||
inject: ['save', 'reset', 'currency'],
|
||||
computed: {
|
||||
...mapGetters('metamask', ['isLoggedIn']),
|
||||
...mapGetters('metamask', {
|
||||
networkCurrency: 'currency'
|
||||
}),
|
||||
isNotLoggedIn() {
|
||||
return !this.isLoggedIn
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onReset() {
|
||||
this.reset()
|
||||
},
|
||||
onSave() {
|
||||
this.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
4
components/settings/tabs/index.js
Normal file
4
components/settings/tabs/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import WalletTab from './WalletTab'
|
||||
import RelayerTab from './RelayerTab'
|
||||
|
||||
export { WalletTab, RelayerTab }
|
60
components/web3Connect/Button.vue
Normal file
60
components/web3Connect/Button.vue
Normal file
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<b-button :type="type" v-bind="$attrs" @click="onLogIn">{{ $t(actionText) }}</b-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
import Web3Connect from './Modal'
|
||||
|
||||
import { detectMob } from '@/utils'
|
||||
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'is-primary'
|
||||
},
|
||||
actionText: {
|
||||
type: String,
|
||||
default: () => 'connect'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasInjectedProvider() {
|
||||
return Boolean(window.ethereum)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('metamask', ['initialize']),
|
||||
async web3Connect(name) {
|
||||
this.$store.dispatch('loading/enable', {})
|
||||
try {
|
||||
await this.initialize({
|
||||
providerName: name
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
this.$store.dispatch('loading/disable')
|
||||
},
|
||||
onLogIn() {
|
||||
if (detectMob() && this.hasInjectedProvider) {
|
||||
this.web3Connect('mobileWallet')
|
||||
return
|
||||
}
|
||||
|
||||
this.$buefy.modal.open({
|
||||
parent: this,
|
||||
component: Web3Connect,
|
||||
hasModalCard: true,
|
||||
width: 440,
|
||||
props: {
|
||||
web3Connect: this.web3Connect
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
70
components/web3Connect/Modal.vue
Normal file
70
components/web3Connect/Modal.vue
Normal file
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<div class="modal-card box box-modal is-wallet-modal">
|
||||
<header class="box-modal-header is-spaced">
|
||||
<div class="box-modal-title">{{ $t('yourWallet') }}</div>
|
||||
<button type="button" class="delete" @click="$emit('close')" />
|
||||
</header>
|
||||
<div class="note">
|
||||
{{ $t('pleaseSelectYourWeb3Wallet') }}
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-centered is-grouped-multiline wallets">
|
||||
<div class="control">
|
||||
<button
|
||||
v-show="isGeneric"
|
||||
class="button is-small is-background is-generic"
|
||||
@click="_web3Connect('generic')"
|
||||
>
|
||||
{{ $t('otherWallet') }}
|
||||
</button>
|
||||
<button v-show="!isMetamask" class="button is-small is-dark is-metamask" @click="onBoarding">
|
||||
Install Metamask
|
||||
</button>
|
||||
<button
|
||||
v-show="isMetamask"
|
||||
class="button is-small is-background is-metamask"
|
||||
@click="_web3Connect('metamask')"
|
||||
>
|
||||
Metamask
|
||||
</button>
|
||||
<button class="button is-small is-background is-walletConnect" @click="_web3Connect('walletConnect')">
|
||||
WalletConnect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Metamask from '@metamask/onboarding'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
web3Connect: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isMetamask() {
|
||||
return window.ethereum?.isMetaMask
|
||||
},
|
||||
isGeneric() {
|
||||
return !this.isMetamask && window.ethereum
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.isMetamask) {
|
||||
this.onboarding = new Metamask()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onBoarding() {
|
||||
this.onboarding.startOnboarding()
|
||||
},
|
||||
async _web3Connect(name) {
|
||||
await this.web3Connect(name)
|
||||
|
||||
this.$parent.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
1
components/web3Connect/index.js
Normal file
1
components/web3Connect/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { default as ConnectButton } from './Button'
|
36
components/withdraw/RelayerTotal.vue
Normal file
36
components/withdraw/RelayerTotal.vue
Normal 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>
|
526
components/withdraw/Withdraw.vue
Normal file
526
components/withdraw/Withdraw.vue
Normal 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>
|
88
components/withdraw/WithdrawModalBox.vue
Normal file
88
components/withdraw/WithdrawModalBox.vue
Normal 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>
|
139
components/withdraw/WithdrawTotal.vue
Normal file
139
components/withdraw/WithdrawTotal.vue
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue