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 (
{children}
this.cancelOrFinalize(e)} voteId={this.props.id} voteType={this.typeName(votingType)} voted={this.hasAlreadyVoted} />
) } }