"use client"; import { memo, useEffect, useMemo, useState } from "react"; import { AdjustmentsHorizontalIcon, XMarkIcon, CheckIcon, BoltIcon, ArrowTopRightOnSquareIcon, } from "@heroicons/react/24/outline"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { toast } from "react-toastify"; import { Address, encodeAbiParameters, formatUnits } from "viem"; import { useAccount, useBalance, useContractRead } from "wagmi"; import { getProposalDataDocument, getProposalDataQuery, getProposalSupportersQuery, getProposalSupportersDocument, getPoolDataDocument, getPoolDataQuery, isMemberDocument, isMemberQuery, getMembersStrategyDocument, getMembersStrategyQuery, } from "#/subgraph/.graphclient"; import { Badge, Button, Countdown, DisplayNumber, EthAddress, InfoBox, LiveFlowingAmount, Statistic, DataTable, } from "@/components"; import CancelButton from "@/components/CancelButton"; import { ConvictionBarChart } from "@/components/Charts/ConvictionBarChart"; import { DisputeModal } from "@/components/DisputeModal"; import EditProposalButton from "@/components/EditProposalButton"; import { LoadingSpinner } from "@/components/LoadingSpinner"; import { LoupeButton } from "@/components/LoupeButton"; import MarkdownWrapper from "@/components/MarkdownWrapper"; import { Skeleton } from "@/components/Skeleton"; import { chainConfigMap } from "@/configs/chains"; import { QUERY_PARAMS } from "@/constants/query-params"; import { useCollectQueryParams } from "@/contexts/collectQueryParams.context"; import { PendingIndexedPublish, usePubSubContext, } from "@/contexts/pubsub.context"; import { useChainIdFromPath } from "@/hooks/useChainIdFromPath"; import { useContractWriteWithConfirmations } from "@/hooks/useContractWriteWithConfirmations"; import { useConvictionRead } from "@/hooks/useConvictionRead"; import { ConditionObject, useDisableButtons } from "@/hooks/useDisableButtons"; import { useFlag } from "@/hooks/useFlag"; import { MetadataV1, useMetadataIpfsFetch } from "@/hooks/useIpfsFetch"; import { useOnchainProposalStatus } from "@/hooks/useOnchainProposalStatus"; import { usePoolToken } from "@/hooks/usePoolToken"; import { dismissPendingSubgraphRefreshToast, useSubgraphQuery, } from "@/hooks/useSubgraphQuery"; import { useSuperfluidStream } from "@/hooks/useSuperfluidStream"; import { useSuperfluidToken } from "@/hooks/useSuperfluidToken"; import { superTokenABI } from "@/src/customAbis"; import { alloABI, cvStrategyABI } from "@/src/generated"; import { PoolTypes, ProposalStatus, Column } from "@/types"; import { useErrorDetails } from "@/utils/getErrorName"; import { logOnce } from "@/utils/log"; import { SEC_TO_MONTH, calculatePercentageBigInt, roundToSignificant, } from "@/utils/numbers"; import { createMemberOptimisticProjector } from "@/utils/optimisticMembers"; import { createProposalOptimisticProjector, getLatestProposalCreation, } from "@/utils/optimisticProposals"; import { buildProposalEntityId, extractProposalNumber, } from "@/utils/proposals"; import { formatLastRebalanceTooltip, prettyTimestamp } from "@/utils/text"; type ProposalSupporter = { id: string; stakes: { amount: number | string | bigint }[]; activatedPoints?: number | string | bigint | null; }; type SupporterColumn = Column; const SYNC_STREAM_HIDE_WINDOW_SECONDS = 15 * 60; const toBigIntValue = (value: unknown): bigint => { if (typeof value === "bigint") return value; if (typeof value === "number") return BigInt(Math.trunc(value)); if (typeof value === "string") { try { return BigInt(value); } catch { return 0n; } } return 0n; }; const getSupporterStakeAmount = (supporter: ProposalSupporter) => (supporter.stakes ?? []).reduce( (total, stake) => total + toBigIntValue(stake.amount), 0n, ); const getSupporterVotingPowerUsedPct = (supporter: ProposalSupporter) => { const activatedPoints = toBigIntValue(supporter.activatedPoints); return activatedPoints > 0n ? calculatePercentageBigInt( getSupporterStakeAmount(supporter), activatedPoints, ) : undefined; }; const normalizeEntityId = (value: unknown) => value?.toString().toLowerCase(); const getSupporterPoolVotingPower = ( supporter: ProposalSupporter, totalActivePoints: number | string | bigint | undefined, ) => { const activePoints = toBigIntValue(totalActivePoints); return activePoints > 0n ? calculatePercentageBigInt( getSupporterStakeAmount(supporter), activePoints, ) : undefined; }; const getMemberIdFromMemberStrategyId = ( memberStrategyId: string, strategyId: string, ) => { const normalizedMemberStrategyId = memberStrategyId.toLowerCase(); const strategySuffix = `-${strategyId.toLowerCase()}`; return normalizedMemberStrategyId.endsWith(strategySuffix) ? normalizedMemberStrategyId.slice(0, -strategySuffix.length) : undefined; }; const formatVotingPowerPct = (value: number | undefined) => value == null ? "--" : ( `${value.toLocaleString(undefined, { maximumFractionDigits: 2, })}%` ); const formatVotingPower = (value: number | undefined) => value == null ? "--" : ( `${value.toLocaleString(undefined, { maximumFractionDigits: 2, })} VP` ); const getElapsedMs = ({ flowRateBn, lastSnapshotAtMs, nowMs, }: { flowRateBn: bigint; lastSnapshotAtMs: bigint; nowMs: bigint; }) => flowRateBn > 0n && lastSnapshotAtMs > 0n && nowMs > lastSnapshotAtMs ? nowMs - lastSnapshotAtMs : 0n; const getLiveEscrowBalanceBn = ({ escrowBalanceSnapshotBn, escrowBalanceSnapshotAtMs, proposalFlowRateBn, nowMs, fallbackEscrowBalanceBn, }: { escrowBalanceSnapshotBn: bigint | null; escrowBalanceSnapshotAtMs: bigint; proposalFlowRateBn: bigint; nowMs: bigint; fallbackEscrowBalanceBn?: bigint; }) => { const escrowBalanceElapsedMs = ( escrowBalanceSnapshotBn != null && proposalFlowRateBn > 0n && nowMs > escrowBalanceSnapshotAtMs ) ? nowMs - escrowBalanceSnapshotAtMs : 0n; return escrowBalanceSnapshotBn != null ? escrowBalanceSnapshotBn + (proposalFlowRateBn * escrowBalanceElapsedMs) / 1000n : fallbackEscrowBalanceBn; }; const getProposalTotalStreamedToBeneficiaryBn = ({ proposalFlowRateBn, streamedUntilSnapshotBn, lastSnapshotAtBn, nowMs, explorerTotalStreamedBn, isDisputedStreamingProposal, escrowBalanceSnapshotBn, escrowBalanceSnapshotAtMs, escrowSuperTokenBalanceValue, }: { proposalFlowRateBn: bigint; streamedUntilSnapshotBn: bigint; lastSnapshotAtBn: bigint; nowMs: bigint; explorerTotalStreamedBn?: bigint | null; isDisputedStreamingProposal: boolean; escrowBalanceSnapshotBn: bigint | null; escrowBalanceSnapshotAtMs: bigint; escrowSuperTokenBalanceValue?: bigint; }) => { const lastSnapshotAtMs = lastSnapshotAtBn * 1000n; const elapsedMs = getElapsedMs({ flowRateBn: proposalFlowRateBn, lastSnapshotAtMs, nowMs, }); const totalReceivedByEscrowBn = streamedUntilSnapshotBn + (proposalFlowRateBn * elapsedMs) / 1000n; const liveEscrowSuperTokenBalanceBn = getLiveEscrowBalanceBn({ escrowBalanceSnapshotBn, escrowBalanceSnapshotAtMs, proposalFlowRateBn, nowMs, fallbackEscrowBalanceBn: escrowSuperTokenBalanceValue, }); const currentEscrowSuperTokenBalanceBn = isDisputedStreamingProposal ? liveEscrowSuperTokenBalanceBn ?? 0n : escrowSuperTokenBalanceValue ?? liveEscrowSuperTokenBalanceBn ?? 0n; const totalReceivedBn = explorerTotalStreamedBn ?? totalReceivedByEscrowBn; return totalReceivedBn > currentEscrowSuperTokenBalanceBn ? totalReceivedBn - currentEscrowSuperTokenBalanceBn : 0n; }; const getLiveBeneficiaryBalanceBn = ({ beneficiaryBalanceSnapshotBn, beneficiaryBalanceSnapshotAtMs, proposalFlowRateBn, nowMs, }: { beneficiaryBalanceSnapshotBn: bigint | null; beneficiaryBalanceSnapshotAtMs: bigint; proposalFlowRateBn: bigint; nowMs: bigint; }) => { const beneficiaryElapsedMs = ( beneficiaryBalanceSnapshotBn != null && proposalFlowRateBn > 0n && nowMs > beneficiaryBalanceSnapshotAtMs ) ? nowMs - beneficiaryBalanceSnapshotAtMs : 0n; return beneficiaryBalanceSnapshotBn != null ? beneficiaryBalanceSnapshotBn + (proposalFlowRateBn * beneficiaryElapsedMs) / 1000n : null; }; const LiveAccumulatedAmount = memo(function LiveAccumulatedAmount({ poolToken, proposalFlowRateBn, escrowBalanceSnapshotBn, escrowBalanceSnapshotAtMs, escrowSuperTokenBalanceValue, escrowDepositAmount, }: { poolToken?: { decimals: number; symbol: string } | null; proposalFlowRateBn: bigint; escrowBalanceSnapshotBn: bigint | null; escrowBalanceSnapshotAtMs: bigint; escrowSuperTokenBalanceValue?: bigint; escrowDepositAmount?: bigint; }) { const [nowMs, setNowMs] = useState(() => BigInt(Date.now())); useEffect(() => { if (proposalFlowRateBn <= 0n) return; const interval = setInterval(() => { if (typeof document !== "undefined" && document.hidden) return; setNowMs(BigInt(Date.now())); }, 100); return () => clearInterval(interval); }, [proposalFlowRateBn]); const escrowBalanceElapsedMs = ( escrowBalanceSnapshotBn != null && proposalFlowRateBn > 0n && nowMs > escrowBalanceSnapshotAtMs ) ? nowMs - escrowBalanceSnapshotAtMs : 0n; const liveEscrowSuperTokenBalanceBn = escrowBalanceSnapshotBn != null ? escrowBalanceSnapshotBn + (proposalFlowRateBn * escrowBalanceElapsedMs) / 1000n : null; const currentEscrowSuperTokenBalanceBn = liveEscrowSuperTokenBalanceBn ?? escrowSuperTokenBalanceValue ?? 0n; const accumulatedAmountBn = ( escrowDepositAmount != null && currentEscrowSuperTokenBalanceBn > escrowDepositAmount ) ? currentEscrowSuperTokenBalanceBn - escrowDepositAmount : 0n; const accumulatedAmountDisplay = poolToken ? Number(formatUnits(accumulatedAmountBn, poolToken.decimals)).toFixed(6) : "--"; return ( {accumulatedAmountDisplay} ); }); export type ProposalPageParams = { proposalId: string; community: string; pool: string; chain: string; }; export type ClientPageProps = { params: ProposalPageParams; }; export default function ClientPage({ params }: ClientPageProps) { const { proposalId: proposalSlug, community: communityAddr, pool: poolSlug, } = params; useEffect(() => { logOnce( "debug", "Loading page: (app)/gardens/[chain]/[community]/[pool]/[proposalId]/page.tsx", ); }, []); const strategyAddress = poolSlug.toLowerCase(); const [convictionRefreshing, setConvictionRefreshing] = useState(true); const [openSupportersModal, setOpenSupportersModal] = useState(false); const [selectedTab, setSelectedTab] = useState(0); const [nowTs, setNowTs] = useState(() => Math.floor(Date.now() / 1000)); const [escrowBalanceSnapshotBn, setEscrowBalanceSnapshotBn] = useState< bigint | null >(null); const [escrowBalanceSnapshotAtMs, setEscrowBalanceSnapshotAtMs] = useState(0n); const [beneficiaryBalanceSnapshotBn, setBeneficiaryBalanceSnapshotBn] = useState(null); const [beneficiaryBalanceSnapshotAtMs, setBeneficiaryBalanceSnapshotAtMs] = useState(0n); const router = useRouter(); const { address } = useAccount(); const { publishAfterIndexed, pendingIndexedPublishes } = usePubSubContext(); const routerSearchParams = useSearchParams(); const collectedParams = useCollectQueryParams(); const [initialSearchParams] = useState | null>(() => { if (typeof window === "undefined") return null; return Object.fromEntries(new URLSearchParams(window.location.search)); }); const proposalNumber = extractProposalNumber(proposalSlug); const proposalEntityId = buildProposalEntityId(strategyAddress, proposalSlug); const memberOptimisticProjector = useMemo( () => createMemberOptimisticProjector({ communityId: communityAddr, strategyId: strategyAddress, memberAddress: address, }), [address, communityAddr, strategyAddress], ); const proposalOptimistic = useMemo( () => ({ scope: [ { topic: "proposal" as const, containerId: strategyAddress, }, { topic: "member" as const, containerId: communityAddr, id: address }, { topic: "member" as const, containerId: strategyAddress, id: address }, ], apply( data: TData | undefined, records: PendingIndexedPublish[], ) { return memberOptimisticProjector( createProposalOptimisticProjector({ strategyId: strategyAddress, proposalId: proposalEntityId, proposalNumber, allocator: address, })(data, records), records, ); }, }), [ address, communityAddr, memberOptimisticProjector, proposalEntityId, proposalNumber, strategyAddress, ], ); const { data, fetching, refetch: refetchProposal, } = useSubgraphQuery({ query: getProposalDataDocument, variables: { proposalId: proposalEntityId?.toLowerCase(), communityId: communityAddr?.toLowerCase(), }, changeScope: strategyAddress != null ? { topic: "proposal", containerId: strategyAddress, id: proposalNumber, type: "update", } : undefined, optimistic: proposalOptimistic, }); const optimisticProposalCreation = getLatestProposalCreation( pendingIndexedPublishes, { strategyId: strategyAddress, proposalId: proposalEntityId, proposalNumber, }, ); const { data: poolData } = useSubgraphQuery({ query: getPoolDataDocument, variables: { strategyId: strategyAddress, }, enabled: optimisticProposalCreation != null && data?.cvproposal == null, changeScope: strategyAddress != null ? { topic: "proposal", containerId: strategyAddress, type: "update", } : undefined, optimistic: proposalOptimistic, }); //query to get proposal supporters const { data: supportersData } = useSubgraphQuery( { query: getProposalSupportersDocument, variables: { proposalId: proposalEntityId.toLowerCase(), }, optimistic: proposalOptimistic, }, ); const { data: membersStrategyData } = useSubgraphQuery({ query: getMembersStrategyDocument, variables: { strategyId: strategyAddress, }, changeScope: strategyAddress != null ? { topic: "proposal", containerId: strategyAddress, type: "update", } : undefined, optimistic: proposalOptimistic, }); //query to get member registry in community const { data: memberData } = useSubgraphQuery({ query: isMemberDocument, variables: { me: address?.toLowerCase(), comm: communityAddr?.toLowerCase(), }, enabled: !!address && !!communityAddr, optimistic: proposalOptimistic, }); const isMemberCommunity = !!memberData?.member?.memberCommunity?.[0]?.isRegistered; // type ProposalData = NonNullable; const proposalDataFromPool = poolData?.cvstrategies[0]?.proposals.find((proposal) => { const proposalIdMatches = normalizeEntityId(proposal.id) === normalizeEntityId(proposalEntityId); const proposalNumberMatches = normalizeEntityId(proposal.proposalNumber) === normalizeEntityId(proposalNumber); return proposalIdMatches || proposalNumberMatches; }); const poolStrategyData = poolData?.cvstrategies[0]; const proposalData: | (ProposalData & { registryCommunity?: getProposalDataQuery["registryCommunity"]; }) | undefined = data?.cvproposal ? { ...data.cvproposal, registryCommunity: data?.registryCommunity, } : proposalDataFromPool && poolStrategyData ? ({ ...proposalDataFromPool, strategy: { ...poolStrategyData, id: poolStrategyData.id, maxCVSupply: proposalDataFromPool.strategy?.maxCVSupply ?? poolStrategyData.maxCVSupply, totalEffectiveActivePoints: proposalDataFromPool.strategy?.totalEffectiveActivePoints ?? poolStrategyData.totalEffectiveActivePoints, }, registryCommunity: poolStrategyData.registryCommunity, arbitrableConfig: poolData?.arbitrableConfigs[0] ?? null, updatedAt: null, version: null, } as ProposalData & { registryCommunity?: getProposalDataQuery["registryCommunity"]; }) : undefined; const proposalSupporters = supportersData?.members ?? []; const activatedPointsByMember = useMemo(() => { const activatedPoints = new Map(); membersStrategyData?.memberStrategies.forEach((memberStrategy) => { const memberId = getMemberIdFromMemberStrategyId( memberStrategy.id, strategyAddress, ); if (memberId == null) return; activatedPoints.set( memberId, toBigIntValue(memberStrategy.activatedPoints), ); }); return activatedPoints; }, [membersStrategyData?.memberStrategies, strategyAddress]); const totalEffectiveActivePoints = proposalData?.strategy?.totalEffectiveActivePoints; const filteredAndSortedProposalSupporters: ProposalSupporter[] = proposalSupporters .filter((item) => item.stakes && item.stakes.length > 0) .map((item) => ({ id: item.id, stakes: item.stakes?.map((stake) => ({ amount: stake.amount })) ?? [], activatedPoints: activatedPointsByMember.get(item.id.toLowerCase()), })) .sort((a, b) => { const stakeA = getSupporterStakeAmount(a); const stakeB = getSupporterStakeAmount(b); return ( stakeA === stakeB ? 0 : stakeA < stakeB ? 1 : -1 ); }); // const proposalIdNumber = proposalData?.proposalNumber != null ? BigInt(proposalData.proposalNumber) : undefined; const chainId = useChainIdFromPath(); const { status: resolvedProposalStatusCode } = useOnchainProposalStatus({ strategyAddress: proposalData?.strategy?.id as Address | undefined, proposalNumber: proposalIdNumber, chainId, fallbackStatus: proposalData?.proposalStatus, enabled: proposalData != null, }); const shouldReadOnchainMetadataHash = !!proposalData?.strategy?.id && proposalIdNumber != null && !proposalData?.metadata && !proposalData?.metadataHash; const poolTokenAddr = proposalData?.strategy?.token as Address; const { data: onchainMetadataHash, isLoading: isOnchainMetadataHashLoading, isFetched: isOnchainMetadataHashFetched, } = useContractRead({ address: proposalData?.strategy?.id as Address, abi: cvStrategyABI, functionName: "getProposalMetadataPointer", args: proposalIdNumber != null ? [proposalIdNumber] : ([0n] as const), chainId, enabled: shouldReadOnchainMetadataHash, }); const resolvedMetadataHash = proposalData?.metadataHash ?? onchainMetadataHash ?? optimisticProposalCreation?.optimistic?.metadataHash ?? undefined; const hasResolvedMetadataHash = typeof resolvedMetadataHash === "string" && resolvedMetadataHash.length > 0; const { data: ipfsResult, fetching: isIpfsMetadataFetching } = useMetadataIpfsFetch({ hash: resolvedMetadataHash, enabled: hasResolvedMetadataHash && proposalData?.metadata == null, }); const isMetadataLoading = proposalData != null && proposalData.metadata == null && ((shouldReadOnchainMetadataHash && (isOnchainMetadataHashLoading || !isOnchainMetadataHashFetched)) || (hasResolvedMetadataHash && isIpfsMetadataFetching)); const path = usePathname(); const metadata: MetadataV1 | null = proposalData ? proposalData.metadata ?? ipfsResult ?? { title: `Proposal #${proposalData.proposalNumber ?? proposalNumber}`, description: "", } : null; const metadataForActions: MetadataV1 = (metadata ?? { title: undefined, description: undefined, }) as MetadataV1; const proposalDataForActions = proposalData ? { ...proposalData, proposalStatus: (resolvedProposalStatusCode ?? proposalData.proposalStatus) as ProposalData["proposalStatus"], ...metadataForActions, metadata: metadataForActions, } : undefined; const isProposerConnected = proposalData?.submitter === address?.toLowerCase(); const pendingProposalParam = collectedParams[QUERY_PARAMS.proposalPage.pendingProposal] ?? routerSearchParams.get(QUERY_PARAMS.proposalPage.pendingProposal) ?? initialSearchParams?.[QUERY_PARAMS.proposalPage.pendingProposal] ?? undefined; const proposalType = proposalData?.strategy?.config?.proposalType; const isSignalingType = PoolTypes[proposalType] === "signaling"; const isStreamingType = PoolTypes[proposalType] === "streaming"; const requestedAmount = proposalData?.requestedAmount; const beneficiary = proposalData?.beneficiary as Address | undefined; const streamingEscrowFromSubgraph = (( proposalData as | { streamingEscrow?: Address | null; } | undefined )?.streamingEscrow ?? undefined) as Address | undefined; const resolvedStreamingEscrow = streamingEscrowFromSubgraph; const submitter = proposalData?.submitter as Address | undefined; const superfluidExplorerBaseUrl = chainId != null ? ( chainConfigMap as Record< string | number, { superfluidExplorerUrl?: string } > )[chainId]?.superfluidExplorerUrl : undefined; const superfluidExplorerUrl = ( superfluidExplorerBaseUrl != null && superfluidExplorerBaseUrl !== "" && resolvedStreamingEscrow != null ) ? `${superfluidExplorerBaseUrl}/accounts/${resolvedStreamingEscrow.toLowerCase()}?tab=streams` : undefined; const showEscrow = useFlag("showEscrow"); const proposalStatusCode = resolvedProposalStatusCode ?? Number(proposalData?.proposalStatus); const proposalStatus = ProposalStatus[proposalStatusCode]; const shouldShowSupportersTab = proposalStatus !== "executed" && proposalStatus !== "cancelled"; const poolTokenResult = usePoolToken({ poolAddress: strategyAddress, poolTokenAddr, chainId, enabled: !!poolTokenAddr && !!strategyAddress && !isSignalingType, }); const poolToken = poolTokenResult?.poolToken; const { superToken: poolSuperToken } = useSuperfluidToken({ token: poolTokenAddr, chainId, enabled: isStreamingType && !!poolTokenAddr, }); const isPureSuperfluidPoolToken = poolSuperToken?.sameAsUnderlying === true; const proposalStream = // Backward-compatible read while graph client schema catches up. (proposalData as { proposalStream?: unknown; proposalStreams?: unknown[] }) ?.proposalStream ?? ( proposalData as { proposalStream?: unknown; proposalStreams?: unknown[]; } )?.proposalStreams?.[0]; const proposalFlowRateBn = ( proposalStream && typeof proposalStream === "object" && "currentFlowRate" in proposalStream ) ? toBigIntValue(proposalStream.currentFlowRate) : 0n; const streamedUntilSnapshotBn = ( proposalStream && typeof proposalStream === "object" && "streamedUntilSnapshot" in proposalStream ) ? toBigIntValue(proposalStream.streamedUntilSnapshot) : 0n; const lastSnapshotAtBn = ( proposalStream && typeof proposalStream === "object" && "lastSnapshotAt" in proposalStream ) ? toBigIntValue(proposalStream.lastSnapshotAt) : 0n; const minThresholdPointsBn = toBigIntValue( proposalData?.strategy?.config?.minThresholdPoints ?? 0, ); const superfluidStreamResult = useSuperfluidStream({ receiver: resolvedStreamingEscrow as Address, superToken: proposalData?.strategy?.config?.superfluidToken as Address, chainId, containerId: strategyAddress, }); const explorerTotalStreamedBn = ( superfluidStreamResult as typeof superfluidStreamResult & { liveTotalStreamedBn?: bigint | null; } )?.liveTotalStreamedBn; const isDisputedStreamingProposal = isStreamingType && proposalStatus === "disputed"; const proposalFlowPerMonth = ( isStreamingType && poolToken && proposalFlowRateBn != null && proposalFlowRateBn > 0n ) ? +formatUnits(proposalFlowRateBn, poolToken.decimals) * SEC_TO_MONTH : null; const superTokenAddress = proposalData?.strategy?.config?.superfluidToken as | Address | undefined; const isBeneficiaryConnected = beneficiary === address?.toLowerCase(); const streamTokenDecimals = poolToken?.decimals ?? 18; const currentFlowRateForDisplay = proposalFlowRateBn; const isThresholdOutOfReach = minThresholdPointsBn > BigInt(proposalData?.strategy?.totalEffectiveActivePoints ?? 0); const { data: beneficiarySuperTokenBalance, refetch: refetchSuperToken } = useBalance({ address: beneficiary, token: superTokenAddress, chainId, enabled: isStreamingType && !!beneficiary && !!superTokenAddress, }); const { data: escrowSuperTokenBalance } = useBalance({ address: resolvedStreamingEscrow as Address, token: superTokenAddress, chainId, enabled: isStreamingType && !!resolvedStreamingEscrow && !!superTokenAddress, }); const displayedNowMs = useMemo( () => BigInt(Date.now()), [ proposalFlowRateBn, streamedUntilSnapshotBn, lastSnapshotAtBn, explorerTotalStreamedBn, isDisputedStreamingProposal, escrowBalanceSnapshotBn, escrowBalanceSnapshotAtMs, escrowSuperTokenBalance?.value, beneficiaryBalanceSnapshotBn, beneficiaryBalanceSnapshotAtMs, ], ); const { currentConvictionPct, thresholdPct, stableThresholdPct, isThresholdBelowDisplayPrecision, hasReachedThreshold, willReachThreshold, totalSupportPct, timeToPass, triggerConvictionRefetch, } = useConvictionRead({ proposalData: proposalData as getProposalDataQuery["cvproposal"], strategyConfig: proposalData?.strategy?.config, tokenData: proposalData?.strategy?.registryCommunity?.garden?.decimals, chainId, enabled: proposalData?.proposalNumber != null && proposalData != null, }); const isProposalCoreReady = proposalData != null && proposalIdNumber != null && strategyAddress != null; const isAwaitingProposal = !!pendingProposalParam && !isProposalCoreReady; useEffect(() => { if (fetching || !isAwaitingProposal) return; refetchProposal({ showToast: false }); }, [fetching, isAwaitingProposal]); useEffect(() => { if (pendingProposalParam && !isAwaitingProposal) { dismissPendingSubgraphRefreshToast(); } }, [isAwaitingProposal, pendingProposalParam]); useEffect(() => { if (escrowSuperTokenBalance?.value == null) return; setEscrowBalanceSnapshotBn(escrowSuperTokenBalance.value); setEscrowBalanceSnapshotAtMs(BigInt(Date.now())); }, [escrowSuperTokenBalance?.value]); useEffect(() => { if (beneficiarySuperTokenBalance?.value == null) return; setBeneficiaryBalanceSnapshotBn(beneficiarySuperTokenBalance.value); setBeneficiaryBalanceSnapshotAtMs(BigInt(Date.now())); }, [beneficiarySuperTokenBalance?.value]); useEffect(() => { if (convictionRefreshing && currentConvictionPct != null) { setConvictionRefreshing(false); } }, [convictionRefreshing, currentConvictionPct]); useEffect(() => { if (!shouldShowSupportersTab && selectedTab === 3) { setSelectedTab(0); } }, [selectedTab, shouldShowSupportersTab]); //encode proposal id to pass as argument to distribute function const encodedDataProposalId = (proposalId_: bigint) => { const encodedProposalId = encodeAbiParameters( [{ name: "proposalId", type: "uint" }], [proposalId_], ); return encodedProposalId; }; //distribution function from Allo contract //args: poolId, strategyId, encoded proposalId const { write: writeDistribute, error: errorDistribute, isError: isErrorDistribute, } = useContractWriteWithConfirmations({ address: data?.allos[0]?.id as Address, abi: alloABI, functionName: "distribute", contractName: "Allo", fallbackErrorMessage: "Error executing proposal, please report a bug.", onConfirmations: (receipt) => { publishAfterIndexed( receipt, { topic: "proposal", type: "update", function: "distribute", id: proposalNumber, containerId: strategyAddress, chainId, }, { optimistic: { kind: "proposal-status", strategyId: strategyAddress, proposalId: proposalEntityId, proposalNumber: proposalNumber.toString(), status: "executed", }, }, ); }, }); const manageSupportClicked = () => { const pathSegments = path.split("/"); pathSegments.pop(); if (pathSegments.length === 3) { pathSegments.pop(); } const newPath = pathSegments.join("/"); router.push(newPath + `?${QUERY_PARAMS.poolPage.allocationView}=true`); }; const distributeErrorName = useErrorDetails(errorDistribute); useEffect(() => { if (isErrorDistribute && distributeErrorName.errorName !== undefined) { toast.error("NOT EXECUTABLE:" + " " + distributeErrorName.errorName); } }, [isErrorDistribute]); const disableManSupportBtn = useMemo( () => [ { condition: !isMemberCommunity, message: "Join community to support", }, ], [address], ); const { tooltipMessage, isConnected, missmatchUrl } = useDisableButtons(disableManSupportBtn); const { tooltipMessage: syncStreamTooltipMessage, isConnected: isSyncStreamConnected, missmatchUrl: isSyncStreamWrongNetwork, } = useDisableButtons(); const hasInsufficientPoolFunds = !isSignalingType && !isStreamingType && requestedAmount != null && poolToken?.balance != null && BigInt(requestedAmount) > poolToken.balance; const disableExecuteButton = useMemo( () => [ { condition: hasInsufficientPoolFunds, message: "Insufficient funds in the pool to execute this proposal.", }, { condition: hasReachedThreshold !== true, message: "Proposal has not reached the threshold yet.", }, { condition: proposalStatus === "disputed", message: "Proposal is being disputed", }, ], [hasReachedThreshold, proposalStatus, hasInsufficientPoolFunds], ); const { tooltipMessage: executeBtnTooltipMessage, isButtonDisabled: isExecuteButtonDisabled, } = useDisableButtons(disableExecuteButton); useEffect(() => { const intervalId = window.setInterval(() => { setNowTs(Math.floor(Date.now() / 1000)); }, 15000); return () => { clearInterval(intervalId); }; }, []); const { data: lastRebalanceAtValue, refetch: refetchLastRebalanceAt } = useContractRead({ address: proposalData?.strategy?.id as Address, abi: cvStrategyABI, functionName: "lastRebalanceAt", chainId, enabled: isStreamingType && !!proposalData?.strategy?.id, watch: true, } as any); const { data: isAuthorizedRebalanceCaller } = useContractRead({ address: proposalData?.strategy?.id as Address, abi: cvStrategyABI, functionName: "isAuthorizedRebalanceCaller", args: address ? [address] : undefined, chainId, enabled: isStreamingType && !!proposalData?.strategy?.id && !!address, watch: true, } as any); const { data: escrowDepositAmount } = useContractRead({ address: resolvedStreamingEscrow as Address, chainId, abi: [ { type: "function", stateMutability: "view", name: "depositAmount", inputs: [], outputs: [{ name: "", type: "uint256" }], }, ] as const, functionName: "depositAmount", enabled: isDisputedStreamingProposal && !!resolvedStreamingEscrow, } as any); const lastRebalanceAt = Number(lastRebalanceAtValue ?? 0n); const hideSyncStreamButton = lastRebalanceAt > 0 && nowTs - lastRebalanceAt < SYNC_STREAM_HIDE_WINDOW_SECONDS; const showSyncStreamButton = !hideSyncStreamButton && !!address && isAuthorizedRebalanceCaller === true; const lastRebalanceTooltip = formatLastRebalanceTooltip(lastRebalanceAt); const syncStreamButtonTooltip = syncStreamTooltipMessage != null ? `${syncStreamTooltipMessage}\n${lastRebalanceTooltip}` : lastRebalanceTooltip; const { write: writeRebalance, isLoading: isRebalanceLoading } = useContractWriteWithConfirmations({ address: proposalData?.strategy?.id as Address, abi: cvStrategyABI, functionName: "rebalance" as any, contractName: "CVStrategy", fallbackErrorMessage: "Failed to sync stream for this strategy. Please try again.", onConfirmations: (receipt) => { void refetchLastRebalanceAt(); publishAfterIndexed(receipt, { topic: "stream", containerId: strategyAddress, function: "rebalance", chainId, } as any); }, }); const { write: writeUnwrapSuperToken, isLoading: isUnwrapSuperTokenLoading } = useContractWriteWithConfirmations({ address: superTokenAddress as Address, abi: superTokenABI, functionName: "downgrade", contractName: "SuperToken", fallbackErrorMessage: "Failed to unwrap super token for this proposal. Please try again.", onConfirmations: async () => { setBeneficiaryBalanceSnapshotBn(0n); setBeneficiaryBalanceSnapshotAtMs(BigInt(Date.now())); await refetchSuperToken(); }, }); const handleClaimClick = async () => { // Refetch latest balance before claiming const latestBalance = await refetchSuperToken(); const balanceToUnwrap = latestBalance.data?.value ?? beneficiarySuperTokenBalance?.value ?? 0n; writeUnwrapSuperToken?.({ args: [balanceToUnwrap], }); }; const formatFlowPerMonth = (flowRate?: bigint | null) => { if (flowRate == null) return "--"; const monthlyFlow = Number(formatUnits(flowRate, streamTokenDecimals)) * SEC_TO_MONTH; if (!Number.isFinite(monthlyFlow)) return "--"; const value = monthlyFlow.toLocaleString(undefined, { maximumFractionDigits: 4, }); return poolToken?.symbol ? `${value} ${poolToken.symbol}` : value; }; const totalStreamedToBeneficiaryBn = getProposalTotalStreamedToBeneficiaryBn({ proposalFlowRateBn, streamedUntilSnapshotBn, lastSnapshotAtBn, nowMs: displayedNowMs, explorerTotalStreamedBn, isDisputedStreamingProposal, escrowBalanceSnapshotBn, escrowBalanceSnapshotAtMs, escrowSuperTokenBalanceValue: escrowSuperTokenBalance?.value, }); const totalStreamedDisplayValue = isStreamingType && poolToken ? Number(formatUnits(totalStreamedToBeneficiaryBn, poolToken.decimals)) : null; const totalStreamedRatePerSecond = ( isStreamingType && poolToken && !isDisputedStreamingProposal && proposalFlowRateBn > 0n ) ? Number(formatUnits(proposalFlowRateBn, poolToken.decimals)) : 0; const liveBeneficiarySuperTokenBalanceBn = getLiveBeneficiaryBalanceBn({ beneficiaryBalanceSnapshotBn, beneficiaryBalanceSnapshotAtMs, proposalFlowRateBn: currentFlowRateForDisplay ?? 0n, nowMs: displayedNowMs, }); const claimableBn = liveBeneficiarySuperTokenBalanceBn != null ? liveBeneficiarySuperTokenBalanceBn < totalStreamedToBeneficiaryBn ? liveBeneficiarySuperTokenBalanceBn : totalStreamedToBeneficiaryBn : totalStreamedToBeneficiaryBn; const claimableDisplayValue = isStreamingType && poolToken ? Number(formatUnits(claimableBn, poolToken.decimals)) : null; const claimableRatePerSecond = ( isStreamingType && poolToken && !isDisputedStreamingProposal && currentFlowRateForDisplay > 0n ) ? Number(formatUnits(currentFlowRateForDisplay, poolToken.decimals)) : 0; const liveBeneficiarySuperTokenBalanceForClaimBn = claimableBn; const minimumClaimableDisplayBn = streamTokenDecimals > 6 ? 10n ** BigInt(streamTokenDecimals - 6) : 1n; const hasMeaningfulClaimableAmount = liveBeneficiarySuperTokenBalanceForClaimBn >= minimumClaimableDisplayBn; const showUnwrapSuperTokenButton = isStreamingType && !isPureSuperfluidPoolToken && isBeneficiaryConnected && hasMeaningfulClaimableAmount; const disableUnwrapBtnConditions: ConditionObject[] = [ { condition: !isBeneficiaryConnected, message: "Connect with beneficiary to unwrap super token", }, { condition: !superTokenAddress, message: "Super token unavailable for this proposal", }, { condition: (beneficiarySuperTokenBalance?.value ?? 0n) <= 0n, message: "No super token balance to unwrap", }, ]; const { tooltipMessage: unwrapSuperTokenTooltipMessage, isConnected: isUnwrapConnected, missmatchUrl: isUnwrapWrongNetwork, } = useDisableButtons(disableUnwrapBtnConditions); if (!isProposalCoreReady) { return (
); } // const handleRefreshConviction = async (e: React.MouseEvent) => { // e.preventDefault(); // e.stopPropagation(); // setConvictionRefreshing(true); // await triggerConvictionRefetch?.(); // setConvictionRefreshing(false); // }; const status = proposalStatus; const canOpenDisputeModal = status === "active" || status === "disputed" || status === "rejected"; const streamingStatusLabel = isStreamingType ? status === "cancelled" ? "Cancelled" : status === "disputed" ? "Disputed" : status === "executed" ? "Closed" : currentFlowRateForDisplay > 0n ? "Streaming" : hasReachedThreshold ? "About to stream" : status === "active" ? "Active" : undefined : undefined; const hasThreshold = thresholdPct != null; const alreadyStreaming = (currentFlowRateForDisplay ?? 0n) > 0n; const proposalWillPass = isStreamingType && hasThreshold && willReachThreshold === true && hasReachedThreshold !== true && !alreadyStreaming; return ( <> {/* ================= DESKTOP ================= */}
{/* main section: proposal details + conviction progress + vote proposals & execute buttons */}
{/* Title - author - beneficairy - request - created - type */}

