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 && (
-
-
+
)}