diff --git a/src/components/ui/etherlink/ProposalDialog.tsx b/src/components/ui/etherlink/ProposalDialog.tsx index 68495a4ff..007056c1b 100644 --- a/src/components/ui/etherlink/ProposalDialog.tsx +++ b/src/components/ui/etherlink/ProposalDialog.tsx @@ -1,13 +1,14 @@ import { Grid, Typography, styled } from "@mui/material" export const OptionContainer = styled(Grid)(({ theme }) => ({ - "minHeight": 80, + "minHeight": 110, "background": theme.palette.primary.main, "borderRadius": 8, "padding": "35px 42px", - "marginBottom": 16, "cursor": "pointer", - "height": 110, + // fill the grid cell so cards in a row share the height of the tallest description + "height": "100%", + "boxSizing": "border-box", "&:hover:enabled": { background: theme.palette.secondary.dark, scale: 1.01, diff --git a/src/modules/etherlink/components/EvmResponsiveDialog.tsx b/src/modules/etherlink/components/EvmResponsiveDialog.tsx index 7bc95b187..42229c8f6 100644 --- a/src/modules/etherlink/components/EvmResponsiveDialog.tsx +++ b/src/modules/etherlink/components/EvmResponsiveDialog.tsx @@ -24,6 +24,7 @@ const TitleText = styled(Typography)(({ theme }) => ({ const CustomDialog = styled(Dialog)({ "& .MuiDialog-paperWidthMd": { width: "-webkit-fill-available", + maxWidth: "960px !important", maxHeight: "calc(100vh - 96px)", display: "flex", flexDirection: "column" diff --git a/src/modules/etherlink/explorer/EvmProposalsActionDialog.tsx b/src/modules/etherlink/explorer/EvmProposalsActionDialog.tsx index d1d97d65d..a03c470a3 100644 --- a/src/modules/etherlink/explorer/EvmProposalsActionDialog.tsx +++ b/src/modules/etherlink/explorer/EvmProposalsActionDialog.tsx @@ -84,7 +84,7 @@ export const EvmProposalsActionDialog = ({ open, handleClose }: { open: boolean; Select Proposal Type - + {availableOptions.map((option: any, index) => ( - We need to deploy the ACI Deployer Contract - - Deploy ACI Deployer Contract - - + + + In order to use open-ended Contract Calls, the DAO contract must be amended. + + + If you have the minimum amount of tokens for the proposal fee, click Submit to create a proposal for adding + the Contract Call capability. Once that proposal is executed, this form becomes available. + + + + + + + ) } @@ -474,11 +483,16 @@ const ContractInteractionForm = ({ }) console.log("Lambda Param", param) + // the form takes tez, the lambda pushes mutez + const amountMutez = new BigNumber(values.amount || 0) + .multipliedBy(1_000_000) + .integerValue(BigNumber.ROUND_DOWN) + .toNumber() const lambda = generateExecuteContractMichelson("1.0.0", { address: values.destination_contract_address, entrypoint, type, - amount: values.amount, + amount: amountMutez, param }) @@ -534,7 +548,6 @@ export const ArbitraryContractInteractionForm: React.FC<{ showHeader: (state: boolean) => void }> = ({ daoLambdas, showHeader }) => { const daoId = useDAOID() - const [aciProposalOpen, setAciProposalOpen] = useState(false) const isInvalidKtOrTzAddress = (address: string) => validateContractAddress(address) !== 3 const initialValue: ACIValues = { @@ -578,8 +591,6 @@ export const ArbitraryContractInteractionForm: React.FC<{ console.log({ daoLambdas }) }, [daoLambdas]) - const isAciDeployerDeployed = daoLambdas?.find((lambda: any) => lambda.key === AppConfig.ACI.EXECUTOR_FUNCTION_NAME) - return ( - {daoLambdas !== undefined && ( - setAciProposalOpen(false)} - title={"Proposal to Enable Arbitrary Contract Interaction"} - template="sm" - > - In order to use open-ended Contract Calls, the DAO contract must be amended. - - If you have the minimum amount of tokens for the proposal fee, click Submit to create a proposal for - adding the ACI capability. - - - - - - )} ) }} diff --git a/src/modules/explorer/components/ConfigProposalFormLambda.tsx b/src/modules/explorer/components/ConfigProposalFormLambda.tsx index cdd417b1c..49f414fea 100644 --- a/src/modules/explorer/components/ConfigProposalFormLambda.tsx +++ b/src/modules/explorer/components/ConfigProposalFormLambda.tsx @@ -417,9 +417,14 @@ export const ProposalFormLambda: React.FC<{ return ProposalAction[action] + " Function Proposal" } + // Without the aci_executor handler the Contract Call dialog only shows a short explanation and a + // Submit button; the "md" template stretches to the full viewport, so use the compact one there. + const isAciEnabled = !!daoLambdas?.find(lambda => lambda.key === AppConfig.ACI.EXECUTOR_FUNCTION_NAME) + const dialogTemplate = action === ProposalAction.aci && daoLambdas !== undefined && !isAciEnabled ? "sm" : "md" + return ( - + {state === LambdaProposalState.write_action ? ( <> diff --git a/src/modules/explorer/components/ResponsiveDialog.tsx b/src/modules/explorer/components/ResponsiveDialog.tsx index 235b750fa..805e0ebbd 100644 --- a/src/modules/explorer/components/ResponsiveDialog.tsx +++ b/src/modules/explorer/components/ResponsiveDialog.tsx @@ -22,7 +22,8 @@ const TitleText = styled(Typography)(({ theme }) => ({ const CustomDialog = styled(Dialog)({ "& .MuiDialog-paperWidthMd": { width: "-webkit-fill-available", - height: "-webkit-fill-available" + height: "auto", + maxWidth: "960px !important" }, "& .MuiDialog-paperWidthXs": { width: "-webkit-fill-available", diff --git a/src/modules/explorer/components/VoteDialog.tsx b/src/modules/explorer/components/VoteDialog.tsx index 252e2584e..eef8b4bcb 100644 --- a/src/modules/explorer/components/VoteDialog.tsx +++ b/src/modules/explorer/components/VoteDialog.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo } from "react" import { Grid, Typography, TextField, Button, useTheme, styled } from "@mui/material" -import { useParams } from "react-router-dom" +import { Link, useParams } from "react-router-dom" import { useVote } from "services/contracts/baseDAO/hooks/useVote" import BigNumber from "bignumber.js" import { useDAO } from "services/services/dao/hooks/useDAO" @@ -13,6 +13,8 @@ import { ProposalFormInput } from "./ProposalFormInput" import { getUserTokenBalance } from "services/bakingBad/tokenBalances" import { useTezos } from "services/beacon/hooks/useTezos" import { useTezosBalance } from "services/contracts/baseDAO/hooks/useTezosBalance" +import { useProposals } from "services/services/dao/hooks/useProposals" +import { ProposalStatus } from "services/services/dao/mappers/proposal/types" const CustomLabelsContainer = styled(Grid)({ marginBottom: 12 @@ -31,6 +33,14 @@ const CustomMaxLabel = styled(Typography)({ marginLeft: 12 }) +const HintText = styled(Typography)(({ theme }) => ({ + "fontSize": 14, + "marginTop": 12, + "color": theme.palette.text.primary, + "opacity": 0.75, + "& a": { color: "inherit", textDecoration: "underline" } +})) + const ErrorText = styled(Typography)({ fontSize: 13, marginTop: 8, @@ -82,6 +92,17 @@ export const VoteDialog: React.FC<{ }, [amount, dao, mutate, onClose, proposalId, support, validate]) const { account } = useTezos() + // votes stay staked after a proposal is executed or dropped until the holder releases them + const { data: executedProposals } = useProposals(daoId, ProposalStatus.EXECUTED) + const { data: droppedProposals } = useProposals(daoId, ProposalStatus.DROPPED) + const releasableStake = useMemo( + () => + (executedProposals ?? []).concat(droppedProposals ?? []).reduce((acc, finishedProposal) => { + const vote = finishedProposal.voters.find(voter => voter.address.toLowerCase() === account?.toLowerCase()) + return vote && vote.staked ? acc.plus(vote.value) : acc + }, new BigNumber(0)), + [executedProposals, droppedProposals, account] + ) useMemo(() => { const found = ledger?.find(l => l.holder.address.toLowerCase() === account.toLowerCase()) @@ -157,6 +178,12 @@ export const VoteDialog: React.FC<{ /> {error ? Amount cannot be greater than max available : null} + {releasableStake.gt(0) ? ( + + {releasableStake.toFormat()} {dao?.data.token.symbol} are still staked on finished proposals. Release + them with Unstake on the User page to vote with them. + + ) : null} diff --git a/src/modules/explorer/pages/ProposalDetails/index.tsx b/src/modules/explorer/pages/ProposalDetails/index.tsx index 70c70b507..c34e1eb4d 100644 --- a/src/modules/explorer/pages/ProposalDetails/index.tsx +++ b/src/modules/explorer/pages/ProposalDetails/index.tsx @@ -160,6 +160,19 @@ const formatConfig = { trimMantissa: true } +const proposalKindLabel = (proposal: Proposal | undefined): string => { + if (!proposal || !(proposal instanceof LambdaProposal)) { + return "Treasury Proposal" + } + const { lambdaType, lambdaHandler } = proposal.metadata + if (lambdaType === "add_handler") return "Add Function Proposal" + if (lambdaType === "remove_handler") return "Remove Function Proposal" + if (lambdaType === "execute_handler") { + return lambdaHandler?.handler_name === "aci_executor" ? "Contract Call Proposal" : "Execute Function Proposal" + } + return "Proposal" +} + export const ProposalDetails: React.FC = () => { const { proposalId } = useParams<{ proposalId: string @@ -333,7 +346,7 @@ export const ProposalDetails: React.FC = () => { - Treasury Proposal • Created by + {proposalKindLabel(proposal)} • Created by {proposal && cycleInfo && ( { - const [isLoading, setIsLoading] = useState(true) - const [proposalData, setProposalData] = useState([]) - const { data: daoData, isLoading: isLoadingDAO, error, cycleInfo } = useDAO(contractAddress) + const { data: daoData, error, cycleInfo } = useDAO(contractAddress) const queryResults = useQuery({ - queryKey: ["proposals", contractAddress, status], - queryFn: async () => { - const dao = daoData as BaseDAO - const proposals = await getProposals(dao) - - if (!status) { - return proposals - } - - return proposals.filter( - proposalData => proposalData.getStatus((cycleInfo as CycleInfo).currentLevel).status === status - ) - }, + queryKey: ["proposals", contractAddress], + queryFn: async () => getProposals(daoData as BaseDAO), refetchInterval: 30000, - enabled: !!daoData && !!cycleInfo + enabled: !!daoData }) - useEffect(() => { - if (queryResults.data) { - setProposalData(queryResults.data) + const data = useMemo(() => { + const proposals = queryResults.data ?? [] + if (!status) { + return proposals + } + if (!cycleInfo) { + return [] } - }, [queryResults.data]) + return proposals.filter(proposal => proposal.getStatus((cycleInfo as CycleInfo).currentLevel).status === status) + }, [queryResults.data, status, cycleInfo]) return { - data: proposalData, - isLoading: isLoading || queryResults.isLoading, + data, + isLoading: queryResults.isLoading, error: error || queryResults.error } }