diff --git a/atp-indexer/src/api/handlers/atp/details.ts b/atp-indexer/src/api/handlers/atp/details.ts index 3100df41f..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, @@ -63,6 +64,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/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/ATPDetailsDelegationItem.tsx b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDelegationItem.tsx index 64e5f9f02..7086e4190 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 { @@ -72,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' @@ -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/ATPDetailsDirectStakeItem.tsx b/staking-dashboard/src/components/ATPDetailsModal/ATPDetailsDirectStakeItem.tsx index fa0ce6534..3c24004ba 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 { @@ -55,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 @@ -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/ATPDetailsModal/WithdrawalActions.tsx b/staking-dashboard/src/components/ATPDetailsModal/WithdrawalActions.tsx index 51e1a2b9f..251c3c692 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/components/WalletStakesDetailsModal/WalletDelegationItem.tsx b/staking-dashboard/src/components/WalletStakesDetailsModal/WalletDelegationItem.tsx index 621f3daf7..7675943ff 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 { @@ -57,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' @@ -380,6 +381,7 @@ export const WalletDelegationItem = ({ { try { - await initiateWithdraw(attesterAddress, recipientAddress) + await initiateWithdraw(attesterAddress, recipientAddress, rollupAddress) } catch (error) { console.error("Failed to initiate withdraw:", error) } @@ -157,7 +159,7 @@ export const WalletWithdrawalActions = ({ 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..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 @@ -21,6 +22,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/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, diff --git a/staking-dashboard/src/hooks/rollup/useAttesterView.ts b/staking-dashboard/src/hooks/rollup/useAttesterView.ts index edd441645..d81483ee5 100644 --- a/staking-dashboard/src/hooks/rollup/useAttesterView.ts +++ b/staking-dashboard/src/hooks/rollup/useAttesterView.ts @@ -3,16 +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) { +export function useAttesterView( + attesterAddress: Address | undefined, + rollupAddress: Address | undefined, +) { const { data, isLoading, error, refetch } = useReadContract({ - address: contracts.rollup.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 cf07c27d3..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) => { + finalizeWithdraw: (attesterAddress: Address, rollupAddress: Address) => { return write.writeContract({ abi: contracts.rollup.abi, - address: 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 c4559935b..7720f1c86 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 | undefined, +) { 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); 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 2708f651f..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) => { + const initiateWithdraw = (attesterAddress: Address, recipientAddress: Address, rollupAddress: Address) => { return writeContract({ abi: contracts.rollup.abi, - address: contracts.rollup.address, + address: rollupAddress, functionName: "initiateWithdraw", args: [attesterAddress, recipientAddress], })