quasvel
Version:
Access and interact with Aragon Organizations and their apps.
303 lines (260 loc) • 10.6 kB
text/typescript
import { subscription } from '@aragon/connect-core'
import { BigNumber, providers as ethersProviders } from 'ethers'
import { Address, SubscriptionCallback, SubscriptionResult } from '@aragon/connect-types'
import ERC20 from './ERC20'
import Setting from './Setting'
import CastVote from './CastVote'
import ArbitratorFee from './ArbitratorFee'
import CollateralRequirement from './CollateralRequirement'
import { IDisputableVotingConnector, VoteData } from '../types'
import {
bn,
formatBn,
PCT_BASE,
PCT_DECIMALS,
currentTimestampEvm,
} from '../helpers'
export default class Vote {
#ethersProvider: ethersProviders.Provider
#connector: IDisputableVotingConnector
readonly id: string
readonly votingId: string
readonly voteId: string
readonly creator: string
readonly duration: string
readonly context: string
readonly voteStatus: string
readonly actionId: string
readonly challengeId: string
readonly challenger: string
readonly challengeEndDate: string
readonly disputeId: string
readonly settingId: string
readonly startDate: string
readonly totalPower: string
readonly snapshotBlock: string
readonly yeas: string
readonly nays: string
readonly pausedAt: string
readonly pauseDuration: string
readonly quietEndingExtension: string
readonly quietEndingExtensionDuration: string
readonly quietEndingSnapshotSupport: string
readonly script: string
readonly settledAt: string
readonly disputedAt: string
readonly executedAt: string
readonly isAccepted: boolean
readonly tokenId: string
readonly tokenDecimals: string
readonly collateralRequirementId: string
readonly submitterArbitratorFeeId: string
readonly challengerArbitratorFeeId: string
constructor(data: VoteData, connector: IDisputableVotingConnector, ethersProvider: ethersProviders.Provider) {
this.#ethersProvider = ethersProvider
this.#connector = connector
this.id = data.id
this.votingId = data.votingId
this.voteId = data.voteId
this.duration = data.duration
this.quietEndingExtension = data.quietEndingExtension
this.creator = data.creator
this.context = data.context
this.voteStatus = data.status
this.actionId = data.actionId
this.challengeId = data.challengeId
this.challenger = data.challenger
this.challengeEndDate = data.challengeEndDate
this.disputeId = data.disputeId
this.settingId = data.settingId
this.startDate = data.startDate
this.totalPower = data.totalPower
this.snapshotBlock = data.snapshotBlock
this.yeas = data.yeas
this.nays = data.nays
this.pausedAt = data.pausedAt
this.pauseDuration = data.pauseDuration
this.quietEndingExtensionDuration = data.quietEndingExtensionDuration
this.quietEndingSnapshotSupport = data.quietEndingSnapshotSupport
this.script = data.script
this.settledAt = data.settledAt
this.disputedAt = data.disputedAt
this.executedAt = data.executedAt
this.isAccepted = data.isAccepted
this.tokenId = data.tokenId
this.tokenDecimals = data.tokenDecimals
this.collateralRequirementId = data.collateralRequirementId
this.submitterArbitratorFeeId = data.submitterArbitratorFeeId
this.challengerArbitratorFeeId = data.challengerArbitratorFeeId
}
get hasEnded(): boolean {
const currentTimestamp = currentTimestampEvm()
return this.voteStatus === 'Cancelled' || this.voteStatus === 'Settled' ||
(this.voteStatus === 'Challenged' && currentTimestamp.gte(this.challengeEndDate)) ||
(this.voteStatus !== 'Challenged' && this.voteStatus !== 'Disputed' && currentTimestamp.gte(this.endDate))
}
get endDate(): string {
const baseVoteEndDate = bn(this.startDate).add(bn(this.duration))
const endDateAfterPause = baseVoteEndDate.add(bn(this.pauseDuration))
const lastComputedEndDate = endDateAfterPause.add(bn(this.quietEndingExtensionDuration))
// The last computed end date is correct if we have not passed it yet or if no flip was detected in the last extension
const currentTimestamp = currentTimestampEvm()
if (currentTimestamp.lt(lastComputedEndDate) || !this.wasFlipped) {
return lastComputedEndDate.toString()
}
// Otherwise, since the last computed end date was reached and included a flip, we need to extend the end date by one more period
return lastComputedEndDate.add(bn(this.quietEndingExtension)).toString()
}
get currentQuietEndingExtensionDuration(): string {
const actualEndDate = bn(this.endDate)
const baseVoteEndDate = bn(this.startDate).add(bn(this.duration))
const endDateAfterPause = baseVoteEndDate.add(bn(this.pauseDuration))
// To know exactly how many extensions due to quiet ending we had, we subtract
// the base vote and pause durations to the actual vote end date
return actualEndDate.sub(endDateAfterPause).toString()
}
get yeasPct(): string {
return this._votingPowerPct(this.yeas)
}
get naysPct(): string {
return this._votingPowerPct(this.nays)
}
get formattedYeas(): string {
return formatBn(this.yeas, this.tokenDecimals)
}
get formattedYeasPct(): string {
return formatBn(this.yeasPct, PCT_DECIMALS)
}
get formattedNays(): string {
return formatBn(this.nays, this.tokenDecimals)
}
get formattedNaysPct(): string {
return formatBn(this.naysPct, PCT_DECIMALS)
}
get formattedTotalPower(): string {
return formatBn(this.totalPower, this.tokenDecimals)
}
get status(): string {
if (this.hasEnded) {
if (this.voteStatus === 'Scheduled') {
return this.isAccepted ? 'Accepted' : 'Rejected'
} else if (this.voteStatus === 'Challenged') {
return 'Settled'
}
}
return this.voteStatus
}
get wasFlipped(): boolean {
// If there was no snapshot taken, it means no one voted during the quiet ending period. Thus, it cannot have been flipped.
if (this.quietEndingSnapshotSupport == 'Absent') {
return false;
}
// Otherwise, we calculate if the vote was flipped by comparing its current acceptance state to its last state at the start of the extension period
const wasInitiallyAccepted = this.quietEndingSnapshotSupport == 'Yea'
const currentExtensions = bn(this.quietEndingExtensionDuration).div(bn(this.quietEndingExtension))
const wasAcceptedBeforeLastFlip = wasInitiallyAccepted == (currentExtensions.mod(bn('2')).eq(bn('0')))
return wasAcceptedBeforeLastFlip != this.isAccepted
}
async hasEndedExecutionDelay(): Promise<boolean> {
const setting = await this.setting()
const currentTimestamp = currentTimestampEvm()
const executionDelayEndDate = bn(this.endDate).add(setting.executionDelay)
return currentTimestamp.gte(executionDelayEndDate)
}
async canVote(voterAddress: Address): Promise<boolean> {
return !this.hasEnded &&
this.voteStatus === 'Scheduled' &&
!(await this.hasVoted(voterAddress)) &&
(await this.votingPower(voterAddress)).gt(bn(0))
}
async canExecute(): Promise<boolean> {
return this.isAccepted &&
this.voteStatus === 'Scheduled' &&
(await this.hasEndedExecutionDelay())
}
async votingPower(voterAddress: Address): Promise<BigNumber> {
const token = await this.token()
return token.balanceAt(voterAddress, this.snapshotBlock)
}
async formattedVotingPower(voterAddress: Address): Promise<string> {
const token = await this.token()
const balance = await token.balanceAt(voterAddress, this.snapshotBlock)
return formatBn(balance, token.decimals)
}
async token(): Promise<ERC20> {
return this.#connector.ERC20(this.tokenId)
}
async hasVoted(voterAddress: Address): Promise<boolean> {
const castVote = await this.castVote(voterAddress)
return castVote !== null
}
castVoteId(voterAddress: Address): string {
return `${this.id}-cast-${voterAddress.toLowerCase()}`
}
async castVote(voterAddress: Address): Promise<CastVote | null> {
return this.#connector.castVote(this.castVoteId(voterAddress))
}
onCastVote(
voterAddress: Address,
callback?: SubscriptionCallback<CastVote | null>
): SubscriptionResult<CastVote | null> {
return subscription<CastVote | null>(callback, (callback) =>
this.#connector.onCastVote(this.castVoteId(voterAddress), callback)
)
}
async castVotes({ first = 1000, skip = 0 } = {}): Promise<CastVote[]> {
return this.#connector.castVotes(this.id, first, skip)
}
onCastVotes(
{ first = 1000, skip = 0 } = {},
callback?: SubscriptionCallback<CastVote[]>
): SubscriptionResult<CastVote[]> {
return subscription<CastVote[]>(callback, (callback) =>
this.#connector.onCastVotes(this.id, first, skip, callback)
)
}
async collateralRequirement(): Promise<CollateralRequirement> {
return this.#connector.collateralRequirement(this.collateralRequirementId)
}
onCollateralRequirement(
callback?: SubscriptionCallback<CollateralRequirement>
): SubscriptionResult<CollateralRequirement> {
return subscription<CollateralRequirement>(callback, (callback) =>
this.#connector.onCollateralRequirement(this.collateralRequirementId, callback)
)
}
async submitterArbitratorFee(): Promise<ArbitratorFee | null> {
return this.#connector.arbitratorFee(this.submitterArbitratorFeeId || '')
}
onSubmitterArbitratorFee(
callback?: SubscriptionCallback<ArbitratorFee | null>
): SubscriptionResult<ArbitratorFee | null> {
return subscription<ArbitratorFee | null>(callback, (callback) =>
this.#connector.onArbitratorFee(this.submitterArbitratorFeeId || '', callback)
)
}
async challengerArbitratorFee(): Promise<ArbitratorFee | null> {
return this.#connector.arbitratorFee(this.challengerArbitratorFeeId || '')
}
onChallengerArbitratorFee(
callback?: SubscriptionCallback<ArbitratorFee | null>
): SubscriptionResult<ArbitratorFee | null> {
return subscription<ArbitratorFee | null>(callback, (callback) =>
this.#connector.onArbitratorFee(this.challengerArbitratorFeeId || '', callback)
)
}
async setting(): Promise<Setting> {
return this.#connector.setting(this.settingId)
}
onSetting(
callback?: SubscriptionCallback<Setting>
): SubscriptionResult<Setting> {
return subscription<Setting>(callback, (callback) =>
this.#connector.onSetting(this.settingId, callback)
)
}
_votingPowerPct(num: string): string {
const totalPower = bn(this.totalPower)
return bn(num).mul(PCT_BASE).div(totalPower).toString()
}
}