Skip to content

Commit

Permalink
Merge pull request #205 from CarmineOptions/feature/user-voted
Browse files Browse the repository at this point in the history
feat: show user voted
  • Loading branch information
DaveVodrazka authored Jul 10, 2024
2 parents a2c3a2f + cfd8111 commit 74cec26
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 82 deletions.
49 changes: 49 additions & 0 deletions src/calls/liveProposals.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { QueryFunctionContext } from "react-query";
import { GovernanceContract } from "../utils/blockchain";

export const fetchLiveProposals = async (): Promise<number[]> => {
Expand All @@ -6,3 +7,51 @@ export const fetchLiveProposals = async (): Promise<number[]> => {

return proposals;
};

export enum UserVote {
Yay,
Nay,
NotVoted,
}

export type ProposalWithOpinion = {
propId: number;
opinion: UserVote;
};

export const fetchUserVotes = async (
proposals: number[],
userAddress?: string
): Promise<ProposalWithOpinion[]> => {
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);
};
11 changes: 6 additions & 5 deletions src/components/Proposal/ProposalItem.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<div>
<h3>Proposal {proposal}</h3>
<h3>Proposal {proposal.propId}</h3>
<div>
<Vote proposal={proposal} balance={balance} account={account} />
<VoteButtons proposal={proposal} balance={balance} account={account} />
</div>
</div>
);
20 changes: 7 additions & 13 deletions src/components/Proposal/ProposalTable.tsx
Original file line number Diff line number Diff line change
@@ -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 <LoadingAnimation />;
}

const ProposalTable = ({ proposals, balance, account }: Props) => {
return (
<div className={styles.listcontainer}>
{activeData.map((item, i) => (
{proposals.map((item, i) => (
<ProposalItem
proposal={item}
account={account}
Expand Down
26 changes: 18 additions & 8 deletions src/components/Proposal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
import { useQuery } from "react-query";
import { NoContent } from "../TableNoContent";
import ProposalTable from "./ProposalTable";
import { QueryKeys } from "../../queries/keys";
import { fetchLiveProposals } from "../../calls/liveProposals";
import { queryProposalsWithOpinions } from "../../calls/liveProposals";
import { LoadingAnimation } from "../Loading/Loading";
import { useAccount } from "../../hooks/useAccount";
import { useUserBalance } from "../../hooks/useUserBalance";
import { VE_CRM_ADDRESS } from "../../constants/amm";

export const Proposals = () => {
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 <p>Something went wrong, please try again later.</p>;
}

if (isLoading || !data) {
if (isLoading || !proposals) {
return <LoadingAnimation />;
}

if (data.length === 0) {
if (proposals.length === 0) {
return <NoContent text="No proposals are currently live" />;
}

return <ProposalTable activeData={data} />;
return (
<ProposalTable proposals={proposals} account={account} balance={balance} />
);
};
138 changes: 83 additions & 55 deletions src/components/Vote/Vote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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 (
<div>
<button disabled style={{ marginRight: "4rem" }}>
<LoadingAnimation />
</button>
<button disabled>
<LoadingAnimation />
</button>
</div>
);
}
if (!account) {
return <p>Connect wallet to vote</p>;
}
if (balance === 0n) {
if (!balance) {
return <p>Only Carmine Token holders can vote</p>;
}
return (
<div className={styles.votebuttoncontainer}>
<button onClick={() => vote(account, propId, Opinion.YAY)}>
Vote Yes
</button>
<button onClick={() => vote(account, propId, Opinion.NAY)}>
Vote No
</button>
</div>
);
};

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 (
<p>
To see proposal details and discuss go to the{" "}
<a target="_blank" rel="noopener nofollow noreferrer" href={link}>
Discord thread
</a>
.
</p>
<div className={styles.votebuttoncontainer}>
<button
onClick={() =>
vote(account, proposal.propId, Opinion.YAY, setProcessing)
}
>
Vote Yes
</button>
<button
onClick={() =>
vote(account, proposal.propId, Opinion.NAY, setProcessing)
}
>
Vote No
</button>
</div>
);
}
return (
<p>
There is currently no thread associated with this proposal, feel free to{" "}
<a
target="_blank"
rel="noopener nofollow noreferrer"
href="https://discord.com/channels/969228248552706078/969228248552706081" // community/general channel
>
discuss on our Discord
</a>
.
</p>
);
};

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 (
<div>
<VoteButtons account={account} propId={proposal} balance={balance} />
<div className={styles.votebuttoncontainer}>
<button disabled className={buttonStyles.green}>
{message}
</button>
</div>
);
};
2 changes: 1 addition & 1 deletion src/queries/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

0 comments on commit 74cec26

Please sign in to comment.