Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Bug of not showing the correct amount in today's rewards when staking later during epoch #749

Merged
merged 10 commits into from
Mar 17, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const getFormattedReward = (reward: number | undefined) =>
const SHARE_TEXT = `I just earned my first reward through the Operate app powered by #olas!\n\nDownload the Pearl app:`;

export const NotifyRewardsModal = () => {
const { isEligibleForRewards, availableRewardsForEpochEth } =
const { isEligibleForRewards, eligibleRewardsThisEpochInEth } =
useRewardContext();
const { mainOlasBalance } = useMainOlasBalance();
const { showNotification, store } = useElectronApi();
Expand All @@ -35,20 +35,20 @@ export const NotifyRewardsModal = () => {
[mainOlasBalance],
);
const formattedEarnedRewards = useMemo(
() => balanceFormat(availableRewardsForEpochEth, 2),
[availableRewardsForEpochEth],
() => balanceFormat(eligibleRewardsThisEpochInEth, 2),
[eligibleRewardsThisEpochInEth],
);

// hook to set the flag to show the notification
useEffect(() => {
if (!isEligibleForRewards) return;
if (!storeState) return;
if (storeState?.firstRewardNotificationShown) return;
if (!availableRewardsForEpochEth) return;
if (!eligibleRewardsThisEpochInEth) return;

firstRewardRef.current = availableRewardsForEpochEth;
firstRewardRef.current = eligibleRewardsThisEpochInEth;
setCanShowNotification(true);
}, [isEligibleForRewards, availableRewardsForEpochEth, storeState]);
}, [isEligibleForRewards, eligibleRewardsThisEpochInEth, storeState]);

// hook to show desktop app notification
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const EarnedTagContainer = styled.div`

const DisplayRewards = () => {
const {
availableRewardsForEpochEth: reward,
eligibleRewardsThisEpochInEth: reward,
isEligibleForRewards,
isStakingRewardsDetailsLoading,
} = useRewardContext();
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/RewardsHistory/RewardsHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ const ContractRewards = ({
}: ContractRewardsProps) => {
const stakingProgramMeta =
STAKING_PROGRAMS[selectedAgentConfig.evmHomeChainId][stakingProgramId];
const { availableRewardsForEpochEth: reward, isEligibleForRewards } =
const { eligibleRewardsThisEpochInEth: reward, isEligibleForRewards } =
useRewardContext();

return (
Expand Down
6 changes: 3 additions & 3 deletions frontend/components/YourWalletPage/YourAgent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,19 +195,19 @@ const YourAgentWalletBreakdown = () => {
const { serviceSafe, middlewareChain, evmHomeChainId } = useYourWallet();

const {
availableRewardsForEpochEth,
eligibleRewardsThisEpochInEth,
isEligibleForRewards,
accruedServiceStakingRewards,
} = useRewardContext();

const reward = useMemo(() => {
if (!isLoaded) return <Skeleton.Input size="small" active />;
if (isEligibleForRewards) {
return `~${balanceFormat(availableRewardsForEpochEth, 2)} OLAS`;
return `~${balanceFormat(eligibleRewardsThisEpochInEth, 2)} OLAS`;
}

return 'Not yet earned';
}, [isLoaded, isEligibleForRewards, availableRewardsForEpochEth]);
}, [isLoaded, isEligibleForRewards, eligibleRewardsThisEpochInEth]);

const serviceSafeOlas = useMemo(
() =>
Expand Down
2 changes: 1 addition & 1 deletion frontend/constants/react-query-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const REACT_QUERY_KEYS = {
chainId: number,
) =>
[
'availableRewardsForEpoch',
'eligibleRewardsThisEpoch',
currentChainId,
serviceConfigId,
stakingProgramId,
Expand Down
102 changes: 78 additions & 24 deletions frontend/context/RewardProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useQuery } from '@tanstack/react-query';
import { formatUnits } from 'ethers/lib/utils';
import { isNil } from 'lodash';
import {
createContext,
Expand All @@ -13,26 +12,29 @@ import {
import { FIVE_SECONDS_INTERVAL } from '@/constants/intervals';
import { REACT_QUERY_KEYS } from '@/constants/react-query-keys';
import { useElectronApi } from '@/hooks/useElectronApi';
import { useOnlineStatusContext } from '@/hooks/useOnlineStatus';
import { useServices } from '@/hooks/useServices';
import { useStakingContractContext } from '@/hooks/useStakingContractDetails';
import { useStore } from '@/hooks/useStore';
import { StakingRewardsInfoSchema } from '@/types/Autonolas';
import { asMiddlewareChain } from '@/utils/middlewareHelpers';
import { formatEther, formatUnits } from '@/utils/numberFormatters';

import { OnlineStatusContext } from './OnlineStatusProvider';
import { StakingProgramContext } from './StakingProgramProvider';

export const RewardContext = createContext<{
isAvailableRewardsForEpochLoading?: boolean;
isEligibleRewardsThisEpochLoading?: boolean;
accruedServiceStakingRewards?: number;
availableRewardsForEpoch?: number;
availableRewardsForEpochEth?: number;
eligibleRewardsThisEpoch?: number;
eligibleRewardsThisEpochInEth?: number;
isEligibleForRewards?: boolean;
optimisticRewardsEarnedForEpoch?: number;
minimumStakedAmountRequired?: number;
updateRewards: () => Promise<void>;
isStakingRewardsDetailsLoading?: boolean;
}>({
isAvailableRewardsForEpochLoading: false,
isEligibleRewardsThisEpochLoading: false,
updateRewards: async () => {},
});

Expand Down Expand Up @@ -101,8 +103,8 @@ const useStakingRewardsDetails = () => {
/**
* hook to fetch available rewards for the current epoch
*/
const useAvailableRewardsForEpoch = () => {
const { isOnline } = useContext(OnlineStatusContext);
const useEligibleRewardsThisEpoch = () => {
const { isOnline } = useOnlineStatusContext();
const { selectedStakingProgramId } = useContext(StakingProgramContext);

const {
Expand Down Expand Up @@ -140,34 +142,86 @@ export const RewardProvider = ({ children }: PropsWithChildren) => {
const { storeState } = useStore();
const electronApi = useElectronApi();

const {
isSelectedStakingContractDetailsLoading,
selectedStakingContractDetails,
} = useStakingContractContext();

const {
data: stakingRewardsDetails,
refetch: refetchStakingRewardsDetails,
isLoading: isStakingRewardsDetailsLoading,
} = useStakingRewardsDetails();

const {
data: availableRewardsForEpoch,
isLoading: isAvailableRewardsForEpochLoading,
refetch: refetchAvailableRewardsForEpoch,
} = useAvailableRewardsForEpoch();
data: eligibleRewardsThisEpoch,
isLoading: isEligibleRewardsThisEpochLoading,
refetch: refetchEligibleRewardsThisEpoch,
} = useEligibleRewardsThisEpoch();

const isEligibleForRewards = stakingRewardsDetails?.isEligibleForRewards;
const accruedServiceStakingRewards =
stakingRewardsDetails?.accruedServiceStakingRewards;

// available rewards for the current epoch in ETH
const availableRewardsForEpochEth = useMemo<number | undefined>(() => {
if (!availableRewardsForEpoch) return;
return parseFloat(formatUnits(`${availableRewardsForEpoch}`));
}, [availableRewardsForEpoch]);
const rewardsPerSecond = stakingRewardsDetails?.rewardsPerSecond;
const serviceStakingStartTime =
selectedStakingContractDetails?.serviceStakingStartTime;

// available rewards for the epoch
const eligibleRewardsThisEpochInEth = useMemo<number | undefined>(() => {
if (!rewardsPerSecond) return;
if (!isEligibleForRewards) return;

// wait for the staking details to load
if (isStakingRewardsDetailsLoading) return;
if (isSelectedStakingContractDetailsLoading) return;
if (isEligibleRewardsThisEpochLoading) return;

// if agent is not staked, return the available rewards for the epoch
// i.e, agent has not yet started staking
if (isNil(serviceStakingStartTime) || serviceStakingStartTime === 0) {
return parseFloat(formatUnits(`${eligibleRewardsThisEpoch}`));
}

// calculate the next checkpoint timestamp
// i.e, next = last + checkpoint period
const nextCheckpointTimestamp =
stakingRewardsDetails.lastCheckpointTimestamp +
stakingRewardsDetails.livenessPeriod;

// default to the last checkpoint timestamp
// ie, if agent has not staked yet, use the last checkpoint timestamp
const rewardCountingStartTime = Math.max(
stakingRewardsDetails.lastCheckpointTimestamp,
serviceStakingStartTime || 0,
);

// calculate the time service staked in the current epoch
const stakingDurationInCurrentEpoch =
nextCheckpointTimestamp - rewardCountingStartTime;

const rewardsInCurrentEpoch =
parseFloat(formatEther(`${rewardsPerSecond}`)) *
stakingDurationInCurrentEpoch;

return parseFloat(`${rewardsInCurrentEpoch}`);
}, [
isEligibleForRewards,
isSelectedStakingContractDetailsLoading,
isStakingRewardsDetailsLoading,
isEligibleRewardsThisEpochLoading,
stakingRewardsDetails,
rewardsPerSecond,
serviceStakingStartTime,
eligibleRewardsThisEpoch,
]);

// optimistic rewards earned for the current epoch in ETH
const optimisticRewardsEarnedForEpoch = useMemo<number | undefined>(() => {
if (!isEligibleForRewards) return;
if (!availableRewardsForEpochEth) return;
return availableRewardsForEpochEth;
}, [availableRewardsForEpochEth, isEligibleForRewards]);
if (!eligibleRewardsThisEpochInEth) return;
return eligibleRewardsThisEpochInEth;
}, [eligibleRewardsThisEpochInEth, isEligibleForRewards]);

// store the first staking reward achieved in the store for notification
useEffect(() => {
Expand All @@ -183,8 +237,8 @@ export const RewardProvider = ({ children }: PropsWithChildren) => {
// refresh rewards data
const updateRewards = useCallback(async () => {
await refetchStakingRewardsDetails();
await refetchAvailableRewardsForEpoch();
}, [refetchStakingRewardsDetails, refetchAvailableRewardsForEpoch]);
await refetchEligibleRewardsThisEpoch();
}, [refetchStakingRewardsDetails, refetchEligibleRewardsThisEpoch]);

return (
<RewardContext.Provider
Expand All @@ -194,9 +248,9 @@ export const RewardProvider = ({ children }: PropsWithChildren) => {
accruedServiceStakingRewards,

// available rewards for the current epoch
isAvailableRewardsForEpochLoading,
availableRewardsForEpoch,
availableRewardsForEpochEth,
isEligibleRewardsThisEpochLoading,
eligibleRewardsThisEpoch,
eligibleRewardsThisEpochInEth,
isEligibleForRewards,
optimisticRewardsEarnedForEpoch,

Expand Down
4 changes: 2 additions & 2 deletions frontend/context/SharedProvider/useMainOlasBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const useMainOlasBalance = () => {
);
const {
isStakingRewardsDetailsLoading,
isAvailableRewardsForEpochLoading,
isEligibleRewardsThisEpochLoading,
optimisticRewardsEarnedForEpoch,
accruedServiceStakingRewards,
} = useRewardContext();
Expand Down Expand Up @@ -90,7 +90,7 @@ export const useMainOlasBalance = () => {
const isMainOlasBalanceLoading = [
!isBalanceLoaded,
isStakingRewardsDetailsLoading,
isAvailableRewardsForEpochLoading,
isEligibleRewardsThisEpochLoading,
!selectedStakingProgramId, // staking program is required to calculate staking rewards
].some(Boolean);

Expand Down
34 changes: 12 additions & 22 deletions frontend/service/agents/Modius.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,30 +59,19 @@ export abstract class ModiusService extends StakedAgentService {

const [
serviceInfo,
livenessPeriod,
rewardsPerSecond,
livenessPeriodInBn,
rewardsPerSecondInBn,
accruedStakingReward,
minStakingDeposit,
tsCheckpoint,
livenessRatio,
tsCheckpointInBn,
livenessRatioInBn,
currentMultisigNonces,
] = multicallResponse;

/**
* struct ServiceInfo {
// Service multisig address
address multisig;
// Service owner
address owner;
// Service multisig nonces
uint256[] nonces; <-- (we use this in the rewards eligibility check)
// Staking start time
uint256 tsStart;
// Accumulated service staking reward
uint256 reward;
// Accumulated inactivity that might lead to the service eviction
uint256 inactivity;}
*/
const rewardsPerSecond = rewardsPerSecondInBn.toNumber();
const livenessPeriod = livenessPeriodInBn.toNumber();
const tsCheckpoint = tsCheckpointInBn.toNumber();
const livenessRatio = livenessRatioInBn.toNumber();

const lastMultisigNonces = serviceInfo[2];
const nowInSeconds = Math.floor(Date.now() / 1000);
Expand All @@ -101,7 +90,7 @@ export abstract class ModiusService extends StakedAgentService {

const isEligibleForRewards = eligibleRequests >= requiredRequests;

const availableRewardsForEpoch = Math.max(
const eligibleRewardsThisEpoch = Math.max(
rewardsPerSecond * livenessPeriod, // expected rewards for the epoch
rewardsPerSecond * (nowInSeconds - tsCheckpoint), // incase of late checkpoint
);
Expand All @@ -117,12 +106,13 @@ export abstract class ModiusService extends StakedAgentService {
livenessRatio,
rewardsPerSecond,
isEligibleForRewards,
availableRewardsForEpoch,
eligibleRewardsThisEpoch,
accruedServiceStakingRewards: parseFloat(
ethers.utils.formatEther(`${accruedStakingReward}`),
),
minimumStakedAmount,
} as StakingRewardsInfo;
lastCheckpointTimestamp: tsCheckpoint,
} satisfies StakingRewardsInfo;
};

static getAvailableRewardsForEpoch = async (
Expand Down
Loading