From 5805df05dcf62d1217946133232edebe2d200941 Mon Sep 17 00:00:00 2001 From: Sebastian <115311276+Roaring30s@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:48:12 -0500 Subject: [PATCH 1/3] Fix: Add Warnings to Delegation Actions (Issue #194) (#524) * feat: add useDelegationReview hook * feat: add delegation warnings --------- Co-authored-by: ECWireless <40322776+ECWireless@users.noreply.github.com> --- apollo/subgraph.ts | 5 +- components/DelegatingView/index.tsx | 40 +++++++--- components/DelegatingWidget/Footer.tsx | 31 ++++++-- components/DelegationReview/index.tsx | 34 +++++++++ components/Redelegate/index.tsx | 47 ++++++++++-- .../RedelegateFromUndelegated/index.tsx | 50 +++++++++++-- components/StakeTransactions/index.tsx | 16 +++- hooks/index.tsx | 1 + hooks/useDelegationReview.tsx | 73 +++++++++++++++++++ queries/account.graphql | 3 + 10 files changed, 264 insertions(+), 36 deletions(-) create mode 100644 components/DelegationReview/index.tsx create mode 100644 hooks/useDelegationReview.tsx diff --git a/apollo/subgraph.ts b/apollo/subgraph.ts index 40973d73..6487bc83 100644 --- a/apollo/subgraph.ts +++ b/apollo/subgraph.ts @@ -9603,7 +9603,7 @@ export type AccountQueryVariables = Exact<{ }>; -export type AccountQuery = { __typename: 'Query', delegator?: { __typename: 'Delegator', id: string, bondedAmount: string, principal: string, unbonded: string, withdrawnFees: string, startRound: string, lastClaimRound?: { __typename: 'Round', id: string } | null, unbondingLocks?: Array<{ __typename: 'UnbondingLock', id: string, amount: string, unbondingLockId: number, withdrawRound: string, delegate: { __typename: 'Transcoder', id: string } }> | null, delegate?: { __typename: 'Transcoder', id: string, active: boolean, status: TranscoderStatus, totalStake: string } | null } | null, transcoder?: { __typename: 'Transcoder', id: string, active: boolean, feeShare: string, rewardCut: string, status: TranscoderStatus, totalStake: string, totalVolumeETH: string, activationTimestamp: number, activationRound: string, deactivationRound: string, thirtyDayVolumeETH: string, ninetyDayVolumeETH: string, lastRewardRound?: { __typename: 'Round', id: string } | null, pools?: Array<{ __typename: 'Pool', rewardTokens?: string | null }> | null, delegators?: Array<{ __typename: 'Delegator', id: string }> | null } | null, gateway?: { __typename: 'Broadcaster', id: string, deposit: string, reserve: string, totalVolumeETH: string, ninetyDayVolumeETH: string, firstActiveDay: number, lastActiveDay: number } | null, protocol?: { __typename: 'Protocol', id: string, totalSupply: string, totalActiveStake: string, participationRate: string, inflation: string, inflationChange: string, lptPriceEth: string, roundLength: string, currentRound: { __typename: 'Round', id: string } } | null }; +export type AccountQuery = { __typename: 'Query', delegator?: { __typename: 'Delegator', id: string, bondedAmount: string, principal: string, unbonded: string, withdrawnFees: string, startRound: string, lastClaimRound?: { __typename: 'Round', id: string } | null, unbondingLocks?: Array<{ __typename: 'UnbondingLock', id: string, amount: string, unbondingLockId: number, withdrawRound: string, delegate: { __typename: 'Transcoder', id: string } }> | null, delegate?: { __typename: 'Transcoder', id: string, active: boolean, status: TranscoderStatus, totalStake: string, lastRewardRound?: { __typename: 'Round', id: string } | null } | null } | null, transcoder?: { __typename: 'Transcoder', id: string, active: boolean, feeShare: string, rewardCut: string, status: TranscoderStatus, totalStake: string, totalVolumeETH: string, activationTimestamp: number, activationRound: string, deactivationRound: string, thirtyDayVolumeETH: string, ninetyDayVolumeETH: string, lastRewardRound?: { __typename: 'Round', id: string } | null, pools?: Array<{ __typename: 'Pool', rewardTokens?: string | null }> | null, delegators?: Array<{ __typename: 'Delegator', id: string }> | null } | null, protocol?: { __typename: 'Protocol', id: string, totalSupply: string, totalActiveStake: string, participationRate: string, inflation: string, inflationChange: string, lptPriceEth: string, roundLength: string, currentRound: { __typename: 'Round', id: string } } | null }; export type AccountInactiveQueryVariables = Exact<{ id: Scalars['ID']; @@ -9780,6 +9780,9 @@ export const AccountDocument = gql` active status totalStake + lastRewardRound { + id + } } } transcoder(id: $account) { diff --git a/components/DelegatingView/index.tsx b/components/DelegatingView/index.tsx index 0f32a1b9..9d72b55c 100644 --- a/components/DelegatingView/index.tsx +++ b/components/DelegatingView/index.tsx @@ -7,6 +7,7 @@ import { QuestionMarkCircledIcon } from "@modulz/radix-icons"; import { AccountQueryResult, OrchestratorsSortedQueryResult } from "apollo"; import { useAccountAddress, + useDelegationReview, useEnsData, usePendingFeesAndStakeData, } from "hooks"; @@ -20,6 +21,7 @@ import Masonry from "react-masonry-css"; import { Address } from "viem"; import { useSimulateContract, useWriteContract } from "wagmi"; +import DelegationReview from "../DelegationReview"; import StakeTransactions from "../StakeTransactions"; const breakpointColumnsObj = { @@ -49,6 +51,12 @@ const Index = ({ delegator, transcoders, protocol, currentRound }: Props) => { const pendingFeesAndStake = usePendingFeesAndStakeData(delegator?.id); + const { delegationWarning } = useDelegationReview({ + delegator, + currentRound, + action: "withdrawFees", + }); + const recipient = delegator?.id as Address | undefined; const amount = pendingFeesAndStake?.pendingFees ?? "0"; @@ -357,18 +365,26 @@ const Index = ({ delegator, transcoders, protocol, currentRound }: Props) => { {isMyAccount && !withdrawButtonDisabled && delegator?.id && ( - + <> + + {delegationWarning && ( + + )} + )} } diff --git a/components/DelegatingWidget/Footer.tsx b/components/DelegatingWidget/Footer.tsx index 7b542a4d..dd281e5a 100644 --- a/components/DelegatingWidget/Footer.tsx +++ b/components/DelegatingWidget/Footer.tsx @@ -11,11 +11,13 @@ import { StakingAction, useAccountAddress, useAccountBalanceData, + useDelegationReview, usePendingFeesAndStakeData, } from "hooks"; import { useMemo } from "react"; import { parseEther } from "viem"; +import DelegationReview from "../DelegationReview"; import Delegate from "./Delegate"; import Footnote from "./Footnote"; import Undelegate from "./Undelegate"; @@ -69,15 +71,22 @@ const Footer = ({ ); const accountBalance = useAccountBalanceData(accountAddress); - const tokenBalance = useMemo(() => accountBalance?.balance, [accountBalance]); - const transferAllowance = useMemo( - () => accountBalance?.allowance, - [accountBalance] - ); + const tokenBalance = accountBalance?.balance; + const transferAllowance = accountBalance?.allowance; const delegatorStatus = useMemo( () => getDelegatorStatus(delegator, currentRound), [currentRound, delegator] ); + const { delegationWarning } = useDelegationReview({ + delegator, + currentRound, + action: isTransferStake + ? "moveStake" + : action === "delegate" + ? "delegate" + : "undelegate", + targetOrchestrator: action === "delegate" ? transcoder : undefined, + }); const stakeWei = useMemo( () => delegatorPendingStakeAndFees?.pendingStake @@ -175,6 +184,12 @@ const Footer = ({ currDelegateNewPosNext: currDelegateNewPosNext, }} /> + {delegationWarning && (isTransferStake || amount) && ( + + )} ); } @@ -194,6 +209,12 @@ const Footer = ({ sufficientStake, isMyTranscoder )} + {delegationWarning && amount && ( + + )} ); }; diff --git a/components/DelegationReview/index.tsx b/components/DelegationReview/index.tsx new file mode 100644 index 00000000..4a632579 --- /dev/null +++ b/components/DelegationReview/index.tsx @@ -0,0 +1,34 @@ +import { Box, Flex, Text } from "@livepeer/design-system"; +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; + +const DelegationReview = ({ + warning, + css, +}: { + warning?: string | null; + css?: object; +}) => { + if (!warning) return null; + + return ( + + + + {warning} + + + ); +}; + +export default DelegationReview; diff --git a/components/Redelegate/index.tsx b/components/Redelegate/index.tsx index 40945c7f..e63723bb 100644 --- a/components/Redelegate/index.tsx +++ b/components/Redelegate/index.tsx @@ -1,10 +1,24 @@ +import { ExplorerTooltip } from "@components/ExplorerTooltip"; import { bondingManager } from "@lib/api/abis/main/BondingManager"; -import { Button } from "@livepeer/design-system"; +import { Box, Button, Flex } from "@livepeer/design-system"; +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; import { useBondingManagerAddress } from "hooks/useContracts"; +import { useDelegationReview } from "hooks/useDelegationReview"; import { useHandleTransaction } from "hooks/useHandleTransaction"; import { useSimulateContract, useWriteContract } from "wagmi"; -const Index = ({ unbondingLockId, newPosPrev, newPosNext }) => { +const Index = ({ + unbondingLockId, + newPosPrev, + newPosNext, + delegator, + currentRound, +}) => { + const { delegationWarning } = useDelegationReview({ + delegator, + currentRound, + action: "redelegate", + }); const { data: bondingManagerAddress } = useBondingManagerAddress(); const { data: config } = useSimulateContract({ @@ -23,13 +37,32 @@ const Index = ({ unbondingLockId, newPosPrev, newPosNext }) => { }); return ( - <> + + {delegationWarning && ( + + + + + + )} - + ); }; diff --git a/components/RedelegateFromUndelegated/index.tsx b/components/RedelegateFromUndelegated/index.tsx index 66d2191d..cbf09bb2 100644 --- a/components/RedelegateFromUndelegated/index.tsx +++ b/components/RedelegateFromUndelegated/index.tsx @@ -1,11 +1,27 @@ import { bondingManager } from "@lib/api/abis/main/BondingManager"; -import { Button } from "@livepeer/design-system"; +import { Box, Button, Flex } from "@livepeer/design-system"; +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; import { useAccountAddress } from "hooks"; import { useBondingManagerAddress } from "hooks/useContracts"; +import { useDelegationReview } from "hooks/useDelegationReview"; import { useHandleTransaction } from "hooks/useHandleTransaction"; import { useSimulateContract, useWriteContract } from "wagmi"; -const Index = ({ unbondingLockId, delegate, newPosPrev, newPosNext }) => { +import { ExplorerTooltip } from "../ExplorerTooltip"; + +const Index = ({ + unbondingLockId, + delegate, + newPosPrev, + newPosNext, + delegator, + currentRound, +}) => { + const { delegationWarning } = useDelegationReview({ + delegator, + currentRound, + action: "redelegateFromUndelegated", + }); const accountAddress = useAccountAddress(); const { data: bondingManagerAddress } = useBondingManagerAddress(); @@ -38,13 +54,20 @@ const Index = ({ unbondingLockId, delegate, newPosPrev, newPosNext }) => { } return ( - <> + - + {delegationWarning && ( + + + + + + )} + ); }; diff --git a/components/StakeTransactions/index.tsx b/components/StakeTransactions/index.tsx index 85661fca..1d737c7c 100644 --- a/components/StakeTransactions/index.tsx +++ b/components/StakeTransactions/index.tsx @@ -111,6 +111,8 @@ const Index = ({ delegator, transcoders, currentRound, isMyAccount }) => { unbondingLockId={lock.unbondingLockId} newPosPrev={newPosPrev} newPosNext={newPosNext} + delegator={delegator} + currentRound={currentRound} /> ) : ( { delegate={lock.delegate.id} newPosPrev={newPosPrev} newPosNext={newPosNext} + delegator={delegator} + currentRound={currentRound} /> )} )} { unbondingLockId={lock.unbondingLockId} newPosPrev={newPosPrev} newPosNext={newPosNext} + delegator={delegator} + currentRound={currentRound} /> ) : ( { delegate={lock.delegate.id} newPosPrev={newPosPrev} newPosNext={newPosNext} + delegator={delegator} + currentRound={currentRound} /> )} @@ -235,11 +243,11 @@ const Index = ({ delegator, transcoders, currentRound, isMyAccount }) => { )} ["delegator"]; +type CurrentRound = NonNullable< + NonNullable["protocol"] +>["currentRound"]; + +type DelegationAction = + | "delegate" + | "undelegate" + | "moveStake" + | "redelegate" + | "redelegateFromUndelegated" + | "withdrawFees"; + +export const useDelegationReview = ({ + delegator, + currentRound, + action, + targetOrchestrator, +}: { + delegator?: Delegator | null; + currentRound?: CurrentRound | null; + action: DelegationAction; + targetOrchestrator?: { lastRewardRound?: { id: string } | null } | null; +}) => { + const delegationWarning = useMemo(() => { + // Safety check + if (!delegator || !currentRound) { + return null; + } + + const isDelegated = + delegator.bondedAmount && delegator.bondedAmount !== "0"; + + // Get orchestrator's last reward round + // Use targetOrchestrator if provided (for moving stake), otherwise use current delegate + const orchestratorToCheck = targetOrchestrator || delegator.delegate; + const orchestratorLastRewardRound = orchestratorToCheck?.lastRewardRound?.id + ? parseInt(orchestratorToCheck.lastRewardRound.id, 10) + : 0; + + const currentRoundNum = currentRound.id ? parseInt(currentRound.id, 10) : 0; + + // Per LIP-36: Warn if orchestrator hasn't called reward() yet this round + // This affects bond(), unbond(), rebond(), rebondFromUnbonded(), and withdrawFees() + // Only warn if we have valid delegate data to avoid false positives + const orchestratorHasntCalledReward = + isDelegated && + orchestratorToCheck?.lastRewardRound?.id && + orchestratorLastRewardRound < currentRoundNum; + + if (!orchestratorHasntCalledReward) { + return null; + } + + // Action-specific warning messages + switch (action) { + case "redelegate": + return "Rebonding will forfeit rewards and fees for the current round on your entire stake."; + case "moveStake": + case "redelegateFromUndelegated": + return "Moving stake to a different orchestrator will forfeit rewards and fees for the current round."; + default: + return "Performing this action before your orchestrator calls reward will forfeit rewards and fees for the current round."; + } + }, [delegator, currentRound, action, targetOrchestrator]); + + return { + delegationWarning, + }; +}; diff --git a/queries/account.graphql b/queries/account.graphql index 010b70fb..74ba1cc3 100644 --- a/queries/account.graphql +++ b/queries/account.graphql @@ -23,6 +23,9 @@ query account($account: ID!) { active status totalStake + lastRewardRound { + id + } } } transcoder(id: $account) { From d1dd19b35b56b5189cfde5b11955923e2c2ad168 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Tue, 31 Mar 2026 08:00:37 -0600 Subject: [PATCH 2/3] fix: bonded amount at 0 suppression --- apollo/subgraph.ts | 5 +- .../RedelegateFromUndelegated/index.tsx | 5 +- components/StakeTransactions/index.tsx | 4 +- hooks/useDelegationReview.test.ts | 37 ++++++ hooks/useDelegationReview.tsx | 105 +++++++++++------- queries/account.graphql | 3 + 6 files changed, 112 insertions(+), 47 deletions(-) create mode 100644 hooks/useDelegationReview.test.ts diff --git a/apollo/subgraph.ts b/apollo/subgraph.ts index 6487bc83..355dcff2 100644 --- a/apollo/subgraph.ts +++ b/apollo/subgraph.ts @@ -9603,7 +9603,7 @@ export type AccountQueryVariables = Exact<{ }>; -export type AccountQuery = { __typename: 'Query', delegator?: { __typename: 'Delegator', id: string, bondedAmount: string, principal: string, unbonded: string, withdrawnFees: string, startRound: string, lastClaimRound?: { __typename: 'Round', id: string } | null, unbondingLocks?: Array<{ __typename: 'UnbondingLock', id: string, amount: string, unbondingLockId: number, withdrawRound: string, delegate: { __typename: 'Transcoder', id: string } }> | null, delegate?: { __typename: 'Transcoder', id: string, active: boolean, status: TranscoderStatus, totalStake: string, lastRewardRound?: { __typename: 'Round', id: string } | null } | null } | null, transcoder?: { __typename: 'Transcoder', id: string, active: boolean, feeShare: string, rewardCut: string, status: TranscoderStatus, totalStake: string, totalVolumeETH: string, activationTimestamp: number, activationRound: string, deactivationRound: string, thirtyDayVolumeETH: string, ninetyDayVolumeETH: string, lastRewardRound?: { __typename: 'Round', id: string } | null, pools?: Array<{ __typename: 'Pool', rewardTokens?: string | null }> | null, delegators?: Array<{ __typename: 'Delegator', id: string }> | null } | null, protocol?: { __typename: 'Protocol', id: string, totalSupply: string, totalActiveStake: string, participationRate: string, inflation: string, inflationChange: string, lptPriceEth: string, roundLength: string, currentRound: { __typename: 'Round', id: string } } | null }; +export type AccountQuery = { __typename: 'Query', delegator?: { __typename: 'Delegator', id: string, bondedAmount: string, principal: string, unbonded: string, withdrawnFees: string, startRound: string, lastClaimRound?: { __typename: 'Round', id: string } | null, unbondingLocks?: Array<{ __typename: 'UnbondingLock', id: string, amount: string, unbondingLockId: number, withdrawRound: string, delegate: { __typename: 'Transcoder', id: string, lastRewardRound?: { __typename: 'Round', id: string } | null } }> | null, delegate?: { __typename: 'Transcoder', id: string, active: boolean, status: TranscoderStatus, totalStake: string, lastRewardRound?: { __typename: 'Round', id: string } | null } | null } | null, transcoder?: { __typename: 'Transcoder', id: string, active: boolean, feeShare: string, rewardCut: string, status: TranscoderStatus, totalStake: string, totalVolumeETH: string, activationTimestamp: number, activationRound: string, deactivationRound: string, thirtyDayVolumeETH: string, ninetyDayVolumeETH: string, lastRewardRound?: { __typename: 'Round', id: string } | null, pools?: Array<{ __typename: 'Pool', rewardTokens?: string | null }> | null, delegators?: Array<{ __typename: 'Delegator', id: string }> | null } | null, gateway?: { __typename: 'Broadcaster', id: string, deposit: string, reserve: string, totalVolumeETH: string, ninetyDayVolumeETH: string, firstActiveDay: number, lastActiveDay: number } | null, protocol?: { __typename: 'Protocol', id: string, totalSupply: string, totalActiveStake: string, participationRate: string, inflation: string, inflationChange: string, lptPriceEth: string, roundLength: string, currentRound: { __typename: 'Round', id: string } } | null }; export type AccountInactiveQueryVariables = Exact<{ id: Scalars['ID']; @@ -9773,6 +9773,9 @@ export const AccountDocument = gql` withdrawRound delegate { id + lastRewardRound { + id + } } } delegate { diff --git a/components/RedelegateFromUndelegated/index.tsx b/components/RedelegateFromUndelegated/index.tsx index cbf09bb2..a9d07670 100644 --- a/components/RedelegateFromUndelegated/index.tsx +++ b/components/RedelegateFromUndelegated/index.tsx @@ -21,6 +21,7 @@ const Index = ({ delegator, currentRound, action: "redelegateFromUndelegated", + targetOrchestrator: delegate, }); const accountAddress = useAccountAddress(); @@ -30,7 +31,7 @@ const Index = ({ address: bondingManagerAddress, abi: bondingManager, functionName: "rebondFromUnbondedWithHint", - args: [delegate, unbondingLockId, newPosPrev, newPosNext], + args: [delegate.id, unbondingLockId, newPosPrev, newPosNext], }); const { data, isPending, writeContract, error, isSuccess } = useWriteContract(); @@ -42,7 +43,7 @@ const Index = ({ isPending, isSuccess, { - delegate, + delegate: delegate.id, unbondingLockId, newPosPrev, newPosNext, diff --git a/components/StakeTransactions/index.tsx b/components/StakeTransactions/index.tsx index 1d737c7c..784ade39 100644 --- a/components/StakeTransactions/index.tsx +++ b/components/StakeTransactions/index.tsx @@ -117,7 +117,7 @@ const Index = ({ delegator, transcoders, currentRound, isMyAccount }) => { ) : ( { ) : ( { + it("warns for rebonding from an unbonded lock when that delegate has not called reward", () => { + const warning = getDelegationWarning({ + delegator: { + bondedAmount: "0", + delegate: null, + } as never, + currentRound: { id: "101" } as never, + action: "redelegateFromUndelegated", + targetOrchestrator: { + lastRewardRound: { id: "100" }, + }, + }); + + expect(warning).toBe( + "Moving stake to a different orchestrator will forfeit rewards and fees for the current round." + ); + }); + + it("does not warn once the unbonding-lock delegate has already called reward this round", () => { + const warning = getDelegationWarning({ + delegator: { + bondedAmount: "0", + delegate: null, + } as never, + currentRound: { id: "101" } as never, + action: "redelegateFromUndelegated", + targetOrchestrator: { + lastRewardRound: { id: "101" }, + }, + }); + + expect(warning).toBeNull(); + }); +}); diff --git a/hooks/useDelegationReview.tsx b/hooks/useDelegationReview.tsx index f46a8216..cd1c99a1 100644 --- a/hooks/useDelegationReview.tsx +++ b/hooks/useDelegationReview.tsx @@ -14,58 +14,79 @@ type DelegationAction = | "redelegateFromUndelegated" | "withdrawFees"; -export const useDelegationReview = ({ +type ReviewOrchestrator = { lastRewardRound?: { id: string } | null } | null; + +type DelegationReviewParams = { + delegator?: Delegator | null; + currentRound?: CurrentRound | null; + action: DelegationAction; + targetOrchestrator?: ReviewOrchestrator; +}; + +export const getDelegationWarning = ({ delegator, currentRound, action, targetOrchestrator, -}: { - delegator?: Delegator | null; - currentRound?: CurrentRound | null; - action: DelegationAction; - targetOrchestrator?: { lastRewardRound?: { id: string } | null } | null; -}) => { - const delegationWarning = useMemo(() => { - // Safety check - if (!delegator || !currentRound) { - return null; - } +}: DelegationReviewParams) => { + if (!delegator || !currentRound) { + return null; + } - const isDelegated = - delegator.bondedAmount && delegator.bondedAmount !== "0"; + const isDelegated = delegator.bondedAmount && delegator.bondedAmount !== "0"; + const hasStakeAtRisk = + action === "redelegateFromUndelegated" + ? Boolean(targetOrchestrator) + : Boolean(isDelegated); - // Get orchestrator's last reward round - // Use targetOrchestrator if provided (for moving stake), otherwise use current delegate - const orchestratorToCheck = targetOrchestrator || delegator.delegate; - const orchestratorLastRewardRound = orchestratorToCheck?.lastRewardRound?.id - ? parseInt(orchestratorToCheck.lastRewardRound.id, 10) - : 0; + // Use an explicit orchestrator when the action concerns stake outside the + // account's current delegate, such as rebonding from an unbonded lock. + const orchestratorToCheck = targetOrchestrator || delegator.delegate; + const orchestratorLastRewardRoundId = + orchestratorToCheck?.lastRewardRound?.id; + const orchestratorLastRewardRound = orchestratorLastRewardRoundId + ? parseInt(orchestratorLastRewardRoundId, 10) + : 0; + const currentRoundNum = currentRound.id ? parseInt(currentRound.id, 10) : 0; - const currentRoundNum = currentRound.id ? parseInt(currentRound.id, 10) : 0; + // Per LIP-36: Warn if the relevant orchestrator hasn't called reward() yet + // this round and the action can still affect stake that was delegated to it. + const orchestratorHasntCalledReward = + hasStakeAtRisk && + Boolean(orchestratorLastRewardRoundId) && + orchestratorLastRewardRound < currentRoundNum; - // Per LIP-36: Warn if orchestrator hasn't called reward() yet this round - // This affects bond(), unbond(), rebond(), rebondFromUnbonded(), and withdrawFees() - // Only warn if we have valid delegate data to avoid false positives - const orchestratorHasntCalledReward = - isDelegated && - orchestratorToCheck?.lastRewardRound?.id && - orchestratorLastRewardRound < currentRoundNum; + if (!orchestratorHasntCalledReward) { + return null; + } - if (!orchestratorHasntCalledReward) { - return null; - } + switch (action) { + case "redelegate": + return "Rebonding will forfeit rewards and fees for the current round on your entire stake."; + case "moveStake": + case "redelegateFromUndelegated": + return "Moving stake to a different orchestrator will forfeit rewards and fees for the current round."; + default: + return "Performing this action before your orchestrator calls reward will forfeit rewards and fees for the current round."; + } +}; - // Action-specific warning messages - switch (action) { - case "redelegate": - return "Rebonding will forfeit rewards and fees for the current round on your entire stake."; - case "moveStake": - case "redelegateFromUndelegated": - return "Moving stake to a different orchestrator will forfeit rewards and fees for the current round."; - default: - return "Performing this action before your orchestrator calls reward will forfeit rewards and fees for the current round."; - } - }, [delegator, currentRound, action, targetOrchestrator]); +export const useDelegationReview = ({ + delegator, + currentRound, + action, + targetOrchestrator, +}: DelegationReviewParams) => { + const delegationWarning = useMemo( + () => + getDelegationWarning({ + delegator, + currentRound, + action, + targetOrchestrator, + }), + [delegator, currentRound, action, targetOrchestrator] + ); return { delegationWarning, diff --git a/queries/account.graphql b/queries/account.graphql index 74ba1cc3..1c85a1f3 100644 --- a/queries/account.graphql +++ b/queries/account.graphql @@ -16,6 +16,9 @@ query account($account: ID!) { withdrawRound delegate { id + lastRewardRound { + id + } } } delegate { From 8308a60c844836cb97463283eb12334a5d5e4327 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Wed, 1 Apr 2026 15:40:47 -0600 Subject: [PATCH 3/3] fix: address copilot suggestions --- components/DelegatingWidget/Footer.tsx | 15 +++++++++------ components/Redelegate/index.tsx | 13 ++++++++++--- components/RedelegateFromUndelegated/index.tsx | 12 +++++++++--- hooks/useDelegationReview.test.ts | 15 +++++++++++++++ hooks/useDelegationReview.tsx | 2 +- 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/components/DelegatingWidget/Footer.tsx b/components/DelegatingWidget/Footer.tsx index dd281e5a..2d2da11e 100644 --- a/components/DelegatingWidget/Footer.tsx +++ b/components/DelegatingWidget/Footer.tsx @@ -77,15 +77,18 @@ const Footer = ({ () => getDelegatorStatus(delegator, currentRound), [currentRound, delegator] ); + const delegationReviewAction = isTransferStake + ? "moveStake" + : action === "delegate" + ? "delegate" + : "undelegate"; + const { delegationWarning } = useDelegationReview({ delegator, currentRound, - action: isTransferStake - ? "moveStake" - : action === "delegate" - ? "delegate" - : "undelegate", - targetOrchestrator: action === "delegate" ? transcoder : undefined, + action: delegationReviewAction, + targetOrchestrator: + delegationReviewAction === "undelegate" ? undefined : transcoder, }); const stakeWei = useMemo( () => diff --git a/components/Redelegate/index.tsx b/components/Redelegate/index.tsx index e63723bb..1f9bf71f 100644 --- a/components/Redelegate/index.tsx +++ b/components/Redelegate/index.tsx @@ -1,6 +1,6 @@ import { ExplorerTooltip } from "@components/ExplorerTooltip"; import { bondingManager } from "@lib/api/abis/main/BondingManager"; -import { Box, Button, Flex } from "@livepeer/design-system"; +import { Button, Flex, IconButton } from "@livepeer/design-system"; import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; import { useBondingManagerAddress } from "hooks/useContracts"; import { useDelegationReview } from "hooks/useDelegationReview"; @@ -49,14 +49,21 @@ const Index = ({ > {delegationWarning && ( - - + )}