From 892557dc799ad44cf4cd2ae1779939f86fab7ac4 Mon Sep 17 00:00:00 2001 From: Robert Brada Date: Fri, 24 Apr 2026 16:50:59 +0200 Subject: [PATCH 1/4] fix: query correct rollup for legacy delegation withdrawals Delegations on a prior rollup version were stuck showing "IN QUEUE" with no Finalize Withdraw button: useAttesterView hard-coded the current rollup, so getAttesterView returned status=NONE for attesters that live on v0. The button, when it did enable, also targeted the wrong rollup. Thread the delegation's own rollupAddress (already stored on stakedWithProvider) through the API response and into useAttesterView, useSequencerStatus, and useFinalizeWithdraw. Hooks fall back to the current rollup when no address is passed, so non-delegation callers are unaffected. --- atp-indexer/src/api/handlers/atp/details.ts | 1 + .../ATPDetailsModal/ATPDetailsDelegationItem.tsx | 4 +++- .../components/ATPDetailsModal/WithdrawalActions.tsx | 4 +++- staking-dashboard/src/hooks/atp/useATPDetails.ts | 1 + staking-dashboard/src/hooks/rollup/useAttesterView.ts | 11 +++++++++-- .../src/hooks/rollup/useFinalizeWithdraw.ts | 4 ++-- .../src/hooks/rollup/useSequencerStatus.ts | 7 +++++-- 7 files changed, 24 insertions(+), 8 deletions(-) diff --git a/atp-indexer/src/api/handlers/atp/details.ts b/atp-indexer/src/api/handlers/atp/details.ts index 3100df41f..af42f1c77 100644 --- a/atp-indexer/src/api/handlers/atp/details.ts +++ b/atp-indexer/src/api/handlers/atp/details.ts @@ -63,6 +63,7 @@ function formatDelegations( providerName: metadata?.providerName || `Provider ${providerId}`, providerLogo: metadata?.providerLogoUrl || '', operatorAddress: checksumAddress(op.attesterAddress), + rollupAddress: checksumAddress(op.rollupAddress), stakedAmount: activationThreshold, totalSlashed: totalSlashed.toString(), splitContract: checksumAddress(op.splitContractAddress), diff --git a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDelegationItem.tsx b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDelegationItem.tsx index 64e5f9f02..20461be6f 100644 --- a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDelegationItem.tsx +++ b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDelegationItem.tsx @@ -60,7 +60,8 @@ export const ATPDetailsDelegationItem = ({ const { getSplitStatus, claimAllHook } = useClaimAllContext() const { isRewardsClaimable } = useIsRewardsClaimable() - const { status, statusLabel, isLoading: isLoadingStatus, canFinalize, actualUnlockTime, refetch: refetchStatus } = useSequencerStatus(delegation.operatorAddress as Address) + const delegationRollupAddress = delegation.rollupAddress as Address + const { status, statusLabel, isLoading: isLoadingStatus, canFinalize, actualUnlockTime, refetch: refetchStatus } = useSequencerStatus(delegation.operatorAddress as Address, delegationRollupAddress) const { withdrawalDelayDays } = useGovernanceConfig() const { @@ -487,6 +488,7 @@ export const ATPDetailsDelegationItem = ({ stakerAddress={stakerAddress} attesterAddress={delegation.operatorAddress as Address} rollupVersion={rollupVersion} + rollupAddress={delegationRollupAddress} status={status} canFinalize={canFinalize} actualUnlockTime={actualUnlockTime} diff --git a/staking-dashboard/src/components/ATPDetailsModal/WithdrawalActions.tsx b/staking-dashboard/src/components/ATPDetailsModal/WithdrawalActions.tsx index 51e1a2b9f..e354af0f1 100644 --- a/staking-dashboard/src/components/ATPDetailsModal/WithdrawalActions.tsx +++ b/staking-dashboard/src/components/ATPDetailsModal/WithdrawalActions.tsx @@ -75,6 +75,7 @@ interface WithdrawalActionsProps { stakerAddress: Address; attesterAddress: Address; rollupVersion: bigint; + rollupAddress?: Address; status: number | undefined; canFinalize: boolean; actualUnlockTime?: bigint; @@ -94,6 +95,7 @@ export const WithdrawalActions = ({ stakerAddress, attesterAddress, rollupVersion, + rollupAddress, status, canFinalize, actualUnlockTime, @@ -186,7 +188,7 @@ export const WithdrawalActions = ({ const handleFinalizeWithdraw = async () => { try { - await finalizeWithdraw(attesterAddress); + await finalizeWithdraw(attesterAddress, rollupAddress); } catch (error) { console.error("Failed to finalize withdraw:", error); } diff --git a/staking-dashboard/src/hooks/atp/useATPDetails.ts b/staking-dashboard/src/hooks/atp/useATPDetails.ts index 9cef7efcb..b7b03fdaa 100644 --- a/staking-dashboard/src/hooks/atp/useATPDetails.ts +++ b/staking-dashboard/src/hooks/atp/useATPDetails.ts @@ -21,6 +21,7 @@ export interface Delegation { providerName?: string providerLogo?: string operatorAddress: string + rollupAddress: string splitContract: string providerTakeRate: number providerRewardsRecipient: string diff --git a/staking-dashboard/src/hooks/rollup/useAttesterView.ts b/staking-dashboard/src/hooks/rollup/useAttesterView.ts index edd441645..d0d3fd8da 100644 --- a/staking-dashboard/src/hooks/rollup/useAttesterView.ts +++ b/staking-dashboard/src/hooks/rollup/useAttesterView.ts @@ -5,9 +5,16 @@ import { contracts } from "@/contracts" /** * Hook to get comprehensive attester/sequencer information including status, balance, and exit details */ -export function useAttesterView(attesterAddress: Address | undefined) { +export function useAttesterView( + attesterAddress: Address | undefined, + rollupAddress?: Address, +) { + // Delegations on a legacy rollup must be queried against their own rollup, + // not the current canonical one, or getAttesterView returns status=NONE and + // the UI strands the user in "IN QUEUE" with no finalize button. + const address = rollupAddress ?? contracts.rollup.address const { data, isLoading, error, refetch } = useReadContract({ - address: contracts.rollup.address, + address, abi: contracts.rollup.abi, functionName: "getAttesterView", args: attesterAddress ? [attesterAddress] : undefined, diff --git a/staking-dashboard/src/hooks/rollup/useFinalizeWithdraw.ts b/staking-dashboard/src/hooks/rollup/useFinalizeWithdraw.ts index cf07c27d3..c27027554 100644 --- a/staking-dashboard/src/hooks/rollup/useFinalizeWithdraw.ts +++ b/staking-dashboard/src/hooks/rollup/useFinalizeWithdraw.ts @@ -19,10 +19,10 @@ export function useFinalizeWithdraw() { }) return { - finalizeWithdraw: (attesterAddress: Address) => { + finalizeWithdraw: (attesterAddress: Address, rollupAddress?: Address) => { return write.writeContract({ abi: contracts.rollup.abi, - address: contracts.rollup.address, + address: rollupAddress ?? contracts.rollup.address, functionName: "finalizeWithdraw", args: [attesterAddress] }) diff --git a/staking-dashboard/src/hooks/rollup/useSequencerStatus.ts b/staking-dashboard/src/hooks/rollup/useSequencerStatus.ts index c4559935b..b6e4d2946 100644 --- a/staking-dashboard/src/hooks/rollup/useSequencerStatus.ts +++ b/staking-dashboard/src/hooks/rollup/useSequencerStatus.ts @@ -42,9 +42,12 @@ export function getStatusLabel(status: number | undefined): string { * @param sequencerAddress - The address of the sequencer * @returns Sequencer status, label, and related information */ -export function useSequencerStatus(sequencerAddress: Address | undefined) { +export function useSequencerStatus( + sequencerAddress: Address | undefined, + rollupAddress?: Address, +) { const { status, effectiveBalance, exit, isLoading, error, refetch } = - useAttesterView(sequencerAddress); + useAttesterView(sequencerAddress, rollupAddress); // Query the governance withdrawal to get the REAL unlock time const { withdrawal, isLoading: isLoadingWithdrawal } = useGovernanceWithdrawal(exit?.withdrawalId); From 19cfc1906b812688b8b13d833863df4041d319af Mon Sep 17 00:00:00 2001 From: Robert Brada Date: Fri, 24 Apr 2026 17:18:03 +0200 Subject: [PATCH 2/4] fix: extend legacy-rollup threading to direct stakes and wallet modal Same underlying bug as the delegation flow, remaining in four spots: - ATPDetailsDirectStakeItem - WalletDelegationItem - WalletDirectStakeItem - WalletWithdrawalActions Each used useSequencerStatus / finalizeWithdraw with no rollup address, so a legacy-rollup stake surfaced here would still stick in "IN QUEUE" and target the wrong contract on finalize. Surface per-row rollupAddress from the indexer (staked + erc20StakedWithProvider + deposit tables all already store it) via /api/atp/:atp/details (direct stakes) and /api/staking/:beneficiary (all four breakdowns). Thread it through the frontend types and into the four components. Pending localStorage stakes default to the current rollup since they were, by definition, just submitted against it. --- atp-indexer/src/api/handlers/atp/details.ts | 1 + .../api/handlers/staking/beneficiary-overview.ts | 4 ++++ .../ATPDetailsModal/ATPDetailsDirectStakeItem.tsx | 4 +++- .../WalletDelegationItem.tsx | 4 +++- .../WalletDirectStakeItem.tsx | 4 +++- .../WalletWithdrawalActions.tsx | 4 +++- staking-dashboard/src/hooks/atp/useATPDetails.ts | 1 + .../src/hooks/atp/useAggregatedStakingData.ts | 13 +++++++++++++ 8 files changed, 31 insertions(+), 4 deletions(-) diff --git a/atp-indexer/src/api/handlers/atp/details.ts b/atp-indexer/src/api/handlers/atp/details.ts index af42f1c77..64f602042 100644 --- a/atp-indexer/src/api/handlers/atp/details.ts +++ b/atp-indexer/src/api/handlers/atp/details.ts @@ -29,6 +29,7 @@ function formatDirectStakes( return { attesterAddress: checksumAddress(stake.attesterAddress), operatorAddress: checksumAddress(stake.operatorAddress), + rollupAddress: checksumAddress(stake.rollupAddress), stakedAmount: activationThreshold, totalSlashed: totalSlashed.toString(), txHash: stake.txHash, diff --git a/atp-indexer/src/api/handlers/staking/beneficiary-overview.ts b/atp-indexer/src/api/handlers/staking/beneficiary-overview.ts index 49b38f3fc..3ea96d34b 100644 --- a/atp-indexer/src/api/handlers/staking/beneficiary-overview.ts +++ b/atp-indexer/src/api/handlers/staking/beneficiary-overview.ts @@ -129,6 +129,7 @@ export async function handleBeneficiaryStakingOverview(c: Context): Promise ({ atpAddress: checksumAddress(atpByStaker.get(stake.stakerAddress.toLowerCase()) || stake.atpAddress), attesterAddress: checksumAddress(stake.attesterAddress), + rollupAddress: checksumAddress(stake.rollupAddress), stakedAmount: stake.stakedAmount.toString(), hasFailedDeposit: stake.hasFailedDeposit, failedDepositTxHash: stake.failedDepositTxHash, @@ -150,6 +151,7 @@ export async function handleBeneficiaryStakingOverview(c: Context): Promise ({ attesterAddress: checksumAddress(dep.attesterAddress), withdrawerAddress: checksumAddress(dep.withdrawerAddress), + rollupAddress: checksumAddress(dep.rollupAddress), stakedAmount: dep.amount.toString(), txHash: dep.txHash, timestamp: Number(dep.timestamp), diff --git a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDirectStakeItem.tsx b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDirectStakeItem.tsx index fa0ce6534..40fe64d7d 100644 --- a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDirectStakeItem.tsx +++ b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDirectStakeItem.tsx @@ -43,7 +43,8 @@ export const ATPDetailsDirectStakeItem = ({ stake, stakerAddress, rollupVersion, const { date, time } = formatBlockTimestamp(stake.timestamp) const { isRewardsClaimable } = useIsRewardsClaimable() - const { status, statusLabel, isLoading: isLoadingStatus, canFinalize, actualUnlockTime, refetch: refetchStatus } = useSequencerStatus(stake.attesterAddress as Address) + const stakeRollupAddress = stake.rollupAddress as Address + const { status, statusLabel, isLoading: isLoadingStatus, canFinalize, actualUnlockTime, refetch: refetchStatus } = useSequencerStatus(stake.attesterAddress as Address, stakeRollupAddress) const { withdrawalDelayDays } = useGovernanceConfig() const { @@ -389,6 +390,7 @@ export const ATPDetailsDirectStakeItem = ({ stake, stakerAddress, rollupVersion, stakerAddress={stakerAddress} attesterAddress={stake.attesterAddress as Address} rollupVersion={rollupVersion} + rollupAddress={stakeRollupAddress} status={status} canFinalize={canFinalize} actualUnlockTime={actualUnlockTime} diff --git a/staking-dashboard/src/components/WalletStakesDetailsModal/WalletDelegationItem.tsx b/staking-dashboard/src/components/WalletStakesDetailsModal/WalletDelegationItem.tsx index 621f3daf7..b85d18ebd 100644 --- a/staking-dashboard/src/components/WalletStakesDetailsModal/WalletDelegationItem.tsx +++ b/staking-dashboard/src/components/WalletStakesDetailsModal/WalletDelegationItem.tsx @@ -45,7 +45,8 @@ export const WalletDelegationItem = ({ const { getSplitStatus, claimAllHook } = useClaimAllContext() const { isRewardsClaimable } = useIsRewardsClaimable() - const { status, statusLabel, isLoading: isLoadingStatus, canFinalize, actualUnlockTime, refetch: refetchStatus } = useSequencerStatus(delegation.attesterAddress as Address) + const delegationRollupAddress = delegation.rollupAddress as Address + const { status, statusLabel, isLoading: isLoadingStatus, canFinalize, actualUnlockTime, refetch: refetchStatus } = useSequencerStatus(delegation.attesterAddress as Address, delegationRollupAddress) const { withdrawalDelayDays } = useGovernanceConfig() const { @@ -380,6 +381,7 @@ export const WalletDelegationItem = ({ { try { - await finalizeWithdraw(attesterAddress) + await finalizeWithdraw(attesterAddress, rollupAddress) } catch (error) { console.error("Failed to finalize withdraw:", error) } diff --git a/staking-dashboard/src/hooks/atp/useATPDetails.ts b/staking-dashboard/src/hooks/atp/useATPDetails.ts index b7b03fdaa..53a7b7a3c 100644 --- a/staking-dashboard/src/hooks/atp/useATPDetails.ts +++ b/staking-dashboard/src/hooks/atp/useATPDetails.ts @@ -6,6 +6,7 @@ import { stringToBigInt } from '@/utils/atpFormatters' export interface DirectStake { attesterAddress: string operatorAddress: string + rollupAddress: string stakedAmount: bigint txHash: string timestamp: string diff --git a/staking-dashboard/src/hooks/atp/useAggregatedStakingData.ts b/staking-dashboard/src/hooks/atp/useAggregatedStakingData.ts index 5ce9559c1..2d2837948 100644 --- a/staking-dashboard/src/hooks/atp/useAggregatedStakingData.ts +++ b/staking-dashboard/src/hooks/atp/useAggregatedStakingData.ts @@ -20,6 +20,7 @@ import { export interface DirectStakeBreakdown { atpAddress: Address attesterAddress: Address + rollupAddress: Address stakedAmount: bigint hasFailedDeposit: boolean failedDepositTxHash: string | null @@ -41,6 +42,7 @@ export interface DelegationBreakdown { providerName?: string providerLogo?: string attesterAddress: Address + rollupAddress: Address stakedAmount: bigint rewards: bigint splitContract: Address @@ -60,6 +62,7 @@ export interface Erc20DelegationBreakdown { providerName?: string providerLogo?: string attesterAddress: Address + rollupAddress: Address stakedAmount: bigint rewards: bigint splitContract: Address @@ -77,6 +80,7 @@ export interface Erc20DelegationBreakdown { export interface Erc20DirectStakeBreakdown { attesterAddress: Address withdrawerAddress: Address + rollupAddress: Address stakedAmount: bigint hasFailedDeposit: boolean failedDepositTxHash: string | null @@ -109,6 +113,7 @@ export interface AggregatedStakingData { interface ApiDirectStake { atpAddress: string attesterAddress: string + rollupAddress: string stakedAmount: string hasFailedDeposit: boolean failedDepositTxHash: string | null @@ -130,6 +135,7 @@ interface ApiDelegation { providerName?: string providerLogo?: string attesterAddress: string + rollupAddress: string stakedAmount: string splitContract: string providerTakeRate: number @@ -148,6 +154,7 @@ interface ApiErc20Delegation { providerName?: string providerLogo?: string attesterAddress: string + rollupAddress: string stakedAmount: string splitContract: string providerTakeRate: number @@ -164,6 +171,7 @@ interface ApiErc20Delegation { interface ApiErc20DirectStake { attesterAddress: string withdrawerAddress: string + rollupAddress: string stakedAmount: string hasFailedDeposit: boolean failedDepositTxHash: string | null @@ -207,6 +215,7 @@ function parseDirectStake(stake: ApiDirectStake): DirectStakeBreakdown { return { atpAddress: stake.atpAddress as Address, attesterAddress: stake.attesterAddress as Address, + rollupAddress: stake.rollupAddress as Address, stakedAmount: stringToBigInt(stake.stakedAmount), hasFailedDeposit: stake.hasFailedDeposit, failedDepositTxHash: stake.failedDepositTxHash, @@ -267,6 +276,7 @@ function parseDelegation( providerName: delegation.providerName, providerLogo: delegation.providerLogo, attesterAddress: delegation.attesterAddress as Address, + rollupAddress: delegation.rollupAddress as Address, stakedAmount: stringToBigInt(delegation.stakedAmount), rewards: delegation.hasFailedDeposit ? 0n : userRewards, splitContract: delegation.splitContract as Address, @@ -324,6 +334,7 @@ function parseErc20Delegation( providerName: delegation.providerName, providerLogo: delegation.providerLogo, attesterAddress: delegation.attesterAddress as Address, + rollupAddress: delegation.rollupAddress as Address, stakedAmount: stringToBigInt(delegation.stakedAmount), rewards: delegation.hasFailedDeposit ? 0n : userRewards, splitContract: delegation.splitContract as Address, @@ -346,6 +357,7 @@ function parseErc20DirectStake(stake: ApiErc20DirectStake): Erc20DirectStakeBrea return { attesterAddress: stake.attesterAddress as Address, withdrawerAddress: stake.withdrawerAddress as Address, + rollupAddress: stake.rollupAddress as Address, stakedAmount: stringToBigInt(stake.stakedAmount), hasFailedDeposit: stake.hasFailedDeposit, failedDepositTxHash: stake.failedDepositTxHash, @@ -502,6 +514,7 @@ export const useAggregatedStakingData = (): AggregatedStakingData => { .map(stake => ({ attesterAddress: stake.attesterAddress, withdrawerAddress: stake.withdrawerAddress, + rollupAddress: contracts.rollup.address, stakedAmount: BigInt(stake.stakedAmount), hasFailedDeposit: false, failedDepositTxHash: null, From 5fb6c8d37253fd7a9e90e53aad837146c6d323f9 Mon Sep 17 00:00:00 2001 From: Robert Brada Date: Fri, 24 Apr 2026 17:22:02 +0200 Subject: [PATCH 3/4] fix: thread rollupAddress through useWalletInitiateWithdraw Same legacy-rollup issue on the initiate-unstake path: an ERC20 direct staker on v0 clicking "Initiate Unstake" would send the tx to the env rollup (v1) where their attester doesn't exist, and it'd revert. --- .../WalletStakesDetailsModal/WalletWithdrawalActions.tsx | 2 +- .../src/hooks/rollup/useWalletInitiateWithdraw.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/staking-dashboard/src/components/WalletStakesDetailsModal/WalletWithdrawalActions.tsx b/staking-dashboard/src/components/WalletStakesDetailsModal/WalletWithdrawalActions.tsx index 38790428d..d668090a6 100644 --- a/staking-dashboard/src/components/WalletStakesDetailsModal/WalletWithdrawalActions.tsx +++ b/staking-dashboard/src/components/WalletStakesDetailsModal/WalletWithdrawalActions.tsx @@ -151,7 +151,7 @@ export const WalletWithdrawalActions = ({ const handleInitiateWithdraw = async () => { try { - await initiateWithdraw(attesterAddress, recipientAddress) + await initiateWithdraw(attesterAddress, recipientAddress, rollupAddress) } catch (error) { console.error("Failed to initiate withdraw:", error) } diff --git a/staking-dashboard/src/hooks/rollup/useWalletInitiateWithdraw.ts b/staking-dashboard/src/hooks/rollup/useWalletInitiateWithdraw.ts index 2708f651f..d91e1f8a1 100644 --- a/staking-dashboard/src/hooks/rollup/useWalletInitiateWithdraw.ts +++ b/staking-dashboard/src/hooks/rollup/useWalletInitiateWithdraw.ts @@ -18,10 +18,10 @@ export function useWalletInitiateWithdraw() { hash, }) - const initiateWithdraw = (attesterAddress: Address, recipientAddress: Address) => { + const initiateWithdraw = (attesterAddress: Address, recipientAddress: Address, rollupAddress?: Address) => { return writeContract({ abi: contracts.rollup.abi, - address: contracts.rollup.address, + address: rollupAddress ?? contracts.rollup.address, functionName: "initiateWithdraw", args: [attesterAddress, recipientAddress], }) From b6a1b3d0ce892e697dd8014bd7fb66e018ca258c Mon Sep 17 00:00:00 2001 From: Robert Brada Date: Fri, 24 Apr 2026 17:31:39 +0200 Subject: [PATCH 4/4] refactor: make rollupAddress required on withdraw hooks and withdrawal action props MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the silent `?? contracts.rollup.address` fallback on the rollup write hooks (`useFinalizeWithdraw`, `useWalletInitiateWithdraw`) and the read hooks (`useAttesterView`, `useSequencerStatus`, `useStakeHealth`). Callers must now pass rollupAddress explicitly — a missing value is a compile error instead of silently routing a legacy-rollup action to the canonical rollup (the exact bug this branch set out to fix). Threads rollupAddress through `useStakeHealth`, the one remaining consumer of `useAttesterView` that wasn't updated in the earlier commits. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ATPDetailsDelegationItem.tsx | 2 +- .../ATPDetailsDirectStakeItem.tsx | 2 +- .../ATPDetailsModal/WithdrawalActions.tsx | 2 +- .../WalletDelegationItem.tsx | 2 +- .../WalletDirectStakeItem.tsx | 2 +- .../WalletWithdrawalActions.tsx | 2 +- .../src/hooks/rollup/useAttesterView.ts | 17 +++++++++-------- .../src/hooks/rollup/useFinalizeWithdraw.ts | 4 ++-- .../src/hooks/rollup/useSequencerStatus.ts | 2 +- .../src/hooks/rollup/useStakeHealth.ts | 7 +++++-- .../hooks/rollup/useWalletInitiateWithdraw.ts | 4 ++-- 11 files changed, 25 insertions(+), 21 deletions(-) diff --git a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDelegationItem.tsx b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDelegationItem.tsx index 20461be6f..7086e4190 100644 --- a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDelegationItem.tsx +++ b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDelegationItem.tsx @@ -73,7 +73,7 @@ export const ATPDetailsDelegationItem = ({ isAtRisk, isCritical, isLoading: isLoadingHealth - } = useStakeHealth(delegation.operatorAddress as Address) + } = useStakeHealth(delegation.operatorAddress as Address, delegationRollupAddress) const splitStatus = getSplitStatus(delegation.splitContract as Address) const isInBatch = splitStatus !== 'idle' diff --git a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDirectStakeItem.tsx b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDirectStakeItem.tsx index 40fe64d7d..3c24004ba 100644 --- a/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDirectStakeItem.tsx +++ b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDirectStakeItem.tsx @@ -56,7 +56,7 @@ export const ATPDetailsDirectStakeItem = ({ stake, stakerAddress, rollupVersion, isAtRisk, isCritical, isLoading: isLoadingHealth - } = useStakeHealth(stake.attesterAddress as Address) + } = useStakeHealth(stake.attesterAddress as Address, stakeRollupAddress) const isUnstaked = stake.status === 'UNSTAKED' const isInQueue = status === SequencerStatus.NONE && !stake.hasFailedDeposit && !isUnstaked diff --git a/staking-dashboard/src/components/ATPDetailsModal/WithdrawalActions.tsx b/staking-dashboard/src/components/ATPDetailsModal/WithdrawalActions.tsx index e354af0f1..251c3c692 100644 --- a/staking-dashboard/src/components/ATPDetailsModal/WithdrawalActions.tsx +++ b/staking-dashboard/src/components/ATPDetailsModal/WithdrawalActions.tsx @@ -75,7 +75,7 @@ interface WithdrawalActionsProps { stakerAddress: Address; attesterAddress: Address; rollupVersion: bigint; - rollupAddress?: Address; + rollupAddress: Address; status: number | undefined; canFinalize: boolean; actualUnlockTime?: bigint; diff --git a/staking-dashboard/src/components/WalletStakesDetailsModal/WalletDelegationItem.tsx b/staking-dashboard/src/components/WalletStakesDetailsModal/WalletDelegationItem.tsx index b85d18ebd..7675943ff 100644 --- a/staking-dashboard/src/components/WalletStakesDetailsModal/WalletDelegationItem.tsx +++ b/staking-dashboard/src/components/WalletStakesDetailsModal/WalletDelegationItem.tsx @@ -58,7 +58,7 @@ export const WalletDelegationItem = ({ isAtRisk, isCritical, isLoading: isLoadingHealth - } = useStakeHealth(delegation.attesterAddress as Address) + } = useStakeHealth(delegation.attesterAddress as Address, delegationRollupAddress) const splitStatus = getSplitStatus(delegation.splitContract as Address) const isInBatch = splitStatus !== 'idle' diff --git a/staking-dashboard/src/components/WalletStakesDetailsModal/WalletDirectStakeItem.tsx b/staking-dashboard/src/components/WalletStakesDetailsModal/WalletDirectStakeItem.tsx index 40c0db977..276440f11 100644 --- a/staking-dashboard/src/components/WalletStakesDetailsModal/WalletDirectStakeItem.tsx +++ b/staking-dashboard/src/components/WalletStakesDetailsModal/WalletDirectStakeItem.tsx @@ -47,7 +47,7 @@ export const WalletDirectStakeItem = ({ isAtRisk, isCritical, isLoading: isLoadingHealth - } = useStakeHealth(stake.attesterAddress as Address) + } = useStakeHealth(stake.attesterAddress as Address, stakeRollupAddress) const isUnstaked = stake.status === 'UNSTAKED' const isInQueue = status === SequencerStatus.NONE && !stake.hasFailedDeposit && !isUnstaked diff --git a/staking-dashboard/src/components/WalletStakesDetailsModal/WalletWithdrawalActions.tsx b/staking-dashboard/src/components/WalletStakesDetailsModal/WalletWithdrawalActions.tsx index d668090a6..60cfcc1bd 100644 --- a/staking-dashboard/src/components/WalletStakesDetailsModal/WalletWithdrawalActions.tsx +++ b/staking-dashboard/src/components/WalletStakesDetailsModal/WalletWithdrawalActions.tsx @@ -64,7 +64,7 @@ function parseContractError(error: Error): string { interface WalletWithdrawalActionsProps { attesterAddress: Address recipientAddress: Address - rollupAddress?: Address + rollupAddress: Address status: number | undefined canFinalize: boolean actualUnlockTime?: bigint diff --git a/staking-dashboard/src/hooks/rollup/useAttesterView.ts b/staking-dashboard/src/hooks/rollup/useAttesterView.ts index d0d3fd8da..d81483ee5 100644 --- a/staking-dashboard/src/hooks/rollup/useAttesterView.ts +++ b/staking-dashboard/src/hooks/rollup/useAttesterView.ts @@ -3,23 +3,24 @@ import type { Address } from "viem" import { contracts } from "@/contracts" /** - * Hook to get comprehensive attester/sequencer information including status, balance, and exit details + * Hook to get comprehensive attester/sequencer information including status, balance, and exit details. + * + * `rollupAddress` is required (but may be undefined while the caller's data is still + * loading). A legacy-rollup stake queried against the current canonical rollup returns + * status=NONE and strands users in "IN QUEUE" with no finalize button — so there is + * deliberately no silent fallback to `contracts.rollup.address`. */ export function useAttesterView( attesterAddress: Address | undefined, - rollupAddress?: Address, + rollupAddress: Address | undefined, ) { - // Delegations on a legacy rollup must be queried against their own rollup, - // not the current canonical one, or getAttesterView returns status=NONE and - // the UI strands the user in "IN QUEUE" with no finalize button. - const address = rollupAddress ?? contracts.rollup.address const { data, isLoading, error, refetch } = useReadContract({ - address, + address: rollupAddress, abi: contracts.rollup.abi, functionName: "getAttesterView", args: attesterAddress ? [attesterAddress] : undefined, query: { - enabled: !!attesterAddress, + enabled: !!attesterAddress && !!rollupAddress, }, }) diff --git a/staking-dashboard/src/hooks/rollup/useFinalizeWithdraw.ts b/staking-dashboard/src/hooks/rollup/useFinalizeWithdraw.ts index c27027554..6a75d0371 100644 --- a/staking-dashboard/src/hooks/rollup/useFinalizeWithdraw.ts +++ b/staking-dashboard/src/hooks/rollup/useFinalizeWithdraw.ts @@ -19,10 +19,10 @@ export function useFinalizeWithdraw() { }) return { - finalizeWithdraw: (attesterAddress: Address, rollupAddress?: Address) => { + finalizeWithdraw: (attesterAddress: Address, rollupAddress: Address) => { return write.writeContract({ abi: contracts.rollup.abi, - address: rollupAddress ?? contracts.rollup.address, + address: rollupAddress, functionName: "finalizeWithdraw", args: [attesterAddress] }) diff --git a/staking-dashboard/src/hooks/rollup/useSequencerStatus.ts b/staking-dashboard/src/hooks/rollup/useSequencerStatus.ts index b6e4d2946..7720f1c86 100644 --- a/staking-dashboard/src/hooks/rollup/useSequencerStatus.ts +++ b/staking-dashboard/src/hooks/rollup/useSequencerStatus.ts @@ -44,7 +44,7 @@ export function getStatusLabel(status: number | undefined): string { */ export function useSequencerStatus( sequencerAddress: Address | undefined, - rollupAddress?: Address, + rollupAddress: Address | undefined, ) { const { status, effectiveBalance, exit, isLoading, error, refetch } = useAttesterView(sequencerAddress, rollupAddress); diff --git a/staking-dashboard/src/hooks/rollup/useStakeHealth.ts b/staking-dashboard/src/hooks/rollup/useStakeHealth.ts index f6ffccdae..f812667e5 100644 --- a/staking-dashboard/src/hooks/rollup/useStakeHealth.ts +++ b/staking-dashboard/src/hooks/rollup/useStakeHealth.ts @@ -28,9 +28,12 @@ const SLASH_AMOUNT = 2000n * 10n ** 18n * - isAtRisk: healthPercentage < 50 (has been slashed significantly) * - isCritical: effectiveBalance <= ejectionThreshold (imminent ejection) */ -export function useStakeHealth(attesterAddress: Address | undefined) { +export function useStakeHealth( + attesterAddress: Address | undefined, + rollupAddress: Address | undefined, +) { const { effectiveBalance, status, isLoading: isLoadingAttester, error: attesterError, refetch: refetchAttester } = - useAttesterView(attesterAddress) + useAttesterView(attesterAddress, rollupAddress) const { ejectionThreshold, isLoading: isLoadingEjection, error: ejectionError, refetch: refetchEjection } = useEjectionThreshold() diff --git a/staking-dashboard/src/hooks/rollup/useWalletInitiateWithdraw.ts b/staking-dashboard/src/hooks/rollup/useWalletInitiateWithdraw.ts index d91e1f8a1..955a2046f 100644 --- a/staking-dashboard/src/hooks/rollup/useWalletInitiateWithdraw.ts +++ b/staking-dashboard/src/hooks/rollup/useWalletInitiateWithdraw.ts @@ -18,10 +18,10 @@ export function useWalletInitiateWithdraw() { hash, }) - const initiateWithdraw = (attesterAddress: Address, recipientAddress: Address, rollupAddress?: Address) => { + const initiateWithdraw = (attesterAddress: Address, recipientAddress: Address, rollupAddress: Address) => { return writeContract({ abi: contracts.rollup.abi, - address: rollupAddress ?? contracts.rollup.address, + address: rollupAddress, functionName: "initiateWithdraw", args: [attesterAddress, recipientAddress], })