diff --git a/src/assets/icons/no.svg b/src/assets/icons/no.svg new file mode 100644 index 0000000..7e55d34 --- /dev/null +++ b/src/assets/icons/no.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/yes.svg b/src/assets/icons/yes.svg new file mode 100644 index 0000000..0f40b62 --- /dev/null +++ b/src/assets/icons/yes.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/config/mainnet.ts b/src/config/mainnet.ts index 36026e9..ae16012 100644 --- a/src/config/mainnet.ts +++ b/src/config/mainnet.ts @@ -1,6 +1,7 @@ const config: App.AppConfig = { - proposalContractId: 'reduce-inflation.near', - validatorApi: 'https://validator-voting-api.linearprotocol.org', + proposalContractId: 'test-proposal.near', + // validatorApi: 'https://validator-voting-api.linearprotocol.org', + validatorApi: "https://validator-voting-api-dev-7d3e8c25989f.herokuapp.com", near: { network: { networkId: 'mainnet', diff --git a/src/config/testnet.ts b/src/config/testnet.ts index c473219..84574b1 100644 --- a/src/config/testnet.ts +++ b/src/config/testnet.ts @@ -1,5 +1,5 @@ const config: App.AppConfig = { - proposalContractId: 'reduce-inflation.testnet', + proposalContractId: 'mock-proposal-alpha.testnet', validatorApi: 'https://validator-voting-api.linearprotocol.org', near: { network: { diff --git a/src/containers/vote/index.ts b/src/containers/vote/index.ts index 478c62c..a1ce8ac 100644 --- a/src/containers/vote/index.ts +++ b/src/containers/vote/index.ts @@ -10,12 +10,13 @@ import { toast } from 'sonner'; interface VoteState { isLoading: boolean; + voteResult: boolean; deadline: number | null; - voteFinishedAt: number | null; - votes: Record; - votesCount: number | null; + votes: Record; + yesVotesCount: number | null; + votedYeaStakeAmount: Big.Big; votedStakeAmount: Big.Big; - totalVotedStakeAmount: Big.Big; + totalStakeAmount: Big.Big; } interface VoteComputed { @@ -23,7 +24,7 @@ interface VoteComputed { progressList: number[]; } -const PROGRESS = [66.67]; +const PROGRESS = [33.33]; type UseVoteContainer = VoteState & VoteComputed; const contractId = config.proposalContractId; @@ -32,26 +33,21 @@ function useVoteContainer(): UseVoteContainer { const { viewFunction } = useNear(); const [deadline, setDeadline] = useState(null); - const [voteFinishedAt, setVoteFinishedAt] = useState(null); - const [votes, setVotes] = useState>({}); + const [voteResult, setVoteResult] = useState(false); + const [votes, setVotes] = useState>({}); const [votedStakeAmount, setVotedStakeAmount] = useState(Big(0)); - const [totalVotedStakeAmount, setTotalVotedStakeAmount] = useState(Big(0)); + const [votedYeaStakeAmount, setVotedYeaStakeAmount] = useState(Big(0)); + const [totalStakeAmount, setTotalStakeAmount] = useState(Big(0)); const _votedPercent = useMemo(() => { - if (!totalVotedStakeAmount) return '0'; - if (totalVotedStakeAmount.eq(0)) return '0'; - return votedStakeAmount.div(totalVotedStakeAmount).times(100).toFixed(2) || '0'; - }, [votedStakeAmount, totalVotedStakeAmount]); + if (!totalStakeAmount) return '0'; + if (totalStakeAmount.eq(0)) return '0'; + return votedStakeAmount.div(totalStakeAmount).times(100).toFixed(2) || '0'; + }, [votedStakeAmount, totalStakeAmount]); const votedPercent = useMemo(() => { - const _percent = Number(_votedPercent); - const lastProgress = PROGRESS[PROGRESS.length - 1]; - if (voteFinishedAt) { - if (_percent >= lastProgress) return _votedPercent; - return lastProgress.toFixed(2); - } return _votedPercent; - }, [_votedPercent, voteFinishedAt]); + }, [_votedPercent]); const getTotalVotedStake = useCallback(async () => { const data = await viewFunction({ @@ -59,12 +55,13 @@ function useVoteContainer(): UseVoteContainer { method: 'get_total_voted_stake', }); logger.debug('get_total_voted_stake', data); - if (!Array.isArray(data) && data.length !== 2) { + if (!Array.isArray(data) || data.length !== 3) { logger.error('get_total_voted_stake error', data); return; } - setVotedStakeAmount(Big(data[0])); - setTotalVotedStakeAmount(Big(data[1])); + setVotedYeaStakeAmount(Big(data[0])); + setVotedStakeAmount(Big(data[1])); + setTotalStakeAmount(Big(data[2])); }, [viewFunction]); const getResult = useCallback(async () => { @@ -73,7 +70,7 @@ function useVoteContainer(): UseVoteContainer { method: 'get_result', }); logger.debug('get_result', data); - setVoteFinishedAt(data || null); + setVoteResult(data || false); }, [viewFunction]); const getVotes = useCallback(async () => { @@ -105,7 +102,7 @@ function useVoteContainer(): UseVoteContainer { const { isLoading, error } = useSWR( 'vote_data', async () => { - const promises = Promise.all([getTotalVotedStake(), getResult(), getVotes(), getDeadline()]); + const promises = Promise.all([getTotalVotedStake(), getVotes(), getDeadline(), getResult()]); return await promises; }, // { @@ -113,9 +110,9 @@ function useVoteContainer(): UseVoteContainer { // revalidateOnReconnect: false, // }, ); - const votesCount = useMemo(() => { + const yesVotesCount = useMemo(() => { if (error) return null; - return Object.keys(votes).length; + return Object.values(votes).filter(([vote]) => vote === 'yes').length; }, [votes, error]); useEffect(() => { @@ -126,12 +123,13 @@ function useVoteContainer(): UseVoteContainer { return { isLoading, + voteResult, deadline, - voteFinishedAt, votes, - votesCount, + yesVotesCount, votedStakeAmount, - totalVotedStakeAmount, + votedYeaStakeAmount, + totalStakeAmount, votedPercent, progressList: PROGRESS, }; diff --git a/src/pages/details/index.tsx b/src/pages/details/index.tsx index 76339da..fcac50c 100644 --- a/src/pages/details/index.tsx +++ b/src/pages/details/index.tsx @@ -33,7 +33,7 @@ interface VotingPowerItem { export default function Details() { const navigate = useNavigate(); - const { isLoading: voteDataLoading, votes, totalVotedStakeAmount } = VoteContainer.useContainer(); + const { isLoading: voteDataLoading, votes, totalStakeAmount } = VoteContainer.useContainer(); const [loading, setLoading] = useState(false); const [list, setList] = useState([]); const [powerOrder, setPowerOrder] = useState<'asc' | 'desc' | undefined>(); @@ -46,8 +46,8 @@ export default function Details() { // }); return list.sort((a, b) => { - const aVote = a.vote === 'yes' ? votes[a.accountId] || '0' : a.totalStakedBalance; - const bVote = b.vote === 'yes' ? votes[b.accountId] || '0' : b.totalStakedBalance; + const aVote = votes[a.accountId] ? votes[a.accountId][1] : '0'; + const bVote = votes[b.accountId] ? votes[b.accountId][1] : '0'; const aDate = a.lastVoteTimestamp || 0; const bDate = b.lastVoteTimestamp || 0; @@ -71,22 +71,18 @@ export default function Details() { const getPercent = useCallback( (n: string | number) => { - if (!totalVotedStakeAmount || !n) return '0'; - if (Big(totalVotedStakeAmount).eq(0)) return '0'; - return Big(n).div(totalVotedStakeAmount).times(100).toFixed(2); + if (!totalStakeAmount || !n) return '0'; + if (Big(totalStakeAmount).eq(0)) return '0'; + return Big(n).div(totalStakeAmount).times(100).toFixed(2); }, - [totalVotedStakeAmount], + [totalStakeAmount], ); const votingPowerMap: Record = useMemo(() => { const data: Record = {}; tableList.forEach((item) => { const isYesVote = item.vote === 'yes'; - let power = votes[item.accountId] || '0'; - if (!isYesVote) { - power = item.totalStakedBalance || '0'; - } - + const power = votes[item.accountId] ? votes[item.accountId][1] : '0'; const formattedPower = power ? formatBigNumber(power, 24) : '0'; const percent = getPercent(power); diff --git a/src/pages/home/components/Countdown.tsx b/src/pages/home/components/Countdown.tsx index a4e2e54..a0491fa 100644 --- a/src/pages/home/components/Countdown.tsx +++ b/src/pages/home/components/Countdown.tsx @@ -8,10 +8,12 @@ export interface CountdownProps { deadline: number | null; } -export default function Countdown({ votedPercent, deadline }: CountdownProps) { +export default function Countdown({ deadline }: CountdownProps) { const [isPageVisible, setPageVisibility] = useState(!document.hidden); const [countdownSeconds, setCountdownSeconds] = useState(null); + const finished = deadline && Date.now() > deadline; + const deadlineFromNow = useMemo(() => { if (!countdownSeconds) return null; const diffSeconds = countdownSeconds; @@ -61,12 +63,13 @@ export default function Countdown({ votedPercent, deadline }: CountdownProps) { ); if (!deadlineFromNow) return null; + if (finished) return null; return (
-

+ {/*

{votedPercent}% of Stake Voted for YEA -

+ */}
diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index b4be144..60e6bc6 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -1,123 +1,184 @@ import './index.css'; -import { useMemo } from 'react'; - -import { ArrowRight, CircleHelp } from 'lucide-react'; +import { ArrowRight } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { PulseLoader } from 'react-spinners'; import NEARLogo from '@/assets/images/near-green.jpg'; -import ApprovedImg from '@/assets/images/approved.png'; import Bg1 from '@/assets/images/home-star-bg1.png'; import Bg2 from '@/assets/images/home-star-bg2.png'; import Markdown from '@/components/markdown'; import { Button } from '@/components/ui/button'; -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import config from '@/config'; import VoteContainer from '@/containers/vote'; import { cn, formatBigNumber, isNotNullAndNumber } from '@/lib/utils'; import { article } from './article'; +import YesIcon from '@/assets/icons/yes.svg?react'; +import NoIcon from '@/assets/icons/no.svg?react'; +import ApprovedImg from '@/assets/images/approved.png'; import Countdown from './components/Countdown'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; +import Big from 'big.js'; dayjs.extend(utc); -function toFraction(x: number): string { - if (!x) return '2/3'; - if (x > 1) x = x / 100; - try { - const tolerance = 0.01; - let h1 = 1, - h2 = 0, - k1 = 0, - k2 = 1, - b = x; - do { - const a = Math.floor(b); - let aux = h1; - h1 = a * h1 + h2; - h2 = aux; - aux = k1; - k1 = a * k1 + k2; - k2 = aux; - b = 1 / (b - a); - } while (Math.abs(x - h1 / k1) > x * tolerance); - - return h1 + '/' + k1; - } catch (error) { - console.error('Error converting to fraction:', error); - return '2/3'; - } -} - export default function Home() { const navigate = useNavigate(); const { isLoading, deadline, - votesCount, + votes, + voteResult, + yesVotesCount, votedPercent, - progressList, - voteFinishedAt, - votedStakeAmount, + votedYeaStakeAmount, } = VoteContainer.useContainer(); const NEAR_ENV = config.proposalContractId?.split('.').pop() === 'near' ? 'mainnet' : 'testnet'; - const passed = useMemo(() => { - return Number(votedPercent) >= progressList[progressList.length - 1]; - }, [votedPercent, progressList]); - - const showTooltip = useMemo(() => { - if (voteFinishedAt) return false; - return passed; - }, [voteFinishedAt, passed]); - const renderVoteProgressStatus = () => { - if (!voteFinishedAt) { - return ; + if (voteResult) { + return ( +
+ {/*

+ {votedPercent}% of Stake Voted for YEA +

*/} + +
+ ); } + return ; + }; + + const renderProgress = () => { + const votedPercentNum = Number(votedPercent); + const targetPercent = 33.33; + const gt50Percent = votedPercentNum >= 50; + const totalPercent = gt50Percent ? votedPercentNum : 50; + const currentProgressPercent = gt50Percent + ? 100 + : Math.round((votedPercentNum / totalPercent) * 100); + + const targetBadge = ( +
+ Quorum + + + +
+ ); return ( -
-

- {votedPercent}% of Stake Voted for YEA +
+

+ {votedPercentNum}% of STAKE VOTED

- +
+
+
+ {!!votedPercentNum && ( +
20, + 'text-app-black pl-1.5': currentProgressPercent <= 20, + })} + > + {votedPercentNum}% +
+ )} +
+
+
+
+
0%
+ +
+ {targetPercent}%{targetBadge} +
+
+ + {!gt50Percent &&
50%
} +
); }; - const renderProgress = () => { - const _percent = Number(votedPercent); + const renderVoteBar = () => { + const voteData = Object.values(votes).reduce( + (acc, [vote, stake]) => { + if (!acc[vote]) { + acc[vote] = Big(stake); + } else { + acc[vote] = acc[vote].plus(Big(stake)); + } + return acc; + }, + {} as Record<'yes' | 'no', Big.Big>, + ); + + const safeBig = (val: Big.Big | undefined): Big => (val instanceof Big ? val : Big(val || 0)); + + const yes = safeBig(voteData?.yes); + const no = safeBig(voteData?.no); + const voteTotal = yes.plus(no); + + const yesPercent = voteTotal.eq(0) ? '0.00' : yes.div(voteTotal).times(100).toFixed(2); + const noPercent = voteTotal.eq(0) ? '0.00' : no.div(voteTotal).times(100).toFixed(2); + return ( -
-
- {progressList.map((p) => ( -
- ))} +
+
+ + YEA +
+
- {!!votedPercent && ( -
20, - 'text-app-black pl-1.5': _percent <= 20, - })} - > - {votedPercent}% -
- )} +
+ {yesPercent}% +
+
+
+
+ {noPercent}% +
+
+
+
+ + NAY
); @@ -135,67 +196,19 @@ export default function Home() { return ( <> {/* progress bar */} -
-
- Pass - - - -
- - {renderProgress()} -
-
0%
- {progressList.map((p) => ( -
- {p}% -
- ))} + {renderProgress()} -
100%
-
-
+ {/* vote bar */} + {renderVoteBar()} {/* voting process status */} {renderVoteProgressStatus()}
- {isNotNullAndNumber(votesCount) ? votesCount : '-'} Votes &{' '} - {formatBigNumber(votedStakeAmount)} + {isNotNullAndNumber(yesVotesCount) ? yesVotesCount : '-'} Votes &{' '} + {formatBigNumber(votedYeaStakeAmount)} near -
- Voting Power for YEA - {showTooltip && ( - - - - - - The proposal will pass if the total voted stake keeps above{' '} - {toFraction(Number(votedPercent))} at the beginning of next epoch or a new vote - comes in. - - - )} -
+
Voting Power for YEA
@@ -238,11 +251,10 @@ export default function Home() { '## Vote with NEAR CLI\n' + 'Instructions for Validator Voting:\n' + '- If you are a validator, please use the CLI commands shown below to vote. We do not support voting through wallet for security considerations. This page is only used to display voting results.\n' + - '- You can vote **yes** or **no** for the proposal. You can change your vote before the voting ends.\n' + - `- This voting ends when **2/3 of stake votes yes** or when **the deadline (${dayjs.utc(deadline).format('MM/DD/YYYY HH:mm:ss')} UTC) passes**.\n` + + `- You can vote **yes** or **no** for the proposal. You can change your vote before the deadline (**${dayjs.utc(deadline).format('MM/DD/YYYY HH:mm:ss')} UTC**).\n` + + '- This proposal will be approved if more than **1/3 of total stake** joins the voting and more than **2/3 of stake participating in the voting** is **yes** when the deadline is reached.\n' + '- Replace **<validator-account-id>** and **<validator-owner-id>** in the commands below with your own account IDs.\n' + "- [The indexer](https://thegraph.com/explorer/subgraphs/3EbPN5sxnMtSof4M8LuaSKLcNzvzDLrY3eyrRKBhVGaK?view=Query&chain=arbitrum-one) that tracks the voting results may have several minutes delay. If you don't see your vote in the details page, please refresh the page after a while.\n" + - "- If the voting power of your validator is **0** on the details page, it's probably because your validator is kicked out in recent epochs. This is a known limitation of the current voting contract. Please try **vote again** after your validator is back online for such case.\n" + '\n' + 'Vote **yes** with the below command, if you support this proposal. \n' + '\n' +