Skip to content

Commit 6bbd9eb

Browse files
y3v63nclaude
andauthored
Fix: show and claim distributed rewards for delegators who stake from a wallet (#126)
* Fix: show distributed rewards to delegators who stake from a wallet The dashboard reads the rewards warehouse address from the first delegation that has a split contract, but it only looked at token-vault delegations. A delegator who stakes directly from a wallet has none, so the address stayed undefined, the warehouse balance was never read, and already distributed rewards showed as zero. Fall back to an ERC20 delegation's split contract. Every split points at the same warehouse for a given token, so either source resolves the same address. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Let wallet delegators claim the warehouse balance, not just see it The previous commit made the distributed rewards balance visible to a delegator who stakes from a wallet. The claim path stayed shut. "Claim All Rewards" resolves the warehouse address from token-vault delegations only. For a wallet-only delegator that address is undefined, so the withdraw step is skipped while the cart still opens. The user sees the amount itemised, clicks, and gets an empty cart with no error. The per-delegation path does not cover it either. A fully distributed position has no rollup or split balance left, so the row reports zero rewards and its claim button is disabled. Apply the same fallback in the claim-all modal and pass the wallet delegations in from the overview. The withdraw leg is a per-user, per-token call, so either delegation kind resolves the same warehouse address. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Resolve the warehouse split in one shared helper The rule for picking a split contract to read the warehouse address from was written out at each call site, and the copies had drifted: two of them looked at token-vault delegations only, which is what hid the balance from wallet delegators in the first place. Move the rule into `resolveWarehouseSplit` and call it from the three sites that choose from a list. Each site keeps its own preference order, so behaviour is unchanged for a delegator who holds token-vault delegations. The helper also scans for the first entry that carries a split contract, rather than reading index zero. An entry without one no longer hides the warehouse from the entries behind it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 38b244d commit 6bbd9eb

6 files changed

Lines changed: 41 additions & 10 deletions

File tree

‎staking-dashboard/src/components/ATPStakingOverview/ATPStakingOverview.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ export const ATPStakingOverview = ({ atpData, walletBalance = 0n }: ATPStakingOv
183183
decimals={decimals}
184184
symbol={symbol}
185185
delegationBreakdown={delegationBreakdown}
186+
erc20DelegationBreakdown={erc20DelegationBreakdown}
186187
coinbaseBreakdown={coinbaseBreakdown}
187188
onClaimSuccess={() => {
188189
refetchAggregatedData()

‎staking-dashboard/src/components/ATPStakingOverview/ATPStakingOverviewClaimableRewards.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { TooltipIcon } from "@/components/Tooltip"
44
import { formatTokenAmount, formatTokenAmountFull } from "@/utils/atpFormatters"
55
import { ManageRewardsAddressesModal } from "@/components/RewardsManagement"
66
import { ClaimAllRewardsModal } from "@/components/ClaimAllRewardsModal"
7-
import type { DelegationBreakdown } from "@/hooks/atp/useAggregatedStakingData"
7+
import type { DelegationBreakdown, Erc20DelegationBreakdown } from "@/hooks/atp/useAggregatedStakingData"
88
import type { CoinbaseBreakdown } from "@/hooks/rewards/rewardsTypes"
99

1010
interface ATPStakingOverviewClaimableRewardsProps {
@@ -17,6 +17,7 @@ interface ATPStakingOverviewClaimableRewardsProps {
1717
decimals: number
1818
symbol: string
1919
delegationBreakdown?: DelegationBreakdown[]
20+
erc20DelegationBreakdown?: Erc20DelegationBreakdown[]
2021
coinbaseBreakdown?: CoinbaseBreakdown[]
2122
onClaimSuccess?: () => void
2223
}
@@ -25,7 +26,7 @@ interface ATPStakingOverviewClaimableRewardsProps {
2526
* Displays claimable delegation rewards and self-stake rewards
2627
*/
2728
export const ATPStakingOverviewClaimableRewards = forwardRef<HTMLDivElement, ATPStakingOverviewClaimableRewardsProps>(
28-
({ totalRewards, selfStakeRewards = 0n, pendingWarehouseWithdrawal = 0n, isRewardsClaimable = true, isExpanded, onToggle, decimals, symbol, delegationBreakdown = [], coinbaseBreakdown = [], onClaimSuccess }, ref) => {
29+
({ totalRewards, selfStakeRewards = 0n, pendingWarehouseWithdrawal = 0n, isRewardsClaimable = true, isExpanded, onToggle, decimals, symbol, delegationBreakdown = [], erc20DelegationBreakdown = [], coinbaseBreakdown = [], onClaimSuccess }, ref) => {
2930
const [isManageModalOpen, setIsManageModalOpen] = useState(false)
3031
const [isClaimAllModalOpen, setIsClaimAllModalOpen] = useState(false)
3132

@@ -193,6 +194,7 @@ export const ATPStakingOverviewClaimableRewards = forwardRef<HTMLDivElement, ATP
193194
isOpen={isClaimAllModalOpen}
194195
onClose={() => setIsClaimAllModalOpen(false)}
195196
delegations={delegationBreakdown}
197+
erc20Delegations={erc20DelegationBreakdown}
196198
coinbases={coinbaseBreakdown}
197199
pendingWarehouseWithdrawal={pendingWarehouseWithdrawal}
198200
onSuccess={onClaimSuccess}

‎staking-dashboard/src/components/ClaimAllDelegationRewardsButton/ClaimAllDelegationRewardsButton.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
buildDelegationClaimEntries,
99
buildWarehouseWithdrawEntry,
1010
getRecoveryDustThreshold,
11+
resolveWarehouseSplit,
1112
type ClaimCartEntry,
1213
} from "@/utils/claimCart"
1314
import { Icon } from "@/components/Icon"
@@ -63,8 +64,9 @@ export const ClaimAllDelegationRewardsButton = ({
6364
const { isRewardsClaimable } = useIsRewardsClaimable()
6465
const { addTransaction, checkTransactionInQueue, openCart, replaceTransactionByTx } = useTransactionCart()
6566

66-
// Warehouse is the same per-token across all delegations; resolve from the first one.
67-
const firstSplit = delegations[0]?.splitContract
67+
// Warehouse is the same per-token across all delegations; resolve from the
68+
// first one that has a split contract.
69+
const firstSplit = resolveWarehouseSplit(delegations)
6870
const { warehouseAddress } = useSplitsWarehouse(firstSplit)
6971

7072
// Filter to delegations that have *anything* claimable — canonical, stranded

‎staking-dashboard/src/components/ClaimAllRewardsModal/ClaimAllRewardsModal.tsx‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@ import {
1212
buildCoinbaseClaimEntry,
1313
buildWarehouseWithdrawEntry,
1414
getRecoveryDustThreshold,
15+
resolveWarehouseSplit,
1516
type ClaimCartEntry,
1617
} from "@/utils/claimCart"
17-
import type { DelegationBreakdown } from "@/hooks/atp/useAggregatedStakingData"
18+
import type { DelegationBreakdown, Erc20DelegationBreakdown } from "@/hooks/atp/useAggregatedStakingData"
1819
import type { CoinbaseBreakdown } from "@/hooks/rewards/rewardsTypes"
1920

2021
interface ClaimAllRewardsModalProps {
2122
isOpen: boolean
2223
onClose: () => void
2324
delegations: DelegationBreakdown[]
25+
erc20Delegations?: Erc20DelegationBreakdown[]
2426
coinbases: CoinbaseBreakdown[]
2527
pendingWarehouseWithdrawal?: bigint
2628
onSuccess?: () => void
@@ -37,6 +39,7 @@ export const ClaimAllRewardsModal = ({
3739
isOpen,
3840
onClose,
3941
delegations,
42+
erc20Delegations = [],
4043
coinbases,
4144
pendingWarehouseWithdrawal = 0n,
4245
onSuccess,
@@ -47,9 +50,12 @@ export const ClaimAllRewardsModal = ({
4750
const { addTransaction, openCart, replaceTransactionByTx } = useTransactionCart()
4851
const { showAlert } = useAlert()
4952

50-
// Resolve warehouse via the first delegation's split contract. All splits
51-
// funnel into the same SplitsWarehouse for a given token, so any split works.
52-
const firstSplit = delegations.find((d) => d.rewards > 0n)?.splitContract ?? delegations[0]?.splitContract
53+
// Prefer a split that has rewards, then fall back to any of the user's
54+
// splits, ATP or wallet. Without the wallet fallback the warehouse address
55+
// is undefined and the withdraw leg below is skipped, leaving an empty cart.
56+
const firstSplit =
57+
delegations.find((d) => d.rewards > 0n)?.splitContract
58+
?? resolveWarehouseSplit(delegations, erc20Delegations)
5359
const { warehouseAddress } = useSplitsWarehouse(firstSplit)
5460

5561
const handleAddAllToBatch = () => {

‎staking-dashboard/src/hooks/atp/useAggregatedStakingData.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ERC20Abi } from '@/contracts/abis/ERC20'
66
import { SplitAbi } from '@/contracts/abis/Split'
77
import { SplitWarehouseAbi } from '@/contracts/abis/SplitWarehouse'
88
import { calculateTotalUserShareFromSplitRewards } from '@/utils/rewardCalculations'
9+
import { resolveWarehouseSplit } from '@/utils/claimCart'
910
import { useFeeAssetTokenDetails } from '@/hooks/rollup'
1011
import { contracts, getRollupVersions, type RollupVersion } from '@/contracts'
1112
import type { Address } from 'viem'
@@ -519,8 +520,8 @@ export const useAggregatedStakingData = (): AggregatedStakingData => {
519520
},
520521
})
521522

522-
// Get warehouse address from first split contract (all splits use the same warehouse)
523-
const firstSplitContract = delegations[0]?.splitContract as Address | undefined
523+
// Resolve the warehouse from any of the user's splits, ATP or wallet.
524+
const firstSplitContract = resolveWarehouseSplit(delegations, erc20Delegations)
524525
const { data: warehouseAddress } = useReadContract({
525526
address: firstSplitContract,
526527
abi: SplitAbi,

‎staking-dashboard/src/utils/claimCart.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,25 @@ export function getRecoveryDustThreshold(decimals: number): bigint {
8080
return decimals >= 1 ? RECOVERY_DUST_THRESHOLD_NUMERATOR * 10n ** BigInt(decimals - 1) : 0n
8181
}
8282

83+
/**
84+
* Pick a split contract to resolve the SplitsWarehouse address from.
85+
*
86+
* Every split funnels into the same warehouse for a given token, so any one of
87+
* the user's splits answers the question. The lists are tried in order, which
88+
* matters because a delegator who stakes only from a wallet has no ATP
89+
* delegations: reading just the first list left the address undefined and
90+
* silently disabled every warehouse read and withdraw for them.
91+
*/
92+
export function resolveWarehouseSplit(
93+
...delegationLists: Array<ReadonlyArray<{ splitContract?: string }> | undefined>
94+
): Address | undefined {
95+
for (const list of delegationLists) {
96+
const found = list?.find((d) => d?.splitContract)?.splitContract
97+
if (found) return found as Address
98+
}
99+
return undefined
100+
}
101+
83102
export interface DelegationClaimResult {
84103
entries: ClaimCartEntry[]
85104
/** `stepGroupIdentifier` of the delegation's distribute step. Pass to

0 commit comments

Comments
 (0)