init
This commit is contained in:
commit
44f31f8b9f
402 changed files with 47865 additions and 0 deletions
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue