diff --git a/staking-dashboard/src/components/ClaimDelegationRewardsButton/ClaimDelegationRewardsButton.tsx b/staking-dashboard/src/components/ClaimDelegationRewardsButton/ClaimDelegationRewardsButton.tsx index ab5996546..738e34c8e 100644 --- a/staking-dashboard/src/components/ClaimDelegationRewardsButton/ClaimDelegationRewardsButton.tsx +++ b/staking-dashboard/src/components/ClaimDelegationRewardsButton/ClaimDelegationRewardsButton.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react" +import { useEffect, useMemo } from "react" import { useAccount } from "wagmi" import { useClaimSplitRewards } from "@/hooks/splits" import { useStakingAssetTokenDetails } from "@/hooks/stakingRegistry" @@ -33,11 +33,11 @@ export const ClaimDelegationRewardsButton = ({ const { stakingAssetAddress: tokenAddress } = useStakingAssetTokenDetails() const { showAlert } = useAlert() - // Fetch balances for skip logic + // Fetch balances for skip logic - extract refetch functions const { warehouseAddress } = useSplitsWarehouse(splitContract) - const { rewards: rollupBalance } = useSequencerRewards(splitContract) - const { balance: splitContractBalance } = useERC20Balance(tokenAddress!, splitContract) - const { balance: warehouseBalance } = useWarehouseBalance(warehouseAddress, beneficiary, tokenAddress) + const { rewards: rollupBalance, refetch: refetchRollup } = useSequencerRewards(splitContract) + const { balance: splitContractBalance, refetch: refetchSplitContract } = useERC20Balance(tokenAddress!, splitContract) + const { balance: warehouseBalance, refetch: refetchWarehouse } = useWarehouseBalance(warehouseAddress, beneficiary, tokenAddress) // Calculate split allocations based on provider take rate const totalAllocation = 10000n @@ -52,6 +52,16 @@ export const ClaimDelegationRewardsButton = ({ distributionIncentive: 0 } + // Memoize balances object to prevent effect re-runs on every render + const balances = useMemo(() => ({ + rollupBalance, + splitContractBalance, + warehouseBalance, + refetchRollup, + refetchSplitContract, + refetchWarehouse + }), [rollupBalance, splitContractBalance, warehouseBalance, refetchRollup, refetchSplitContract, refetchWarehouse]) + const { claim, claimStep, @@ -65,11 +75,7 @@ export const ClaimDelegationRewardsButton = ({ splitData, tokenAddress!, beneficiary as Address, - { - rollupBalance, - splitContractBalance, - warehouseBalance - } + balances ) // Call onSuccess callback when claim completes @@ -79,12 +85,15 @@ export const ClaimDelegationRewardsButton = ({ } }, [isSuccess, onSuccess]) - // Handle errors + // Handle errors - show all errors, not just rejections useEffect(() => { if (error) { const errorMessage = error.message if (errorMessage.includes('User rejected') || errorMessage.includes('rejected')) { showAlert('warning', 'Transaction was cancelled') + } else { + // Show error for all other failures + showAlert('error', `Claim failed: ${errorMessage}`) } } }, [error, showAlert]) diff --git a/staking-dashboard/src/components/VestingSchedule/VestingGraph.tsx b/staking-dashboard/src/components/VestingSchedule/VestingGraph.tsx index 054db07c4..169530181 100644 --- a/staking-dashboard/src/components/VestingSchedule/VestingGraph.tsx +++ b/staking-dashboard/src/components/VestingSchedule/VestingGraph.tsx @@ -3,7 +3,6 @@ import type { Address } from "viem" import { formatTokenAmount } from "@/utils/atpFormatters" import { useStakingAssetTokenDetails } from "@/hooks/stakingRegistry" import { useVestingCalculation } from "@/hooks/atp" -import { isAuctionRegistry } from "@/hooks/atpRegistry" interface VestingGraphProps { globalLock: { @@ -20,12 +19,9 @@ interface VestingGraphProps { /** * SVG vector graph showing cliff vesting pattern */ -export const VestingGraph = ({ globalLock, registryAddress, className = "" }: VestingGraphProps) => { +export const VestingGraph = ({ globalLock, className = "" }: VestingGraphProps) => { const { symbol, decimals } = useStakingAssetTokenDetails() - // Check if this is an ATP from auction registry - const isAuctionATP = isAuctionRegistry(registryAddress) - // Check for invalid time range const hasInvalidTimeRange = Number(globalLock.endTime) < Number(globalLock.startTime) @@ -165,23 +161,6 @@ export const VestingGraph = ({ globalLock, registryAddress, className = "" }: Ve return (
- {/* TGE Notice for Auction ATP */} - {isAuctionATP && ( -
-
- TGE Notice: Tokens become available at TGE. TGE is decided by governance. Earliest anticipated in 90 days from start date. Latest is{' '} - - {new Date(Number(globalLock.endTime) * 1000).toLocaleDateString('en-US', { - day: 'numeric', - month: 'short', - year: 'numeric' - })} - - {' '}as shown in the graph below. -
-
- )} - { const currentSplitContract = currentTask?.type === 'delegation' ? currentTask.splitContract : undefined const currentCoinbase = currentTask?.type === 'coinbase' ? currentTask.coinbaseAddress : undefined - // Fetch balances for current task (for delegations) + // Fetch balances for current task (for delegations) - extract refetch functions const { warehouseAddress, isLoading: isLoadingWarehouse } = useSplitsWarehouse(currentSplitContract) - const { rewards: rollupBalance, isLoading: isLoadingRollup } = useSequencerRewards(currentSplitContract || currentCoinbase || '') - const { balance: splitContractBalance, isLoading: isLoadingSplitBalance } = useERC20Balance(tokenAddress, currentSplitContract) - const { balance: warehouseBalance, isLoading: isLoadingWarehouseBalance } = useWarehouseBalance(warehouseAddress, userAddress, tokenAddress) + const { rewards: rollupBalance, isLoading: isLoadingRollup, refetch: refetchRollup } = useSequencerRewards(currentSplitContract || currentCoinbase || '') + const { balance: splitContractBalance, isLoading: isLoadingSplitBalance, refetch: refetchSplitContract } = useERC20Balance(tokenAddress, currentSplitContract) + const { balance: warehouseBalance, isLoading: isLoadingWarehouseBalance, refetch: refetchWarehouse } = useWarehouseBalance(warehouseAddress, userAddress, tokenAddress) const isLoadingBalances = currentTask?.type === 'delegation' ? (isLoadingWarehouse || isLoadingRollup || isLoadingSplitBalance || isLoadingWarehouseBalance) : isLoadingRollup + // Memoize balances object to prevent effect re-runs on every render + const balances = useMemo(() => ({ + rollupBalance, + splitContractBalance, + warehouseBalance, + refetchRollup, + refetchSplitContract, + refetchWarehouse + }), [rollupBalance, splitContractBalance, warehouseBalance, refetchRollup, refetchSplitContract, refetchWarehouse]) + // Use existing hooks for claiming const delegationClaimHook = useClaimSplitRewards( currentSplitContract, currentTask?.splitData || { recipients: [], allocations: [], totalAllocation: 0n, distributionIncentive: 0 }, tokenAddress, userAddress, - { - rollupBalance, - splitContractBalance, - warehouseBalance - } + balances ) const coinbaseClaimHook = useClaimSequencerRewards() diff --git a/staking-dashboard/src/hooks/splits/useClaimAllSplitRewards.ts b/staking-dashboard/src/hooks/splits/useClaimAllSplitRewards.ts index 29af5b135..c7a87fef7 100644 --- a/staking-dashboard/src/hooks/splits/useClaimAllSplitRewards.ts +++ b/staking-dashboard/src/hooks/splits/useClaimAllSplitRewards.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from "react" +import { useState, useEffect, useCallback, useMemo } from "react" import { useAccount } from "wagmi" import { useClaimSplitRewards } from "./useClaimSplitRewards" import { useSequencerRewards } from "@/hooks/rollup/useSequencerRewards" @@ -39,25 +39,31 @@ export const useClaimAllSplitRewards = () => { // Get token address for balance queries const { stakingAssetAddress: tokenAddress } = useStakingAssetTokenDetails() - // Fetch balances for current task + // Fetch balances for current task - extract refetch functions const { warehouseAddress, isLoading: isLoadingWarehouse } = useSplitsWarehouse(currentTask?.splitContract) - const { rewards: rollupBalance, isLoading: isLoadingRollupBalance } = useSequencerRewards(currentTask?.splitContract || '') - const { balance: splitContractBalance, isLoading: isLoadingSplitContractBalance } = useERC20Balance(tokenAddress, currentTask?.splitContract) - const { balance: warehouseBalance, isLoading: isLoadingWarehouseBalance } = useWarehouseBalance(warehouseAddress, beneficiary, tokenAddress) + const { rewards: rollupBalance, isLoading: isLoadingRollupBalance, refetch: refetchRollup } = useSequencerRewards(currentTask?.splitContract || '') + const { balance: splitContractBalance, isLoading: isLoadingSplitContractBalance, refetch: refetchSplitContract } = useERC20Balance(tokenAddress, currentTask?.splitContract) + const { balance: warehouseBalance, isLoading: isLoadingWarehouseBalance, refetch: refetchWarehouse } = useWarehouseBalance(warehouseAddress, beneficiary, tokenAddress) const isLoading = isLoadingWarehouse || isLoadingRollupBalance || isLoadingSplitContractBalance || isLoadingWarehouseBalance + // Memoize balances object to prevent effect re-runs on every render + const balances = useMemo(() => ({ + rollupBalance, + splitContractBalance, + warehouseBalance, + refetchRollup, + refetchSplitContract, + refetchWarehouse + }), [rollupBalance, splitContractBalance, warehouseBalance, refetchRollup, refetchSplitContract, refetchWarehouse]) + // Use the single claim hook for the current task const claimHook = useClaimSplitRewards( currentTask?.splitContract, currentTask?.splitData || { recipients: [], allocations: [], totalAllocation: 0n, distributionIncentive: 0 }, currentTask?.tokenAddress, currentTask?.userAddress, - { - rollupBalance, - splitContractBalance, - warehouseBalance - } + balances ) // Monitor claim completion and move to next task diff --git a/staking-dashboard/src/hooks/splits/useClaimSplitRewards.ts b/staking-dashboard/src/hooks/splits/useClaimSplitRewards.ts index f20564669..f60159f9b 100644 --- a/staking-dashboard/src/hooks/splits/useClaimSplitRewards.ts +++ b/staking-dashboard/src/hooks/splits/useClaimSplitRewards.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react" +import { useState, useEffect, useRef } from "react" import { useDistributeRewards } from "./useDistributeRewards" import { useWithdrawRewards } from "./useWithdrawRewards" import { useSplitsWarehouse } from "./useSplitsWarehouse" @@ -10,6 +10,9 @@ interface BalanceData { rollupBalance?: bigint splitContractBalance?: bigint warehouseBalance?: bigint + refetchRollup?: () => Promise + refetchSplitContract?: () => Promise + refetchWarehouse?: () => Promise } type QueueStep = 'claiming' | 'distributing' | 'withdrawing' @@ -31,6 +34,10 @@ export const useClaimSplitRewards = ( const [skipMessage, setSkipMessage] = useState(null) const [completedMessage, setCompletedMessage] = useState(null) const [isProcessing, setIsProcessing] = useState(false) + const [refetchError, setRefetchError] = useState(null) + + // Track which step is currently completing to prevent duplicate timeout scheduling + const completingStepRef = useRef(null) // Get warehouse address from split contract const { warehouseAddress, isLoading: isLoadingWarehouse } = useSplitsWarehouse(splitContractAddress) @@ -111,7 +118,7 @@ export const useClaimSplitRewards = ( } }, [queue, balances]) - // Handle transaction success - show completion message then remove from queue + // Handle transaction success - show completion message, refetch balances, then remove from queue useEffect(() => { if (!isProcessing || queue.length === 0) return @@ -135,14 +142,64 @@ export const useClaimSplitRewards = ( } if (stepCompleted && stepToRemove) { + // Guard: prevent duplicate timeout scheduling if already completing this step + if (completingStepRef.current === stepToRemove) return + + completingStepRef.current = stepToRemove setCompletedMessage(message) - setTimeout(() => { - setCompletedMessage(null) - setIsProcessing(false) - setQueue(prev => prev.filter(step => step !== stepToRemove)) - }, 1000) + + // Determine which balances need refetching for the NEXT step + const refetchPromises: Promise[] = [] + + if (stepToRemove === 'claiming') { + // Next step is 'distributing', which checks splitContractBalance + if (balances?.refetchSplitContract) { + refetchPromises.push(balances.refetchSplitContract()) + } + } else if (stepToRemove === 'distributing') { + // After distributing, tokens move from split contract to warehouse + // Refetch BOTH balances to keep the UI accurate + if (balances?.refetchSplitContract) { + refetchPromises.push(balances.refetchSplitContract()) + } + if (balances?.refetchWarehouse) { + refetchPromises.push(balances.refetchWarehouse()) + } + } + + // Wait for refetch to complete before advancing + if (refetchPromises.length > 0) { + Promise.all(refetchPromises) + .then(() => { + // Refetch succeeded - advance to next step after delay + setTimeout(() => { + setCompletedMessage(null) + setIsProcessing(false) + setQueue(prev => prev.filter(step => step !== stepToRemove)) + completingStepRef.current = null // Clear ref after advancing + }, 500) + }) + .catch(err => { + console.error('Balance refetch failed:', err) + // Treat refetch failure as an error - halt the flow completely + setRefetchError(err instanceof Error ? err : new Error('Balance refetch failed')) + setCompletedMessage(null) + setQueue([]) + setClaimStep('idle') + setIsProcessing(false) + completingStepRef.current = null // Clear ref on error + }) + } else { + // No refetch needed (e.g., last step) - advance immediately + setTimeout(() => { + setCompletedMessage(null) + setIsProcessing(false) + setQueue(prev => prev.filter(step => step !== stepToRemove)) + completingStepRef.current = null // Clear ref after advancing + }, 500) + } } - }, [queue, claimHook.isSuccess, distributeHook.isSuccess, withdrawHook.isSuccess, isProcessing]) + }, [queue, claimHook.isSuccess, distributeHook.isSuccess, withdrawHook.isSuccess, isProcessing, balances]) // Handle errors - reset queue useEffect(() => { @@ -151,6 +208,8 @@ export const useClaimSplitRewards = ( setQueue([]) setClaimStep('idle') setIsProcessing(false) + setRefetchError(null) + completingStepRef.current = null claimHook.reset() distributeHook.reset() withdrawHook.reset() @@ -160,7 +219,9 @@ export const useClaimSplitRewards = ( const claim = () => { if (!warehouseAddress) return setSkipMessage(null) + setRefetchError(null) setIsProcessing(false) + completingStepRef.current = null setQueue(['claiming', 'distributing', 'withdrawing']) } @@ -176,8 +237,8 @@ export const useClaimSplitRewards = ( isLoading: isLoadingWarehouse, isClaiming, isSuccess, - isError: claimHook.isError || distributeHook.isError || withdrawHook.isError, - error: claimHook.error || distributeHook.error || withdrawHook.error, + isError: claimHook.isError || distributeHook.isError || withdrawHook.isError || !!refetchError, + error: refetchError || claimHook.error || distributeHook.error || withdrawHook.error, claimTxHash: claimHook.txHash, distributeTxHash: distributeHook.txHash, withdrawTxHash: withdrawHook.txHash, @@ -187,6 +248,8 @@ export const useClaimSplitRewards = ( setSkipMessage(null) setCompletedMessage(null) setIsProcessing(false) + setRefetchError(null) + completingStepRef.current = null claimHook.reset() distributeHook.reset() withdrawHook.reset() diff --git a/staking-dashboard/src/hooks/staker/useMultipleStakeWithProviderRewards.ts b/staking-dashboard/src/hooks/staker/useMultipleStakeWithProviderRewards.ts index 05164005a..7a225bde8 100644 --- a/staking-dashboard/src/hooks/staker/useMultipleStakeWithProviderRewards.ts +++ b/staking-dashboard/src/hooks/staker/useMultipleStakeWithProviderRewards.ts @@ -1,7 +1,8 @@ import { useReadContracts } from 'wagmi' import { ERC20Abi } from '@/contracts/abis/ERC20' -import { calculateUserShareFromTakeRate } from '@/utils/rewardCalculations' +import { calculateTotalUserShareFromSplitRewards } from '@/utils/rewardCalculations' import { useStakingAssetTokenDetails } from '@/hooks/stakingRegistry' +import { contracts } from '@/contracts' import type { Delegation } from '@/hooks/atp' import type { StakeWithProviderReward } from './types' @@ -14,8 +15,9 @@ interface MultipleStakeWithProviderRewardsParams { * Hook to calculate rewards for multiple delegations (stakeWithProvider method) * * Reward Calculation Logic: - * 1. Get total rewards for each: totalRewards = stakingToken.balanceOf(splitContract) - * 2. Apply user's share: userRewards = totalRewards * (10000 - providerTakeRate) / 10000 + * 1. Get rollup rewards: rollup.getSequencerRewards(splitContract) + * 2. Get split contract balance: stakingToken.balanceOf(splitContract) + * 3. Calculate user's share from both sources using take rate */ export const useMultipleStakeWithProviderRewards = ({ delegations, @@ -23,19 +25,29 @@ export const useMultipleStakeWithProviderRewards = ({ }: MultipleStakeWithProviderRewardsParams) => { const { stakingAssetAddress: tokenAddress } = useStakingAssetTokenDetails() - // Build contracts array for all split contract balance queries - const balanceContracts = tokenAddress && delegations.length > 0 - ? delegations.map(delegation => ({ - address: tokenAddress as `0x${string}`, - abi: ERC20Abi, - functionName: 'balanceOf', - args: [delegation.splitContract as `0x${string}`], - })) + // Build contracts array for both rollup rewards and split balance queries + const rewardContracts = tokenAddress && delegations.length > 0 + ? delegations.flatMap(delegation => [ + // Query rollup rewards + { + address: contracts.rollup.address, + abi: contracts.rollup.abi, + functionName: 'getSequencerRewards', + args: [delegation.splitContract as `0x${string}`], + }, + // Query split contract balance + { + address: tokenAddress as `0x${string}`, + abi: ERC20Abi, + functionName: 'balanceOf', + args: [delegation.splitContract as `0x${string}`], + }, + ]) : [] - // Get balances for all split contracts - const { data: balancesData, isLoading, error, refetch } = useReadContracts({ - contracts: balanceContracts, + // Get rewards from both rollup and split contracts + const { data: rewardData, isLoading, error, refetch } = useReadContracts({ + contracts: rewardContracts, query: { enabled: !!tokenAddress && delegations.length > 0 && enabled, }, @@ -43,8 +55,16 @@ export const useMultipleStakeWithProviderRewards = ({ // Calculate user rewards for each delegation const delegationRewards: StakeWithProviderReward[] = delegations.map((delegation, index) => { - const totalRewards = (balancesData?.[index]?.result as bigint) || 0n - const userRewards = calculateUserShareFromTakeRate(totalRewards, delegation.providerTakeRate) + const rollupRewards = (rewardData?.[index * 2]?.result as bigint) || 0n + const splitBalance = (rewardData?.[index * 2 + 1]?.result as bigint) || 0n + + const totalRewards = rollupRewards + splitBalance + const userRewards = calculateTotalUserShareFromSplitRewards( + rollupRewards, + splitBalance, + 0n, // warehouse balance (omitted for this flow) + delegation.providerTakeRate + ) return { providerId: delegation.providerId, @@ -63,7 +83,7 @@ export const useMultipleStakeWithProviderRewards = ({ totalUserRewards, isLoading, error, - isSuccess: !!balancesData, + isSuccess: !!rewardData, refetch } } diff --git a/staking-dashboard/src/pages/ATP/MyPositionPage.tsx b/staking-dashboard/src/pages/ATP/MyPositionPage.tsx index ebd4beed2..22b2f7fa9 100644 --- a/staking-dashboard/src/pages/ATP/MyPositionPage.tsx +++ b/staking-dashboard/src/pages/ATP/MyPositionPage.tsx @@ -201,7 +201,7 @@ export default function MyPositionPage() { )} {/* Progress bar - only show when user can stake */} - {canStake && ( + {canStake ? (
{stakedPercent > 0 && (
)}
- )} + ): null}