poa-dapps-voting/src/components/BallotCard/index.js

763 lines
23 KiB
JavaScript

import React from 'react'
import moment from 'moment'
import swal from 'sweetalert2'
import { BallotDataPair } from '../BallotDataPair'
import { BallotFooter } from '../BallotFooter'
import { BallotInfoContainer } from '../BallotInfoContainer'
import { Votes } from '../Votes'
import { getNetworkBranch } from '../../utils/utils'
import { inject, observer } from 'mobx-react'
import messages from '../../utils/messages'
import { observable, action, computed } from 'mobx'
import { sendTransactionByVotingKey } from '../../utils/helpers'
const ACCEPT = 1
const REJECT = 2
const SEND = 1
const BURN = 2
const FREEZE = 3
const USDateTimeFormat = 'MM/DD/YYYY h:mm:ss A'
const zeroTimeTo = '00:00'
@inject('commonStore', 'contractsStore', 'routing', 'ballotsStore')
@observer
export class BallotCard extends React.Component {
@observable cancelDeadline = 0
@observable nowTimeUnix
@observable startTime
@observable startTimeUnix
@observable endTime
@observable timeTo = {}
@observable
timeToStart = {
val: 0,
displayValue: zeroTimeTo,
title: 'To start'
}
@observable
timeToFinish = {
val: 0,
displayValue: zeroTimeTo,
title: 'To close'
}
@observable
timeToCancel = {
val: 0,
displayValue: zeroTimeTo
}
@observable creatorMiningKey
@observable creator
@observable progress
@observable totalVoters
@observable burnVotes
@observable freezeVotes
@observable sendVotes
@observable isFinalized
@observable isCanceled = false
@observable canBeFinalized
@observable hasAlreadyVoted
@observable memo
@observable quorumState
@observable minBallotDuration
@computed
get cancelOrFinalizeButtonDisplayName() {
if (this.isFinalized) {
return 'Finalized'
} else if (this.isCanceled) {
return 'Canceled'
} else if (this.timeToCancel.val > 0) {
return 'Cancel ballot'
} else {
return 'Finalize ballot'
}
}
@computed
get cancelOrFinalizeButtonState() {
if (this.isFinalized || this.isCanceled) {
return 'disabled'
} else if (this.timeToCancel.val > 0) {
return 'danger'
}
}
@computed
get cancelOrFinalizeDescription() {
if (this.isFinalized) {
if (this.props.votingType === 'votingToManageEmissionFunds') {
switch (Number(this.quorumState)) {
case 2:
return 'The funds were sent to the proposed receiver address.'
case 3:
return 'The funds were burnt.'
case 4:
return 'The funds were frozen.'
default:
return 'Unknown error'
}
}
return ''
} else if (this.isCanceled) {
return ''
} else if (this.timeToCancel.val > 0) {
return `You can cancel this ballot within ${this.timeToCancel.displayValue}`
} else {
let description = 'Finalization is available after ballot time is finished'
if (this.canBeFinalized !== null && this.nowTimeUnix - this.startTimeUnix > this.minBallotDuration) {
description += ' or all validators are voted'
}
return description
}
}
@computed
get votesForNumber() {
let votes = (this.totalVoters + this.progress) / 2
if (isNaN(votes)) votes = 0
return votes
}
@computed
get votesForPercents() {
if (this.totalVoters <= 0) {
return 0
}
let votesPercents = Math.round((this.votesForNumber / this.totalVoters) * 100)
if (isNaN(votesPercents)) votesPercents = 0
return votesPercents
}
@computed
get votesAgainstNumber() {
let votes = (this.totalVoters - this.progress) / 2
if (isNaN(votes)) votes = 0
return votes
}
@computed
get votesAgainstPercents() {
if (this.totalVoters <= 0) {
return 0
}
let votesPercents = Math.round((this.votesAgainstNumber / this.totalVoters) * 100)
if (isNaN(votesPercents)) votesPercents = 0
return votesPercents
}
@computed
get votesBurnNumber() {
let votes = this.burnVotes
if (isNaN(votes)) votes = 0
return votes
}
@computed
get votesBurnPercents() {
if (this.totalVoters <= 0) {
return 0
}
let votesPercents = Math.round((this.votesBurnNumber / this.totalVoters) * 100)
if (isNaN(votesPercents)) votesPercents = 0
return votesPercents
}
@computed
get votesFreezeNumber() {
let votes = this.freezeVotes
if (isNaN(votes)) votes = 0
return votes
}
@computed
get votesFreezePercents() {
if (this.totalVoters <= 0) {
return 0
}
let votesPercents = Math.round((this.votesFreezeNumber / this.totalVoters) * 100)
if (isNaN(votesPercents)) votesPercents = 0
return votesPercents
}
@computed
get votesSendNumber() {
let votes = this.sendVotes
if (isNaN(votes)) votes = 0
return votes
}
@computed
get votesSendPercents() {
if (this.totalVoters <= 0) {
return 0
}
let votesPercents = Math.round((this.votesSendNumber / this.totalVoters) * 100)
if (isNaN(votesPercents)) votesPercents = 0
return votesPercents
}
@action('Calculate time to start/finish')
calcTimeTo = () => {
const _now = moment()
const cancel = moment.utc(this.cancelDeadline, USDateTimeFormat)
const start = moment.utc(this.startTime, USDateTimeFormat)
const finish = moment.utc(this.endTime, USDateTimeFormat)
const msCancel = cancel.diff(_now)
const msStart = start.diff(_now)
const msFinish = finish.diff(_now)
this.nowTimeUnix = moment()
.utc()
.unix()
if (msCancel > 0 && !this.isCanceled) {
this.timeToCancel.val = msCancel
this.timeToCancel.displayValue = this.formatMs(msCancel, ':mm:ss')
} else {
this.timeToCancel.val = 0
this.timeToCancel.displayValue = zeroTimeTo
}
if (msStart > 0 && !this.isCanceled) {
this.timeToStart.val = msStart + 5000
this.timeToStart.displayValue = this.formatMs(msStart, ':mm:ss')
return (this.timeTo = this.timeToStart)
}
if (msFinish > 0 && !this.isCanceled) {
this.timeToStart.val = 0
this.timeToFinish.val = msFinish
this.timeToFinish.displayValue = this.formatMs(msFinish, ':mm:ss')
return (this.timeTo = this.timeToFinish)
}
this.timeToFinish.val = 0
this.timeToFinish.displayValue = zeroTimeTo
return (this.timeTo = this.timeToFinish)
}
constructor(props) {
super(props)
const { votingState, contractsStore, votingType } = this.props
// getTimes
if (
votingState.hasOwnProperty('creationTime') &&
contractsStore.ballotCancelingThreshold > 0 &&
votingState.creatorMiningKey === contractsStore.miningKey
) {
votingState.creationTime = Number(votingState.creationTime)
this.cancelDeadline = moment
.utc((votingState.creationTime + contractsStore.ballotCancelingThreshold) * 1000)
.format(USDateTimeFormat)
}
this.startTimeUnix = moment.utc(votingState.startTime * 1000) / 1000
this.startTime = moment.utc(votingState.startTime * 1000).format(USDateTimeFormat)
this.endTime = moment.utc(votingState.endTime * 1000).format(USDateTimeFormat)
// getCreator
this.creator = votingState.creator
this.creatorMiningKey = votingState.creatorMiningKey
// getTotalVoters
if (votingState.hasOwnProperty('totalVoters')) {
this.totalVoters = Number(votingState.totalVoters)
} else {
this.burnVotes = Number(votingState.burnVotes)
this.freezeVotes = Number(votingState.freezeVotes)
this.sendVotes = Number(votingState.sendVotes)
this.totalVoters = this.burnVotes + this.freezeVotes + this.sendVotes
}
// getProgress
if (votingState.hasOwnProperty('progress')) {
this.progress = Number(votingState.progress)
}
// getIsFinalized
this.isFinalized = votingState.isFinalized
// getIsCanceled
if (votingState.hasOwnProperty('isCanceled')) {
this.isCanceled = votingState.isCanceled
}
this.calcTimeTo()
// canBeFinalizedNow
if (votingState.hasOwnProperty('canBeFinalizedNow')) {
this.canBeFinalized = votingState.canBeFinalizedNow
} else {
this.canBeFinalizedNow()
}
// getMemo
this.memo = votingState.memo
// hasAlreadyVoted
if (votingState.hasOwnProperty('alreadyVoted')) {
this.hasAlreadyVoted = votingState.alreadyVoted
} else {
this.getHasAlreadyVoted()
}
if (votingType === 'votingToManageEmissionFunds') {
this.getQuorumState()
}
this.minBallotDuration = this.getMinBallotDuration(contractsStore, votingType)
}
formatMs(ms, format) {
let dur = moment.duration(ms)
let hours = Math.floor(dur.asHours())
hours = hours < 10 ? '0' + hours : hours
let formattedMs = hours + moment.utc(ms).format(':mm:ss')
return formattedMs
}
@action('validator has already voted')
getHasAlreadyVoted = async () => {
const { contractsStore, id, votingType } = this.props
let _hasAlreadyVoted = false
try {
_hasAlreadyVoted = await this.getContract(contractsStore, votingType).hasAlreadyVoted(
id,
contractsStore.votingKey
)
} catch (e) {
console.error(e.message)
}
this.hasAlreadyVoted = _hasAlreadyVoted
}
isValidVote = async () => {
const { contractsStore, id, votingType } = this.props
let _isValidVote
try {
_isValidVote = await this.getContract(contractsStore, votingType).isValidVote(id, contractsStore.votingKey)
} catch (e) {
console.error(e.message)
}
return _isValidVote
}
isActive = async () => {
const { contractsStore, id, votingType } = this.props
let _isActive = await this.repeatGetProperty(contractsStore, votingType, id, 'isActive', 0)
return _isActive
}
canBeFinalizedNow = async () => {
const { contractsStore, id, votingType } = this.props
let _canBeFinalizedNow = await this.repeatGetProperty(contractsStore, votingType, id, 'canBeFinalizedNow', 0)
this.canBeFinalized = _canBeFinalizedNow
}
getQuorumState = async () => {
const { contractsStore, id, votingType } = this.props
this.quorumState = await this.repeatGetProperty(contractsStore, votingType, id, 'getQuorumState', 0)
}
vote = async ({ choice }) => {
if (this.isCanceled) {
swal('Warning!', messages.INVALID_VOTE_MSG, 'warning')
return
}
if (this.timeToStart.val > 0) {
swal('Warning!', messages.ballotIsNotActiveMsg(this.timeTo.displayValue), 'warning')
return
}
const { commonStore, contractsStore, id, votingType, ballotsStore, pos } = this.props
const { push } = this.props.routing
if (!contractsStore.votingKey) {
swal('Warning!', messages.NO_METAMASK_MSG, 'warning')
return
} else if (!contractsStore.networkMatch) {
swal('Warning!', messages.networkMatchError(contractsStore.netId), 'warning')
return
} else if (!contractsStore.isValidVotingKey) {
swal('Warning!', messages.invalidVotingKeyMsg(contractsStore.votingKey), 'warning')
return
}
commonStore.showLoading()
let isValidVote = await this.isValidVote()
if (!isValidVote) {
commonStore.hideLoading()
swal('Warning!', messages.INVALID_VOTE_MSG, 'warning')
return
}
const contract = this.getContract(contractsStore, votingType)
sendTransactionByVotingKey(
this.props,
contract.address,
contract.vote(id, choice),
async tx => {
const ballotInfo = await contract.getBallotInfo(id, contractsStore.votingKey)
if (ballotInfo.hasOwnProperty('totalVoters')) {
this.totalVoters = Number(ballotInfo.totalVoters)
} else {
this.burnVotes = ballotInfo.burnVotes
this.freezeVotes = ballotInfo.freezeVotes
this.sendVotes = ballotInfo.sendVotes
this.totalVoters =
Number(ballotInfo.burnVotes) + Number(ballotInfo.freezeVotes) + Number(ballotInfo.sendVotes)
}
if (ballotInfo.hasOwnProperty('progress')) {
this.progress = Number(ballotInfo.progress)
}
this.isFinalized = Boolean(ballotInfo.isFinalized)
if (ballotInfo.hasOwnProperty('isCanceled')) {
this.isCanceled = Boolean(ballotInfo.isCanceled)
}
if (ballotInfo.hasOwnProperty('canBeFinalizedNow')) {
this.canBeFinalized = Boolean(ballotInfo.canBeFinalizedNow)
} else {
await this.canBeFinalizedNow()
}
this.hasAlreadyVoted = true
if (ballotInfo.hasOwnProperty('totalVoters')) {
ballotsStore.ballotCards[pos].props.votingState.totalVoters = this.totalVoters
} else {
ballotsStore.ballotCards[pos].props.votingState.burnVotes = this.burnVotes
ballotsStore.ballotCards[pos].props.votingState.freezeVotes = this.freezeVotes
ballotsStore.ballotCards[pos].props.votingState.sendVotes = this.sendVotes
}
if (ballotInfo.hasOwnProperty('progress')) {
ballotsStore.ballotCards[pos].props.votingState.progress = this.progress
}
ballotsStore.ballotCards[pos].props.votingState.isFinalized = this.isFinalized
ballotsStore.ballotCards[pos].props.votingState.isCanceled = this.isCanceled
ballotsStore.ballotCards[pos].props.votingState.canBeFinalized = this.canBeFinalized
ballotsStore.ballotCards[pos].props.votingState.hasAlreadyVoted = this.hasAlreadyVoted
swal('Congratulations!', messages.VOTED_SUCCESS_MSG, 'success').then(result => {
push(`${commonStore.rootPath}`)
})
},
messages.VOTE_FAILED_TX
)
}
cancelOrFinalize = e => {
if (this.isFinalized || this.isCanceled) {
return
}
if (this.timeToCancel.val > 0) {
this.cancel(e)
} else {
this.finalize(e)
}
}
cancel = async e => {
const { votingState, contractsStore, commonStore, ballotsStore, votingType, id, pos } = this.props
const { push } = this.props.routing
const contract = this.getContract(contractsStore, votingType)
let canCancel = true
if (!this.timeToCancel.val) {
canCancel = false
}
if (votingState.creatorMiningKey.toLowerCase() !== contractsStore.miningKey.toLowerCase()) {
canCancel = false
}
commonStore.showLoading()
if (!votingState.creationTime) {
canCancel = false
} else {
const currentTime = Number(await contract.getTime())
if (currentTime - votingState.creationTime > contractsStore.ballotCancelingThreshold) {
canCancel = false
}
}
if (!canCancel) {
commonStore.hideLoading()
swal('Warning!', messages.INVALID_CANCEL_MSG, 'warning')
return
}
sendTransactionByVotingKey(
this.props,
contract.address,
contract.cancelBallot(id),
async tx => {
this.isFinalized = false
this.isCanceled = true
ballotsStore.ballotCards[pos].props.votingState.isFinalized = this.isFinalized
ballotsStore.ballotCards[pos].props.votingState.isCanceled = this.isCanceled
if (this.canBeFinalized !== null) {
this.canBeFinalized = false
ballotsStore.ballotCards[pos].props.votingState.canBeFinalized = this.canBeFinalized
}
swal('Congratulations!', messages.CANCELED_SUCCESS_MSG, 'success').then(result => {
push(`${commonStore.rootPath}`)
})
},
messages.CANCEL_BALLOT_FAILED_TX
)
}
finalize = async e => {
if (this.timeToStart.val > 0) {
swal('Warning!', messages.ballotIsNotActiveMsg(this.timeTo.displayValue), 'warning')
return
}
const { commonStore, contractsStore, id, votingType, ballotsStore, pos } = this.props
const { push } = this.props.routing
if (!contractsStore.votingKey) {
swal('Warning!', messages.NO_METAMASK_MSG, 'warning')
return
} else if (!contractsStore.networkMatch) {
swal('Warning!', messages.networkMatchError(contractsStore.netId), 'warning')
return
} else if (!contractsStore.isValidVotingKey) {
swal('Warning!', messages.invalidVotingKeyMsg(contractsStore.votingKey), 'warning')
return
}
if (this.isFinalized) {
swal('Warning!', messages.ALREADY_FINALIZED_MSG, 'warning')
return
}
commonStore.showLoading()
await this.canBeFinalizedNow()
let _canBeFinalized = this.canBeFinalized
if (_canBeFinalized === null) {
console.log('canBeFinalizedNow does not exist')
_canBeFinalized = !(await this.isActive())
}
if (!_canBeFinalized) {
commonStore.hideLoading()
swal('Warning!', messages.INVALID_FINALIZE_MSG, 'warning')
return
}
const contract = this.getContract(contractsStore, votingType)
sendTransactionByVotingKey(
this.props,
contract.address,
contract.finalize(id),
async tx => {
const events = await contract.instance.getPastEvents('BallotFinalized', {
fromBlock: tx.blockNumber,
toBlock: tx.blockNumber
})
if (events.length > 0) {
this.isFinalized = true
this.isCanceled = false
ballotsStore.ballotCards[pos].props.votingState.isFinalized = this.isFinalized
ballotsStore.ballotCards[pos].props.votingState.isCanceled = this.isCanceled
if (this.canBeFinalized !== null) {
this.canBeFinalized = false
ballotsStore.ballotCards[pos].props.votingState.canBeFinalized = this.canBeFinalized
}
swal('Congratulations!', messages.FINALIZED_SUCCESS_MSG, 'success').then(result => {
push(`${commonStore.rootPath}`)
})
} else {
swal('Warning!', messages.INVALID_FINALIZE_MSG, 'warning')
}
},
messages.FINALIZE_FAILED_TX
)
}
repeatGetProperty = async (contractsStore, contractType, id, methodID, tryID) => {
try {
let val = await this.getContract(contractsStore, contractType)[methodID](id)
if (tryID > 0) {
console.log(`success from Try ${tryID + 1}`)
}
return val
} catch (e) {
if (tryID < 10) {
console.log(`trying to repeat get value again... Try ${tryID + 1}`)
tryID++
await setTimeout(async () => {
this.repeatGetProperty(contractsStore, contractType, id, methodID, tryID)
}, 1000)
} else {
return null
}
}
}
getContract(contractsStore, contractType) {
switch (contractType) {
case 'votingToChangeKeys':
return contractsStore.votingToChangeKeys
case 'votingToChangeMinThreshold':
return contractsStore.votingToChangeMinThreshold
case 'votingToChangeProxy':
return contractsStore.votingToChangeProxy
case 'votingToManageEmissionFunds':
return contractsStore.votingToManageEmissionFunds
case 'validatorMetadata':
return contractsStore.validatorMetadata
default:
return contractsStore.votingToChangeKeys
}
}
getThreshold(contractsStore, votingType) {
switch (votingType) {
case 'votingToChangeKeys':
return contractsStore.keysBallotThreshold
case 'votingToChangeMinThreshold':
return contractsStore.minThresholdBallotThreshold
case 'votingToChangeProxy':
return contractsStore.proxyBallotThreshold
case 'votingToManageEmissionFunds':
return contractsStore.emissionFundsBallotThreshold
default:
return contractsStore.keysBallotThreshold
}
}
getMinBallotDuration(contractsStore, votingType) {
switch (votingType) {
case 'votingToChangeKeys':
return contractsStore.minBallotDuration.keys
case 'votingToChangeMinThreshold':
return contractsStore.minBallotDuration.minThreshold
case 'votingToChangeProxy':
return contractsStore.minBallotDuration.proxy
default:
return 0
}
}
componentDidMount() {
this.interval = setInterval(() => {
this.calcTimeTo()
}, 1000)
}
componentWillUnmount() {
window.clearInterval(this.interval)
}
showCard = () => {
let { commonStore } = this.props
let checkToFinalizeFilter = commonStore.isToFinalizeFilter
? !this.isFinalized &&
!this.isCanceled &&
(this.timeToFinish.val === 0 || this.canBeFinalized) &&
this.timeToStart.val === 0
: true
let show = commonStore.isActiveFilter ? !this.isFinalized && !this.isCanceled : checkToFinalizeFilter
return show
}
typeName(type) {
switch (type) {
case 'votingToChangeMinThreshold':
return 'Consensus'
case 'votingToChangeKeys':
return 'Keys'
case 'votingToChangeProxy':
return 'Proxy'
case 'votingToManageEmissionFunds':
return 'EmissionFunds'
default:
return ''
}
}
getVotingNetworkBranch = () => {
const { contractsStore } = this.props
return contractsStore.netId ? getNetworkBranch(contractsStore.netId) : null
}
render() {
let { contractsStore, votingType, children } = this.props
let votes
const threshold = this.getThreshold(contractsStore, votingType)
const networkBranch = this.getVotingNetworkBranch()
if (votingType === 'votingToManageEmissionFunds') {
votes = [
{
onClick: e => this.vote({ choice: BURN }),
text: 'Burn',
type: 'negative',
votesAmount: this.votesBurnNumber,
votesPercentage: this.votesBurnPercents
},
{
onClick: e => this.vote({ choice: FREEZE }),
text: 'Freeze',
type: 'neutral',
votesAmount: this.votesFreezeNumber,
votesPercentage: this.votesFreezePercents
},
{
onClick: e => this.vote({ choice: SEND }),
text: 'Send',
type: 'positive',
votesAmount: this.votesSendNumber,
votesPercentage: this.votesSendPercents
}
]
} else {
votes = [
{
onClick: e => this.vote({ choice: REJECT }),
text: 'No',
type: 'negative',
votesAmount: this.votesAgainstNumber,
votesPercentage: this.votesAgainstPercents
},
{
onClick: e => this.vote({ choice: ACCEPT }),
side: 'right',
text: 'Yes',
type: 'positive',
votesAmount: this.votesForNumber,
votesPercentage: this.votesForPercents
}
]
}
return (
<div className={`sw-BallotCard ${!this.showCard() ? 'hidden' : ''}`}>
<div className="sw-BallotAbout">
<BallotDataPair dataType="name" title="Proposer" value={[this.creator]} />
{children}
<BallotDataPair
dataType="time"
title="Ballot Time (UTC)"
value={[this.startTime, `${this.timeTo.displayValue} (${this.timeTo.title})`]}
/>
</div>
<Votes networkBranch={networkBranch} votes={votes} />
<BallotInfoContainer
memo={this.memo}
networkBranch={networkBranch}
threshold={threshold}
validatorsLength={contractsStore.validatorsLength}
/>
<BallotFooter
buttonState={this.cancelOrFinalizeButtonState}
buttonText={this.cancelOrFinalizeButtonDisplayName}
description={this.cancelOrFinalizeDescription}
networkBranch={networkBranch}
onClick={e => this.cancelOrFinalize(e)}
voteId={this.props.id}
voteType={this.typeName(votingType)}
voted={this.hasAlreadyVoted}
/>
</div>
)
}
}