From 96ee6ee03af2d9fff9b01e69eb811b70fdeb1fea Mon Sep 17 00:00:00 2001 From: eightrice Date: Sun, 11 Oct 2026 20:55:31 +0300 Subject: [PATCH 1/3] fix: lambda proposal flow (Execute button, contract call amount, labels) - Execute and Drop Expired buttons stayed disabled on a fresh page load even when proposals were shown as Executable. useProposals filtered by status inside the query function with the cycle info known at the first fetch (block 0 before the TzKT websocket delivers), under a cache key that never changed. The list is now fetched once per DAO and the status filter is applied at render time with the current cycle info. - Contract Call proposals pushed the form's XTZ amount as mutez. Converted to mutez before building the lambda. - Proposal detail page labelled every proposal "Treasury Proposal"; lambda proposals now read Add Function, Remove Function, Execute Function or Contract Call. Verified on a local sandbox stack (deposit, enable ACI, contract call with 1 XTZ attached, vote, Execute on a fresh load; target contract received 1 tez and the string). --- .../ArbitraryContractInteractionForm.tsx | 8 +++- .../explorer/pages/ProposalDetails/index.tsx | 15 ++++++- .../services/dao/hooks/useProposals.ts | 45 +++++++++---------- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/modules/explorer/components/ArbitraryContractInteractionForm.tsx b/src/modules/explorer/components/ArbitraryContractInteractionForm.tsx index 0842eea49..4a193b4fe 100644 --- a/src/modules/explorer/components/ArbitraryContractInteractionForm.tsx +++ b/src/modules/explorer/components/ArbitraryContractInteractionForm.tsx @@ -27,6 +27,7 @@ import ProposalExecuteForm from "./ProposalExecuteForm" import { Expr, MichelsonData } from "@taquito/michel-codec" import { getContract } from "services/contracts/baseDAO" import { TezosToolkit } from "@taquito/taquito" +import { BigNumber } from "bignumber.js" import { useDAO } from "services/services/dao/hooks/useDAO" import { useDAOID } from "../pages/DAO/router" import AppConfig from "config" @@ -474,11 +475,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 }) 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 } } From c045e05ca332e7164f4ebfa5d5bc252263b04fb1 Mon Sep 17 00:00:00 2001 From: eightrice Date: Sun, 11 Oct 2026 21:06:32 +0300 Subject: [PATCH 2/3] fix: single closable dialog when Contract Call is not enabled, staked-votes hint in the vote dialog - In a DAO without the aci_executor handler, choosing Contract Call opened two dialogs on top of each other: the proposal dialog with a bare "deploy" message and a second dialog whose open state was never released, so neither could be closed and the page had to be reloaded. The explanation and the Submit button (which leads to the add-function proposal) now live inside the single proposal dialog, which closes normally. - The vote dialog explains why the maximum is lower than expected when the voter still has tokens staked on executed or dropped proposals, with a link to the User page where Unstake releases them. --- .../ArbitraryContractInteractionForm.tsx | 42 +++++++------------ .../explorer/components/VoteDialog.tsx | 29 ++++++++++++- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/src/modules/explorer/components/ArbitraryContractInteractionForm.tsx b/src/modules/explorer/components/ArbitraryContractInteractionForm.tsx index 4a193b4fe..452fb679b 100644 --- a/src/modules/explorer/components/ArbitraryContractInteractionForm.tsx +++ b/src/modules/explorer/components/ArbitraryContractInteractionForm.tsx @@ -33,7 +33,6 @@ import { useDAOID } from "../pages/DAO/router" import AppConfig from "config" import { Link } from "react-router-dom" import { Button } from "components/ui/Button" -import { ResponsiveDialog } from "./ResponsiveDialog" import { useNotification } from "modules/common/hooks/useNotification" // Base ACI Lambda @@ -251,13 +250,22 @@ const ContractInteractionForm = ({ } if (!isAciDeployerDeployed && !isLoading) { + // the DAO has no aci_executor handler yet: explain and point to the add-function proposal (one dialog, closable) return ( -
- 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. + + + + + + + ) } @@ -540,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 = { @@ -584,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/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}
From 15fe91a3e4f12cba0a5dbc3cd054dde3e9741455 Mon Sep 17 00:00:00 2001 From: eightrice Date: Wed, 11 Nov 2026 11:35:24 +0200 Subject: [PATCH 3/3] fix: dialogs no longer stretch to the full viewport - ResponsiveDialog "md" template filled the whole viewport (width and height). It now fits its content and is capped at 960px wide, like the other templates. Affects the lambda proposal dialogs and the Etherlink member profile. - The Contract Call dialog in a DAO without the aci_executor handler only shows a short explanation, so it uses the compact template. - Etherlink new proposal: the form and review steps ("md") stretched edge to edge; capped at 960px. The proposal type cards had a fixed height and their descriptions overflowed; cards now grow with their text and share the row height. --- src/components/ui/etherlink/ProposalDialog.tsx | 7 ++++--- src/modules/etherlink/components/EvmResponsiveDialog.tsx | 1 + .../etherlink/explorer/EvmProposalsActionDialog.tsx | 2 +- .../explorer/components/ConfigProposalFormLambda.tsx | 7 ++++++- src/modules/explorer/components/ResponsiveDialog.tsx | 3 ++- 5 files changed, 14 insertions(+), 6 deletions(-) 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) => ( 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",