From a7f5ec70e50aa95e03bb9a76501074e0e0446dfb Mon Sep 17 00:00:00 2001 From: eightrice Date: Sun, 11 Oct 2026 20:55:31 +0300 Subject: [PATCH 1/2] 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). Claude-Session: https://claude.ai/code/session_01UG9AFKKU31ba2d9QNLV44R --- .../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 da2217d7bd35e5389f66871a60f9609881093b30 Mon Sep 17 00:00:00 2001 From: eightrice Date: Sun, 11 Oct 2026 21:06:32 +0300 Subject: [PATCH 2/2] 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. Claude-Session: https://claude.ai/code/session_01UG9AFKKU31ba2d9QNLV44R --- .../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}