{metadata?.title}

{!isSignalingType && ( )}
{status !== "executed" && (
{prettyTimestamp(proposalData?.createdAt ?? 0)} {!isSignalingType && !isStreamingType && ( )}
)}
{/* Divider */} {/* Conviction Progress */} {proposalData.strategy?.isEnabled && currentConvictionPct != null && totalSupportPct != null && (
{(status === "active" || status === "disputed") && (

Progress

)}
)}
{!proposalData.strategy?.isEnabled && ( The pool is not enabled. )} {/* Action Buttons */} {status && status === "active" && (
Actions
{!isSignalingType && !isStreamingType && proposalData?.strategy != null && ( )}
)}
{/* Proposal Description */}

Proposal Description

{/* Right side: Status + view supporters + cancel button */}
{isStreamingType && (
Stream Info

Streaming

{formatFlowPerMonth(currentFlowRateForDisplay)}/m

Total

{showEscrow && (

Escrow

{resolvedStreamingEscrow ?
:

--

}
)} {!isPureSuperfluidPoolToken && (

Claimable

)} {isDisputedStreamingProposal && (

Accumulated

{superTokenAddress && poolToken?.symbol && ( )}
)}
{superfluidExplorerUrl != null && superfluidExplorerUrl !== "" && ( View on Superfluid Explorer )} {showSyncStreamButton && ( )} {showUnwrapSuperTokenButton && ( )}
)}
Status
{proposalWillPass && timeToPass != null && (

Before streaming starts:

)} {status === "executed" && (
  • Created

    {prettyTimestamp(proposalData?.createdAt)}

    {/*
    ; */}
  • {isStreamingType ? "Closed" : "Executed"}

    {prettyTimestamp(proposalData?.executedAt)}

    {!isSignalingType && !isStreamingType && (
    Funded:
    )} {isStreamingType && (
    Stream:{" "}

    {proposalFlowPerMonth != null ? `${roundToSignificant(proposalFlowPerMonth, 4)} ${poolToken?.symbol ?? ""}/mo` : "No active stream"}

    Total:

    )}
)}
{!isSignalingType && status === "cancelled" && ( <>

Cancelled

)} {status !== "executed" && status !== "cancelled" && status !== "disputed" && ( )} {status === "disputed" && ( )}
{canOpenDisputeModal && proposalData.strategy?.isEnabled && proposalDataForActions && ( )} {status !== "executed" && status !== "cancelled" && ( )}
{isProposerConnected && proposalStatus === "active" && (
{proposalDataForActions && ( <> )}
)} {totalSupportPct != null && (
)}
{/* ================= MOBILE ================= */}
{[ "Overview", "Description", "Status", ...(shouldShowSupportersTab ? ["Supporters"] : []), ].map((label, index) => ( ))}
{/* Overview */} {selectedTab === 0 && (
{/* Title - author - beneficairy - request - created - type */}

{metadata?.title}

{!isSignalingType && ( )}
{status !== "executed" && (
{prettyTimestamp(proposalData?.createdAt ?? 0)} {!isSignalingType && !isStreamingType && ( )}
)} {/*
{prettyTimestamp(proposalData?.createdAt ?? 0)} {!isSignalingType && ( )}
*/}
{/* Conviction Progress */} {proposalData.strategy.isEnabled && currentConvictionPct != null && totalSupportPct != null && (
{(status === "active" || status === "disputed") && (

Progress

)}
)}
{!proposalData.strategy.isEnabled && ( The pool is not enabled. )} {/* Action Buttons */} {status && status === "active" && (
Actions
{!isSignalingType && !isStreamingType && proposalData?.strategy != null && ( )}
)}
)} {/* Description */} {selectedTab === 1 && (

Proposal Description

)} {/* Status */} {selectedTab === 2 && ( <> {isStreamingType && (
Stream Info

Streaming

{formatFlowPerMonth(currentFlowRateForDisplay)}/m

Total

{showEscrow && (

StreamingEscrow

{resolvedStreamingEscrow ?
:

--

}
)} {!isPureSuperfluidPoolToken && (

Claimable

)} {isDisputedStreamingProposal && (

Accumulated

{superTokenAddress && poolToken?.symbol && ( )}
)}
{superfluidExplorerUrl != null && superfluidExplorerUrl !== "" && ( View on Superfluid Explorer )} {showSyncStreamButton && ( )} {showUnwrapSuperTokenButton && ( )}
)}
Status
{proposalWillPass && timeToPass != null && (

Before streaming starts:

)}
{status !== "executed" && status !== "cancelled" && status !== "disputed" && ( )} {status === "disputed" && ( )} {status === "executed" && (
  • Created

    {prettyTimestamp(proposalData?.createdAt)}

    {/*
    ; */}
  • {isStreamingType ? "Closed" : "Executed"}

    {prettyTimestamp(proposalData?.executedAt)}

    {!isSignalingType && !isStreamingType && (
    Funded:{" "}
    )} {isStreamingType && (
    Stream:{" "}

    {proposalFlowPerMonth != null ? `${roundToSignificant(proposalFlowPerMonth, 4)} ${poolToken?.symbol ?? ""}/mo` : "No active stream"}

    Total:

    )}
)}
{canOpenDisputeModal && proposalData.strategy.isEnabled && ( )}
{isProposerConnected && proposalStatus === "active" && (
{proposalDataForActions && ( <> )}
)} )} {shouldShowSupportersTab && selectedTab === 3 && totalSupportPct != null && ( )}
); } const ProposalSupportersTable = ({ supporters, totalActivePoints, totalVotingPowerUsedPct, openSupportersModal, setOpenSupportersModal, withModal = true, }: { supporters: ProposalSupporter[]; totalActivePoints: number | string | bigint | undefined; totalVotingPowerUsedPct: number; openSupportersModal: boolean; setOpenSupportersModal: (open: boolean) => void; withModal?: boolean; }) => { const columns: SupporterColumn[] = [ { header: "Member", render: (supporter: ProposalSupporter) => ( ), }, { header: ( Voting Power ), render: (supporter: ProposalSupporter) => ( {formatVotingPower( getSupporterPoolVotingPower(supporter, totalActivePoints), )} ), className: "text-right min-w-[9rem]", }, { header: ( Voting power used ), render: (supporter: ProposalSupporter) => ( {formatVotingPowerPct(getSupporterVotingPowerUsedPct(supporter))} ), className: "text-right min-w-[8rem]", }, ]; return (

Total Support:

{formatVotingPower(totalVotingPowerUsedPct)}

} className="border1 rounded-lg bg-neutral p-2" withModal={withModal} emptyMessage="No supporters yet." /> ); };