From ee1b6112473f43a3d48bf85ef6b526fd6174680e Mon Sep 17 00:00:00 2001 From: Illya Gerasymchuk Date: Thu, 19 Dec 2024 19:15:23 +0000 Subject: [PATCH] feat: disable OCV voting on FE if connected wallet already voted --- .../[id]/consideration-proposals/route.ts | 6 +- src/components/ConsiderationProposalList.tsx | 24 +++++- src/components/web3/OCVVoteButton.tsx | 80 ++++++++++++++++--- .../web3/dialogs/OCVManualInstructions.tsx | 60 +++++++++++--- src/types/consideration.ts | 2 + 5 files changed, 144 insertions(+), 28 deletions(-) diff --git a/src/app/api/funding-rounds/[id]/consideration-proposals/route.ts b/src/app/api/funding-rounds/[id]/consideration-proposals/route.ts index 6ea26ef..b89c0ca 100644 --- a/src/app/api/funding-rounds/[id]/consideration-proposals/route.ts +++ b/src/app/api/funding-rounds/[id]/consideration-proposals/route.ts @@ -32,7 +32,8 @@ function parseOCVVoteData(data: JsonValue | null | undefined): OCVVoteData { elegible: Boolean(voteData.elegible), votes: Array.isArray(voteData.votes) ? voteData.votes.map(vote => ({ account: String(vote.account || ''), - timestamp: Number(vote.timestamp || 0) + timestamp: Number(vote.timestamp || 0), + hash: String(vote.hash || '') })) : [] }; } @@ -145,7 +146,8 @@ export async function GET( isEligible: ocvVotes.elegible || false, voters: ocvVotes.votes?.map((v: OCVVote) => ({ address: v.account, - timestamp: v.timestamp + timestamp: v.timestamp, + hash: v.hash })) || [] }, reviewerEligible: approved >= minReviewerApprovals, diff --git a/src/components/ConsiderationProposalList.tsx b/src/components/ConsiderationProposalList.tsx index 97caa84..e22e638 100644 --- a/src/components/ConsiderationProposalList.tsx +++ b/src/components/ConsiderationProposalList.tsx @@ -300,8 +300,16 @@ export function ConsiderationProposalList({ fundingRoundId, fundingRoundName }: if (!proposal.isReviewerEligible) { return (
- - + +
); } @@ -329,8 +337,16 @@ export function ConsiderationProposalList({ fundingRoundId, fundingRoundName }: if (!proposal.isReviewerEligible) { return (
- - + +
); } diff --git a/src/components/web3/OCVVoteButton.tsx b/src/components/web3/OCVVoteButton.tsx index 36ec550..6d845b4 100644 --- a/src/components/web3/OCVVoteButton.tsx +++ b/src/components/web3/OCVVoteButton.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useState, useMemo } from "react" import { Button } from "@/components/ui/button" import { useWallet } from "@/contexts/WalletContext" import { WalletConnectorDialog } from "./WalletConnectorDialog" @@ -8,18 +8,45 @@ import { ManualVoteDialog } from "./dialogs/OCVManualInstructions" import { OCVTransactionDialog } from "./dialogs/OCVTransactionDialog" import { Icons } from "@/components/icons" import { TARGET_NETWORK } from "@/contexts/WalletContext" +import { + Tooltip, + TooltipContent, + TooltipTrigger, + TooltipProvider +} from "@/components/ui/tooltip" +import { formatDistanceToNow } from "date-fns" +import type { ConsiderationVoteStats } from "@/types/consideration" interface OCVVoteButtonProps { proposalId: string useWallet?: boolean + voteStats: ConsiderationVoteStats } -export function OCVVoteButton({ proposalId, useWallet: isWalletEnabled = true }: OCVVoteButtonProps) { +// Add type for voter +interface Voter { + address: string + timestamp: number + hash: string +} + +export function OCVVoteButton({ + proposalId, + useWallet: isWalletEnabled = true, + voteStats +}: OCVVoteButtonProps) { const { state, enforceTargetNetwork } = useWallet() const [showWalletDialog, setShowWalletDialog] = useState(false) const [showManualDialog, setShowManualDialog] = useState(false) const [showTransactionDialog, setShowTransactionDialog] = useState(false) + const existingVote = useMemo(() => { + if (!state.wallet?.address) return null + return voteStats.communityVotes.voters.find( + (voter: Voter) => voter.address.toLowerCase() === state.wallet!.address.toLowerCase() + ) + }, [voteStats.communityVotes.voters, state.wallet]) + const handleVoteClick = async () => { if (!isWalletEnabled) { setShowManualDialog(true) @@ -43,20 +70,50 @@ export function OCVVoteButton({ proposalId, useWallet: isWalletEnabled = true }: const buttonText = isWalletEnabled ? state.wallet - ? "Vote with Wallet" + ? existingVote + ? "Already Voted" + : "Vote with Wallet" : "Connect Wallet to Vote" : "Vote Without Wallet" + const walletButton = ( + + ) + return ( <> - + {isWalletEnabled && state.wallet && existingVote ? ( + + + +
{walletButton}
+
+ +
+

You have already voted on this proposal

+
+

Time: {formatDistanceToNow(existingVote.timestamp)} ago

+

Transaction: {existingVote.hash}

+
+
+
+
+
+ ) : ( + walletButton + )} {state.wallet && ( diff --git a/src/components/web3/dialogs/OCVManualInstructions.tsx b/src/components/web3/dialogs/OCVManualInstructions.tsx index 55bf9e5..f9c08f2 100644 --- a/src/components/web3/dialogs/OCVManualInstructions.tsx +++ b/src/components/web3/dialogs/OCVManualInstructions.tsx @@ -10,19 +10,27 @@ import { DialogTitle, } from "@/components/ui/dialog" import { cn } from "@/lib/utils" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { formatDistanceToNow } from "date-fns" interface ManualVoteDialogProps { open: boolean onOpenChange: (open: boolean) => void voteId: string voteType: "YES" | "NO" + existingVote?: { + address: string + timestamp: number + hash: string + } | null } export function ManualVoteDialog({ open, onOpenChange, voteId, - voteType + voteType, + existingVote }: ManualVoteDialogProps) { const [copied, setCopied] = useState(false) const memo = `${voteType} ${voteId}` @@ -45,21 +53,47 @@ export function ManualVoteDialog({ How do I cast my vote? -
+ +
+ {existingVote && ( + svg]:text-orange-900 dark:[&>svg]:text-orange-200" + )} + > + +
+

+ Your connected wallet has already voted on this proposal +

+
+

Time: {formatDistanceToNow(existingVote.timestamp)} ago

+

+ Transaction: {existingVote.hash} +

+
+
+
+
+ )} +
-

+

Send a transaction to yourself with the following text in the memo field:

-
- {memo} +
+ {memo}
-
-

Instructions:

-
    +
    +

    Instructions:

    +
    1. Copy the memo shown above
    2. Use your preferred wallet (or a CLI) to create a transaction
    3. Set the recipient address to your own address
    4. @@ -83,8 +117,12 @@ export function ManualVoteDialog({
    5. Set the memo field to the copied text
    6. Send the transaction to cast your vote
    -

    - Note: Make sure to follow the exact format of the memo to ensure your vote is properly recorded. The transaction must be sent to your own address, and the amount can be 0 MINA. +

    + +
    +

    + Note: Make sure to follow the exact format of the memo to ensure your vote is properly recorded. + The transaction must be sent to your own address, and the amount can be 0 MINA.

diff --git a/src/types/consideration.ts b/src/types/consideration.ts index bd0ce37..ac8a2cd 100644 --- a/src/types/consideration.ts +++ b/src/types/consideration.ts @@ -12,6 +12,7 @@ export interface ConsiderationVoteStats { voters: Array<{ address: string; timestamp: number; + hash: string; }>; }; reviewerEligible: boolean; @@ -39,6 +40,7 @@ export interface ConsiderationProposal { export interface OCVVote { account: string; timestamp: number; + hash: string; } export interface OCVVoteData {