diff --git a/components/ProposalExecutionCard.tsx b/components/ProposalExecutionCard.tsx index 5a3782c01c..d57c90333d 100644 --- a/components/ProposalExecutionCard.tsx +++ b/components/ProposalExecutionCard.tsx @@ -1,4 +1,3 @@ -import { ProgramAccount, ProposalTransaction } from '@solana/spl-governance' import classNames from 'classnames' import { useEffect, useState, useRef } from 'react' import dayjs from 'dayjs' @@ -12,43 +11,7 @@ import useProposal from '@hooks/useProposal' import { ntext } from '@utils/ntext' import Button from '@components/Button' import { diffTime } from '@components/ProposalRemainingVotingTime' - -function parseTransactions( - transactions: ProgramAccount[] -) { - const executed: ProgramAccount[] = [] - const ready: ProgramAccount[] = [] - const notReady: ProgramAccount[] = [] - let minHoldUpTime: number | null = null - - for (const transaction of transactions) { - const holdUpTime = transaction.account.holdUpTime - - if (transaction.account.executedAt) { - executed.push(transaction) - } else if (!holdUpTime || holdUpTime <= 0) { - ready.push(transaction) - } else { - notReady.push(transaction) - - if (holdUpTime) { - if (minHoldUpTime === null || holdUpTime < minHoldUpTime) { - minHoldUpTime = holdUpTime - } - } - } - } - - // Order instructions by instruction index - return { - executed, - ready: ready.sort( - (a, b) => a.account.instructionIndex - b.account.instructionIndex - ), - notReady, - minHoldUpTime, - } -} +import useProposalTransactions from '@hooks/useProposalTransactions' interface Props { className?: string @@ -63,29 +26,38 @@ export default function ProposalExecutionCard(props: Props) { const timer = useRef() const allTransactions = Object.values(instructions) - const { executed, ready, notReady, minHoldUpTime } = parseTransactions( - allTransactions + + const proposalTransactions = useProposalTransactions( + allTransactions, + proposal ) useEffect(() => { - if (typeof window !== 'undefined' && minHoldUpTime) { + if ( + typeof window !== 'undefined' && + proposalTransactions && + proposalTransactions.nextExecuteAt + ) { timer.current = window.setInterval(() => { - const end = dayjs(1000 * (dayjs().unix() + minHoldUpTime)) + const end = dayjs(1000 * proposalTransactions.nextExecuteAt!) setTimeLeft(diffTime(false, dayjs(), end)) }, 1000) } return () => clearInterval(timer.current) - }, [minHoldUpTime]) + }, [proposalTransactions?.nextExecuteAt]) if ( allTransactions.length === 0 || !proposal || - allTransactions.length === executed.length + !proposalTransactions || + allTransactions.length === proposalTransactions.executed.length ) { return null } + const { ready, notReady, executed, nextExecuteAt } = proposalTransactions + return (

- {minHoldUpTime !== null + {nextExecuteAt !== null ? 'Execution Hold Up Time' : 'Execute Proposal'}

diff --git a/hooks/useProposalTransactions.ts b/hooks/useProposalTransactions.ts new file mode 100644 index 0000000000..23ad6421c5 --- /dev/null +++ b/hooks/useProposalTransactions.ts @@ -0,0 +1,110 @@ +import { + ProgramAccount, + Proposal, + ProposalTransaction, +} from '@solana/spl-governance' +import { useEffect, useState } from 'react' + +function parseTransactions( + transactions: ProgramAccount[], + proposal: ProgramAccount +) { + const executed: ProgramAccount[] = [] + const ready: ProgramAccount[] = [] + const notReady: ProgramAccount[] = [] + + let nextExecuteAt: number | null = null + + for (const transaction of transactions) { + const holdUpTime = transaction.account.holdUpTime + + // already executed + if (transaction.account.executedAt) { + executed.push(transaction) + } + // doesn't have a hold up time + else if (!holdUpTime || holdUpTime <= 0) { + ready.push(transaction) + } + // has a hold up time, so check if it's ready + else { + const votingCompletedAt = proposal.account.votingCompletedAt + if (votingCompletedAt) { + const canExecuteAt = votingCompletedAt.toNumber() + holdUpTime + const now = new Date().getTime() / 1000 // unix timestamp in seconds + + // ready to execute + if (now > canExecuteAt) { + ready.push(transaction) + } + // not ready to execute + else { + notReady.push(transaction) + // find the soonest transaction to execute + if (!nextExecuteAt || canExecuteAt < nextExecuteAt) + nextExecuteAt = canExecuteAt + } + } + } + } + + return { + executed, + // Order instructions by instruction index + ready: ready.sort( + (a, b) => a.account.instructionIndex - b.account.instructionIndex + ), + notReady, + nextExecuteAt, + } +} + +export default function useProposalTransactions( + allTransactions: ProgramAccount[], + proposal?: ProgramAccount +) { + if (!proposal) return null + + const [executed, setExecuted] = useState< + ProgramAccount[] + >([]) + const [ready, setReady] = useState[]>([]) + const [notReady, setNotReady] = useState< + ProgramAccount[] + >([]) + + const [nextExecuteAt, setNextExecuteAt] = useState(null) + + useEffect(() => { + let interval: NodeJS.Timeout | null = null + + if (allTransactions.length !== executed.length) { + interval = setInterval(() => { + const { executed, ready, notReady, nextExecuteAt } = parseTransactions( + allTransactions, + proposal + ) + setExecuted(executed) + setReady(ready) + setNotReady(notReady) + setNextExecuteAt(nextExecuteAt) + }, 1000) + } else { + if (interval) { + clearInterval(interval) + interval = null + } + } + + return () => { + if (interval) clearInterval(interval) + } + }, [executed]) + + return { + executed, + ready, + notReady, + nextExecuteAt, + } +}