diff --git a/src/components/Plugin/ContributionRewardExtRewarders/Competition/utils.ts b/src/components/Plugin/ContributionRewardExtRewarders/Competition/utils.ts index 691d054a9..79b2b4fab 100644 --- a/src/components/Plugin/ContributionRewardExtRewarders/Competition/utils.ts +++ b/src/components/Plugin/ContributionRewardExtRewarders/Competition/utils.ts @@ -8,6 +8,7 @@ import { operationNotifierObserver } from "actions/arcActions"; import { IRootState } from "reducers"; import { Observable, of } from "rxjs"; import { map, mergeMap, toArray, first } from "rxjs/operators"; +import { safeMoment } from "lib/util"; /** * Defined in the order that Competition cards should be sorted in the List component. @@ -72,10 +73,10 @@ export class CompetitionStatus { export const competitionStatus = (competition: ICompetitionProposalState): CompetitionStatus => { const now = moment(); - const startTime = moment(competition.startTime); - const submissionsEndTime = moment(competition.suggestionsEndTime); - const votingStartTime = moment(competition.votingStartTime); - const endTime = moment(competition.endTime); + const startTime = safeMoment(competition.startTime); + const submissionsEndTime = safeMoment(competition.suggestionsEndTime); + const votingStartTime = safeMoment(competition.votingStartTime); + const endTime = safeMoment(competition.endTime); const hasSubmissions = !!competition.totalSuggestions; const hasWinners = !!competition.numberOfWinningSuggestions; let status: CompetitionStatusEnum; diff --git a/src/components/Proposal/Voting/VoteButtons.tsx b/src/components/Proposal/Voting/VoteButtons.tsx index c509d6c37..9ab225a40 100644 --- a/src/components/Proposal/Voting/VoteButtons.tsx +++ b/src/components/Proposal/Voting/VoteButtons.tsx @@ -7,7 +7,7 @@ import classNames from "classnames"; import Reputation from "components/Account/Reputation"; import { ActionTypes, default as PreTransactionModal } from "components/Shared/PreTransactionModal"; import Analytics from "lib/analytics"; -import { fromWei, targetedNetwork } from "lib/util"; +import { fromWei, targetedNetwork, safeMoment } from "lib/util"; import { Page } from "pages"; import * as React from "react"; import { connect } from "react-redux"; @@ -105,7 +105,7 @@ class VoteButtons extends React.Component { (proposalState.stage === IProposalStage.Boosted && expired) || (proposalState.stage === IProposalStage.QuietEndingPeriod && expired) || (currentAccountState && currentAccountState.reputation.eq(new BN(0))) || - (currentAccountState && (proposalState.createdAt < currentAccountState.createdAt) && + (currentAccountState && (safeMoment(proposalState.createdAt) < safeMoment(currentAccountState.createdAt)) && //this is a workaround till https://github.com/daostack/subgraph/issues/548 (targetedNetwork() !== "ganache")) || currentVote === IProposalOutcome.Pass || @@ -133,7 +133,7 @@ class VoteButtons extends React.Component { * 5) when `currentAccount` is not found in the subgraph, then a fake `currentAccountState` is created with * rep == 0 */ - (currentAccountState && (proposalState.createdAt < currentAccountState.createdAt)) ? + (currentAccountState && (safeMoment(proposalState.createdAt) < safeMoment(currentAccountState.createdAt))) ? "Must have had reputation in this DAO when the proposal was created" : proposalState.stage === IProposalStage.ExpiredInQueue || (proposalState.stage === IProposalStage.Boosted && expired) || diff --git a/src/lib/proposalHelpers.ts b/src/lib/proposalHelpers.ts index 3942ae5c7..cd23a6b0f 100644 --- a/src/lib/proposalHelpers.ts +++ b/src/lib/proposalHelpers.ts @@ -1,6 +1,7 @@ import * as moment from "moment"; import { IProposalOutcome, IProposalStage, IProposalState } from "@daostack/arc.js"; +import { safeMoment } from "lib/util"; export interface IRedemptionState { accountAddress: string; @@ -18,23 +19,23 @@ export interface IRedemptionState { voterReputation: number; } -export const closingTime = (proposal: IProposalState) => { +export const closingTime = (proposal: IProposalState): moment.Moment => { switch (proposal.stage) { case IProposalStage.ExpiredInQueue: case IProposalStage.Queued: - return moment((Number(proposal.createdAt.toString()) + proposal.genesisProtocolParams.queuedVotePeriodLimit) * 1000); + return safeMoment(proposal.createdAt).add(proposal.genesisProtocolParams.queuedVotePeriodLimit, "seconds"); case IProposalStage.PreBoosted: - return moment((proposal.preBoostedAt + proposal.genesisProtocolParams.preBoostedVotePeriodLimit) * 1000); + return safeMoment(proposal.preBoostedAt).add(proposal.genesisProtocolParams.preBoostedVotePeriodLimit, "seconds"); case IProposalStage.Boosted: - return moment((proposal.boostedAt + proposal.genesisProtocolParams.boostedVotePeriodLimit) * 1000); + return safeMoment(proposal.boostedAt).add(proposal.genesisProtocolParams.boostedVotePeriodLimit, "seconds"); case IProposalStage.QuietEndingPeriod: - return moment((proposal.quietEndingPeriodBeganAt + proposal.genesisProtocolParams.quietEndingPeriod) * 1000); + return safeMoment(proposal.quietEndingPeriodBeganAt).add(proposal.genesisProtocolParams.quietEndingPeriod, "seconds"); case IProposalStage.Executed: - return moment(proposal.executedAt * 1000); + return safeMoment(proposal.executedAt); } }; -export function proposalExpired(proposal: IProposalState) { +export function proposalExpired(proposal: IProposalState): boolean { const res = ( (proposal.stage === IProposalStage.ExpiredInQueue) || (proposal.stage === IProposalStage.Queued && closingTime(proposal) <= moment()) @@ -42,20 +43,20 @@ export function proposalExpired(proposal: IProposalState) { return res; } -export function proposalEnded(proposal: IProposalState) { +export function proposalEnded(proposal: IProposalState): boolean { const res = ( (proposal.stage === IProposalStage.Executed) || proposalExpired(proposal)); return res; } -export function proposalPassed(proposal: IProposalState) { +export function proposalPassed(proposal: IProposalState): boolean { const res = ( (proposal.stage === IProposalStage.Executed && proposal.winningOutcome === IProposalOutcome.Pass) ); return res; } -export function proposalFailed(proposal: IProposalState) { +export function proposalFailed(proposal: IProposalState): boolean { const res = ( (proposal.stage === IProposalStage.Executed && proposal.winningOutcome === IProposalOutcome.Fail) || proposalExpired(proposal) diff --git a/src/lib/proposalUtils.ts b/src/lib/proposalUtils.ts index 69d1ce07c..5a6db9d38 100644 --- a/src/lib/proposalUtils.ts +++ b/src/lib/proposalUtils.ts @@ -3,7 +3,7 @@ import moment = require("moment-timezone"); const cloneDeep = require("clone-deep"); -export function importUrlValues(defaultValues: Values) { +export function importUrlValues(defaultValues: Values): any { const { search } = window.location; const params = new URLSearchParams(search); const initialFormValues: any = cloneDeep([defaultValues])[0]; @@ -41,14 +41,14 @@ export function importUrlValues(defaultValues: Values) { return initialFormValues; } -export const exportUrl = (values: any) => { - const setQueryString = (key: string) => { +export const exportUrl = (values: unknown): void => { + const setQueryString = (key: keyof typeof values): string => { if (values[key] === undefined) { return ""; } if (typeof values[key] === "object") { if (moment.isMoment(values[key])) { - return `${key}=${values[key].toString()}`; + return `${key}=${(values[key] as moment.Moment).toString()}`; } else { return `${key}=${JSON.stringify(values[key])}`; } diff --git a/src/lib/util.ts b/src/lib/util.ts index 8403edad1..a2543ac30 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -301,7 +301,7 @@ export async function getNetworkId(web3Provider?: Web3Provider): Promise return network.chainId.toString(); } else { const promise = new Promise((resolve, reject) => { - web3Provider.send("net_version", (error, response) => { + web3Provider.send("net_version", (error: any, response: any) => { if (error) { reject(error); } else { @@ -634,3 +634,27 @@ export function ethBalance(address: Address): Observable { const arc = getArc(); return arc.ethBalance(address); } + +/** + * arc.js is inconsistent in how it returns datetimes. + * Convert all possibilities safely to a `moment`. + * Is OK when the dateSpecifier is already a moment. + * If is a string, must be ISO-conformant. + * @param dateSpecifier + */ +export function safeMoment(dateSpecifier: moment.Moment | Date | number | string | undefined): moment.Moment { + switch (typeof dateSpecifier) { + case "object": + if (moment.isMoment(dateSpecifier)) { + return dateSpecifier; + } + // else assume is a Date, fallthrough + case "string": + return moment(dateSpecifier); + case "number": + // then should be a count of seconds in UNIX epoch + return moment.unix(dateSpecifier); + default: + throw new Error(`safeMoment: unknown type: ${typeof dateSpecifier}`); + } +}