352 lines
11 KiB
Vue
352 lines
11 KiB
Vue
|
<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>
|