Skip to content

Commit

Permalink
fix: proposal page now working
Browse files Browse the repository at this point in the history
  • Loading branch information
carlosgj94 committed Mar 12, 2024
1 parent 7a2eb23 commit 11cd5b8
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 237 deletions.
145 changes: 10 additions & 135 deletions plugins/lockToVote/components/proposal/description.tsx
Original file line number Diff line number Diff line change
@@ -1,159 +1,34 @@
import { Action } from "@/utils/types";
import { Proposal } from "@/plugins/lockToVote/utils/types";
import { whatsabi } from "@shazow/whatsabi";
import { ReactNode, useCallback, useEffect, useState } from "react";
import { usePublicClient } from "wagmi";
import { Address, decodeFunctionData } from "viem";
import { Else, If, IfCase, IfNot, Then } from "@/components/if";
import { PleaseWaitSpinner } from "@/components/please-wait";
import { AddressText } from "@/components/text/address";
import { isAddress } from "@/utils/evm";
import * as DOMPurify from "dompurify";
import { PUB_CHAIN, PUB_ETHERSCAN_API_KEY } from "@/constants";
import { ActionCard } from "@/components/actions/action";
import { If } from "@/components/if";

const DEFAULT_PROPOSAL_SUMMARY = "(No description available)";

type FunctionData = {
args: readonly unknown[] | undefined;
functionName: string;
to: Address;
};
const DEFAULT_PROPOSAL_METADATA_SUMMARY = "(No description available)";

