diff --git a/src/calls/liveProposals.ts b/src/calls/liveProposals.ts index 159478c5..2d7d6ee1 100644 --- a/src/calls/liveProposals.ts +++ b/src/calls/liveProposals.ts @@ -1,3 +1,4 @@ +import { QueryFunctionContext } from "react-query"; import { GovernanceContract } from "../utils/blockchain"; export const fetchLiveProposals = async (): Promise => { @@ -6,3 +7,51 @@ export const fetchLiveProposals = async (): Promise => { return proposals; }; + +export enum UserVote { + Yay, + Nay, + NotVoted, +} + +export type ProposalWithOpinion = { + propId: number; + opinion: UserVote; +}; + +export const fetchUserVotes = async ( + proposals: number[], + userAddress?: string +): Promise => { + if (!userAddress) { + return proposals.map((propId) => ({ propId, opinion: UserVote.NotVoted })); + } + + const promises = proposals.map((propId) => + GovernanceContract.call("get_user_voted", [userAddress, propId]) + ); + + const res = (await Promise.all(promises)) as bigint[]; + + const parsed = res.map((opinion) => { + if (opinion === 1n) { + return UserVote.Yay; + } + if (opinion === 0n) { + return UserVote.NotVoted; + } + return UserVote.Nay; + }); + + return proposals.map((propId, index) => ({ propId, opinion: parsed[index] })); +}; + +export const queryProposalsWithOpinions = async ({ + queryKey, +}: QueryFunctionContext<[string, string | undefined]>): Promise< + ProposalWithOpinion[] +> => { + const userAddress = queryKey[1]; + const proposals = await fetchLiveProposals(); + return fetchUserVotes(proposals, userAddress); +}; diff --git a/src/components/Proposal/ProposalItem.tsx b/src/components/Proposal/ProposalItem.tsx index 252ea844..d514695f 100644 --- a/src/components/Proposal/ProposalItem.tsx +++ b/src/components/Proposal/ProposalItem.tsx @@ -1,17 +1,18 @@ import { AccountInterface } from "starknet"; -import { Vote } from "../Vote/Vote"; +import { VoteButtons } from "../Vote/Vote"; +import { ProposalWithOpinion } from "../../calls/liveProposals"; type Props = { - proposal: number; - balance: bigint; + proposal: ProposalWithOpinion; + balance?: bigint; account?: AccountInterface; }; export const ProposalItem = ({ proposal, balance, account }: Props) => (
-

Proposal {proposal}

+

Proposal {proposal.propId}

- +
); diff --git a/src/components/Proposal/ProposalTable.tsx b/src/components/Proposal/ProposalTable.tsx index 66b533ba..1796803a 100644 --- a/src/components/Proposal/ProposalTable.tsx +++ b/src/components/Proposal/ProposalTable.tsx @@ -1,25 +1,19 @@ import { ProposalItem } from "./ProposalItem"; -import { useAccount } from "../../hooks/useAccount"; import styles from "./Proposal.module.css"; -import { VE_CRM_ADDRESS } from "../../constants/amm"; -import { useUserBalance } from "../../hooks/useUserBalance"; import { LoadingAnimation } from "../Loading/Loading"; +import { ProposalWithOpinion } from "../../calls/liveProposals"; +import { AccountInterface } from "starknet"; type Props = { - activeData: number[]; + proposals: ProposalWithOpinion[]; + balance?: bigint; + account?: AccountInterface; }; -const ProposalTable = ({ activeData }: Props) => { - const account = useAccount(); - const balance = useUserBalance(VE_CRM_ADDRESS); - - if (balance === undefined) { - return ; - } - +const ProposalTable = ({ proposals, balance, account }: Props) => { return (
- {activeData.map((item, i) => ( + {proposals.map((item, i) => ( { - const { isLoading, isError, data } = useQuery( - [QueryKeys.liveProposals], - fetchLiveProposals + const account = useAccount(); + const balance = useUserBalance(VE_CRM_ADDRESS); + const { + isLoading, + isError, + data: proposals, + } = useQuery( + [`proposals-${account?.address}`, account?.address], + queryProposalsWithOpinions ); if (isError) { return

Something went wrong, please try again later.

; } - if (isLoading || !data) { + if (isLoading || !proposals) { return ; } - if (data.length === 0) { + if (proposals.length === 0) { return ; } - return ; + return ( + + ); }; diff --git a/src/components/Vote/Vote.tsx b/src/components/Vote/Vote.tsx index e294fea4..25719e09 100644 --- a/src/components/Vote/Vote.tsx +++ b/src/components/Vote/Vote.tsx @@ -3,8 +3,17 @@ import { AccountInterface } from "starknet"; import GovernanceAbi from "../../abi/amm_abi.json"; import { GOVERNANCE_ADDRESS } from "../../constants/amm"; import { debug } from "../../utils/debugger"; +import { ProposalWithOpinion, UserVote } from "../../calls/liveProposals"; import styles from "./Vote.module.css"; +import buttonStyles from "../../style/button.module.css"; +import { useState } from "react"; +import { LoadingAnimation } from "../Loading/Loading"; +import { addTx, markTxAsDone, showToast } from "../../redux/actions"; +import { afterTransaction } from "../../utils/blockchain"; +import { invalidateKey } from "../../queries/client"; +import { TransactionAction } from "../../redux/reducers/transactions"; +import { ToastType } from "../../redux/reducers/ui"; enum Opinion { YAY = "1", @@ -14,8 +23,11 @@ enum Opinion { const vote = async ( account: AccountInterface, propId: number, - opinion: Opinion + opinion: Opinion, + setProcessing: (b: boolean) => void ) => { + setProcessing(true); + const call = { contractAddress: GOVERNANCE_ADDRESS, entrypoint: "vote", @@ -25,77 +37,93 @@ const vote = async ( const res = await account.execute(call, [GovernanceAbi]).catch((e) => { debug("Vote rejected or failed", e.message); }); - debug(res); + + if (!res) { + setProcessing(false); + return; + } + + const hash = res.transaction_hash; + + addTx(hash, `vote-${propId}`, TransactionAction.Vote); + afterTransaction( + res.transaction_hash, + () => { + invalidateKey(`proposals-${account?.address}`); + setProcessing(false); + showToast(`Successfully voted on proposal ${propId}`, ToastType.Success); + markTxAsDone(hash); + }, + () => { + setProcessing(false); + showToast(`Vote on proposal ${propId} failed`, ToastType.Error); + markTxAsDone(hash); + } + ); }; type VoteButtonsProps = { account?: AccountInterface; - propId: number; - balance: bigint; + proposal: ProposalWithOpinion; + balance?: bigint; }; -const VoteButtons = ({ account, propId, balance }: VoteButtonsProps) => { +export const VoteButtons = ({ + account, + proposal, + balance, +}: VoteButtonsProps) => { + const [processing, setProcessing] = useState(false); + + if (processing) { + return ( +
+ + +
+ ); + } if (!account) { return

Connect wallet to vote

; } - if (balance === 0n) { + if (!balance) { return

Only Carmine Token holders can vote

; } - return ( -
- - -
- ); -}; - -type PropMessageProps = { - link?: string; -}; - -// TODO: find a way to link prop to discord message -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const PropMessage = ({ link }: PropMessageProps) => { - if (link) { + if (proposal.opinion === UserVote.NotVoted) { return ( -

- To see proposal details and discuss go to the{" "} - - Discord thread - - . -

+
+ + +
); } - return ( -

- There is currently no thread associated with this proposal, feel free to{" "} - - discuss on our Discord - - . -

- ); -}; -type VoteProps = { - proposal: number; - balance: bigint; - account?: AccountInterface; -}; + const message = + proposal.opinion === UserVote.Yay + ? "Already voted Yes ✅" + : "Already voted No ❌"; -export const Vote = ({ proposal, balance, account }: VoteProps) => { return ( -
- +
+
); }; diff --git a/src/queries/client.ts b/src/queries/client.ts index 121e837b..7723c934 100644 --- a/src/queries/client.ts +++ b/src/queries/client.ts @@ -18,5 +18,5 @@ export const invalidatePositions = () => queryClient.invalidateQueries(QueryKeys.position); export const invalidateStake = () => queryClient.invalidateQueries(QueryKeys.stake); -export const invalidateKey = (queryKey: QueryKeys) => +export const invalidateKey = (queryKey: QueryKeys | string) => queryClient.invalidateQueries(queryKey);