export default function ProposalDescription(proposal: Proposal) {
const publicClient = usePublicClient({ chainId: PUB_CHAIN.id });
const [decodedActions, setDecodedActions] = useState<FunctionData[]>([]);
const proposalActions = proposal?.actions || [];

const getFunctionData = async (action: Action) => {
if (!publicClient) return;

const abiLoader = new whatsabi.loaders.EtherscanABILoader({
apiKey: PUB_ETHERSCAN_API_KEY,
});

const { abi } = await whatsabi.autoload(action.to, {
provider: publicClient,
abiLoader,
followProxies: true,
});

return decodeFunctionData({
abi,
data: action.data as Address,
});
};

const fetchActionData = useCallback(async () => {
const decodedActions = await Promise.all(
proposalActions.map(async (action) => {
let functionData: any;
if (action.data != "0x") {
functionData = await getFunctionData(action);
} else {
functionData = { functionName: "transfer", args: [action.value] };
}
return { ...functionData, to: action.to } as FunctionData;
})
);
setDecodedActions(decodedActions);
}, [proposal]);

useEffect(() => {
fetchActionData();
}, [proposal.actions]);

return (
<div className="pt-2">
<div
className="pb-6"
dangerouslySetInnerHTML={{
__html: proposal.summary
? DOMPurify.sanitize(proposal.summary)
: DEFAULT_PROPOSAL_SUMMARY,
: DEFAULT_PROPOSAL_METADATA_SUMMARY,
}}
/>
<h2 className="flex-grow text-2xl text-neutral-900 font-semibold pt-10 pb-3">
Actions
</h2>
<div className="flex flex-row space-between">
<IfNot condition={proposalActions.length}>
<div className="">
<If not={proposal.actions.length}>
<p className="pt-2">The proposal has no actions</p>
</IfNot>
<If condition={proposalActions.length && !decodedActions?.length}>
<PleaseWaitSpinner />
</If>
{decodedActions?.map?.((action, i) => (
<ActionCard
key={`${i}-${action.to}-${action.functionName}`}
action={action}
idx={i}
/>
{proposal.actions?.map?.((action, i) => (
<div className="mb-3" key={i}>
<ActionCard action={action} idx={i} />
</div>
))}
</div>
</div>
);
}

// This should be encapsulated as soon as ODS exports this widget
const Card = function ({ children }: { children: ReactNode }) {
return (
<div
className="p-4 lg:p-6 w-full flex flex-col space-y-6
box-border border border-neutral-100
focus:outline-none focus:ring focus:ring-primary
bg-neutral-0 rounded-xl"
>
{children}
</div>
);
};

const ActionCard = function ({
action,
idx,
}: {
action: FunctionData;
idx: number;
}) {
return (
<Card>
<div className="flex flex-row space-between">
<div className="">
<h3>Target contract</h3>
<p>
<AddressText>{action.to}</AddressText>
</p>
</div>
<div className="w-7 h-7 text-center border border-primary-600 text-primary-500 rounded-lg ml-auto">
{idx + 1}
</div>
</div>

<div>
<h3>Function name</h3>
<p>
<code>{action.functionName}</code>
</p>
</div>

<div>
<h3>Parameters</h3>
<ul className="list-disc pl-4">
{action?.args?.length &&
action?.args?.map((arg: any, j: number) => (
<li key={`arg-${j}`}>
<IfCase condition={isAddress(arg)}>
<Then>
<AddressText>{arg.toString()}</AddressText>
</Then>
<Else>
<code>{arg.toString()}</code>
</Else>
</IfCase>
</li>
))}
</ul>
</div>
</Card>
);
};
159 changes: 57 additions & 102 deletions plugins/lockToVote/components/proposal/header.tsx
Original file line number Diff line number Diff line change
@@ -1,90 +1,35 @@
import { useEffect } from "react";
import { Button, Tag } from "@aragon/ods";
import { Proposal } from "@/plugins/lockToVote/utils/types";
import { Proposal } from "@/plugins/dualGovernance/utils/types";
import { AlertVariant } from "@aragon/ods";
import { Else, If, IfCase, Then } from "@/components/if";
import { ElseIf, If, Then, Else } from "@/components/if";
import { AddressText } from "@/components/text/address";
import { useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import { OptimisticTokenVotingPluginAbi } from "../../artifacts/OptimisticTokenVotingPlugin.sol";
import { AlertContextProps, useAlertContext } from "@/context/AlertContext";
import { useProposalVariantStatus } from "../../hooks/useProposalVariantStatus";
import { PUB_CHAIN, PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS } from "@/constants";
import { useProposalVariantStatus } from "@/plugins/lockToVote/hooks/useProposalVariantStatus";
import { PleaseWaitSpinner } from "@/components/please-wait";
import { useRouter } from "next/router";
import dayjs from "dayjs";

const DEFAULT_PROPOSAL_TITLE = "(No proposal title)";

interface ProposalHeaderProps {
proposalNumber: number;
proposal: Proposal;
userCanVeto: boolean;
transactionLoading: boolean;
canVeto: boolean;
canExecute: boolean;
transactionConfirming: boolean;
onVetoPressed: () => void;
onExecutePressed: () => void;
}

const ProposalHeader: React.FC<ProposalHeaderProps> = ({
proposalNumber,
proposal,
userCanVeto,
transactionLoading,
canVeto,
canExecute,
transactionConfirming,
onVetoPressed,
onExecutePressed,
}) => {
const { reload } = useRouter();
const { addAlert } = useAlertContext() as AlertContextProps;
const proposalVariant = useProposalVariantStatus(proposal);

const {
writeContract: executeWrite,
data: executeTxHash,
error,
status,
} = useWriteContract();
const { isLoading: isConfirming, isSuccess: isConfirmed } =
useWaitForTransactionReceipt({ hash: executeTxHash });

const executeButtonPressed = () => {
executeWrite({
chainId: PUB_CHAIN.id,
abi: OptimisticTokenVotingPluginAbi,
address: PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS,
functionName: "execute",
args: [proposalNumber],
});
};

useEffect(() => {
if (status === "idle" || status === "pending") return;
else if (status === "error") {
if (error?.message?.startsWith("User rejected the request")) {
addAlert("Transaction rejected by the user", {
timeout: 4 * 1000,
});
} else {
console.error(error);
addAlert("Could not execute the proposal", { type: "error" });
}
return;
}

// success
if (!executeTxHash) return;
else if (isConfirming) {
addAlert("Proposal submitted", {
description: "Waiting for the transaction to be validated",
type: "info",
txHash: executeTxHash,
});
return;
} else if (!isConfirmed) return;

addAlert("Proposal executed", {
description: "The transaction has been validated",
type: "success",
txHash: executeTxHash,
});

setTimeout(() => reload(), 1000 * 2);
}, [status, executeTxHash, isConfirming, isConfirmed]);
const ended = proposal.parameters.endDate <= Date.now() / 1000;

return (
<div className="w-full">
Expand All @@ -102,52 +47,62 @@ const ProposalHeader: React.FC<ProposalHeaderProps> = ({
/>
</div>
<span className="text-xl font-semibold text-neutral-700 pt-1">
Proposal {proposalNumber + 1}
Proposal {proposalNumber}
</span>
</div>
</div>
<div className="flex ">
<IfCase condition={userCanVeto}>
<div className="flex">
<If condition={transactionConfirming}>
<Then>
<IfCase condition={!transactionLoading}>
<Then>
<Button
className="flex h-5 items-center"
size="lg"
variant="primary"
onClick={() => onVetoPressed()}
>
Veto
</Button>
</Then>
<Else>
<div>
<PleaseWaitSpinner fullMessage="Confirming..." />
</div>
</Else>
</IfCase>
<div>
<PleaseWaitSpinner fullMessage="Confirming..." />
</div>
</Then>
<Else>
<If condition={proposalVariant.label === "Executable"}>
<Button
className="flex h-5 items-center"
size="lg"
variant="success"
onClick={() => executeButtonPressed()}
>
Execute
</Button>
</If>
</Else>
</IfCase>
<ElseIf condition={canVeto}>
<Button
className="flex h-5 items-center"
size="lg"
variant="primary"
onClick={() => onVetoPressed()}
>
Submit veto
</Button>
</ElseIf>
<ElseIf condition={canExecute}>
<Button
className="flex h-5 items-center"
size="lg"
variant="success"
onClick={() => onExecutePressed()}
>
Execute
</Button>
</ElseIf>
</If>
</div>
</div>

<h4 className="flex-grow mb-1 text-3xl text-neutral-900 font-semibold">
{proposal.title || DEFAULT_PROPOSAL_TITLE}
</h4>
<p className="text-base text-l text-body-color dark:text-dark-6">
Proposed by <AddressText>{proposal?.creator}</AddressText>
Proposed by <AddressText>{proposal?.creator}</AddressText>,{" "}
<If condition={ended}>
<Then>
ended on{" "}
{dayjs(Number(proposal.parameters.endDate) * 1000).format(
"D MMM YYYY HH:mm"
)}
h
</Then>
<Else>
ending on{" "}
{dayjs(Number(proposal.parameters.endDate) * 1000).format(
"D MMM YYYY HH:mm"
)}
h
</Else>
</If>
</p>
</div>
);
Expand Down

0 comments on commit 11cd5b8

Please sign in to comment.