Skip to content

Commit

Permalink
Merge branch 'refactor/multi-chain-support-frontend' into refactor/ba…
Browse files Browse the repository at this point in the history
…lance-provider-hooks
  • Loading branch information
truemiller authored Nov 14, 2024
2 parents 34f8853 + f17c0bd commit b5f9e38
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 81 deletions.
33 changes: 31 additions & 2 deletions frontend/constants/react-query-keys.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,37 @@
export const REACT_QUERY_KEYS = {
// service provider
// services
SERVICES_KEY: ['services'] as const,
SERVICE_DEPLOYMENT_STATUS_KEY: (serviceConfigId: string) =>
['serviceStatus', serviceConfigId] as const,
// wallet provider
// wallets
WALLETS_KEY: ['wallets'] as const,
// rewards
REWARDS_KEY: (
chainId: number,
serviceUuid: string,
stakingProgramId: string,
multisig: string,
token: number,
) =>
[
'rewards',
chainId,
serviceUuid,
stakingProgramId,
multisig,
token,
] as const,
AVAILABLE_REWARDS_FOR_EPOCH_KEY: (
currentChainId: number,
serviceConfigId: string,
stakingProgramId: string,
chainId: number,
) =>
[
'availableRewardsForEpoch',
currentChainId,
serviceConfigId,
stakingProgramId,
chainId,
] as const,
} as const;
180 changes: 112 additions & 68 deletions frontend/context/RewardProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import { ethers } from 'ethers';
import { useQuery } from '@tanstack/react-query';
import { formatUnits } from 'ethers/lib/utils';
import {
createContext,
PropsWithChildren,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { useInterval } from 'usehooks-ts';

import { CHAIN_CONFIG } from '@/config/chains';
import { AGENT_CONFIG } from '@/config/agents';
import { GNOSIS_CHAIN_CONFIG } from '@/config/chains';
import { FIVE_SECONDS_INTERVAL } from '@/constants/intervals';
import { REACT_QUERY_KEYS } from '@/constants/react-query-keys';
import { useElectronApi } from '@/hooks/useElectronApi';
import { useService } from '@/hooks/useService';
import { useServices } from '@/hooks/useServices';
import { useStore } from '@/hooks/useStore';
import { AutonolasService } from '@/service/Autonolas';
import { StakingRewardsInfoSchema } from '@/types/Autonolas';

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

export const RewardContext = createContext<{
Expand All @@ -38,88 +40,130 @@ export const RewardContext = createContext<{
updateRewards: async () => {},
});

export const RewardProvider = ({ children }: PropsWithChildren) => {
const currentAgent = AGENT_CONFIG.trader; // TODO: replace with dynamic agent selection
const currentChainId = GNOSIS_CHAIN_CONFIG.chainId; // TODO: replace with dynamic chain selection

/**
* hook to fetch staking rewards details
*/
const useStakingRewardsDetails = () => {
const { isOnline } = useContext(OnlineStatusContext);
const { activeStakingProgramId } = useContext(StakingProgramContext);

const { selectedService, isLoaded } = useServices();
const serviceConfigId =
isLoaded && selectedService ? selectedService?.service_config_id : '';
const { service } = useService({ serviceConfigId });

// fetch chain data from the selected service
const chainData = service?.chain_configs[currentChainId].chain_data;
const multisig = chainData?.multisig;
const token = chainData?.token;

return useQuery({
queryKey: REACT_QUERY_KEYS.REWARDS_KEY(
currentChainId,
serviceConfigId,
activeStakingProgramId!,
multisig!,
token!,
),
queryFn: async () => {
const response = await currentAgent.serviceApi.getAgentStakingRewardsInfo(
{
agentMultisigAddress: multisig!,
serviceId: token!,
stakingProgramId: activeStakingProgramId!,
chainId: currentChainId,
},
);
return StakingRewardsInfoSchema.parse(response);
},
enabled: !!isOnline && !!activeStakingProgramId && !!multisig && !!token,
refetchInterval: isOnline ? FIVE_SECONDS_INTERVAL : false,
refetchOnWindowFocus: false,
});
};

/**
* hook to fetch available rewards for the current epoch
*/
const useAvailableRewardsForEpoch = () => {
const { isOnline } = useContext(OnlineStatusContext);
const { services } = useContext(ServicesContext);
const service = useMemo(() => services?.[0], [services]);
const { storeState } = useStore();
const electronApi = useElectronApi();
const { activeStakingProgramId, defaultStakingProgramId } = useContext(
StakingProgramContext,
);
const { selectedService, isLoaded } = useServices();
const serviceConfigId =
isLoaded && selectedService ? selectedService?.service_config_id : '';

return useQuery({
queryKey: REACT_QUERY_KEYS.AVAILABLE_REWARDS_FOR_EPOCH_KEY(
currentChainId,
serviceConfigId,
activeStakingProgramId!,
currentChainId,
),
queryFn: async () => {
return await currentAgent.serviceApi.getAvailableRewardsForEpoch(
activeStakingProgramId ?? defaultStakingProgramId,
currentChainId,
);
},
enabled:
!!isOnline && !!activeStakingProgramId && !!defaultStakingProgramId,
refetchInterval: isOnline ? FIVE_SECONDS_INTERVAL : false,
refetchOnWindowFocus: false,
});
};

const [accruedServiceStakingRewards, setAccruedServiceStakingRewards] =
useState<number>();
const [availableRewardsForEpoch, setAvailableRewardsForEpoch] =
useState<number>();
const [isEligibleForRewards, setIsEligibleForRewards] = useState<boolean>();
/**
* Provider to manage rewards context
*/
export const RewardProvider = ({ children }: PropsWithChildren) => {
const { storeState } = useStore();
const electronApi = useElectronApi();

const availableRewardsForEpochEth = useMemo<number | undefined>(() => {
if (!availableRewardsForEpoch) return;
const { data: stakingRewardsDetails, refetch: refetchStakingRewardsDetails } =
useStakingRewardsDetails();
const {
data: availableRewardsForEpoch,
refetch: refetchAvailableRewardsForEpoch,
} = useAvailableRewardsForEpoch();

const formatRewardsEth = parseFloat(
ethers.utils.formatUnits(`${availableRewardsForEpoch}`, 18),
);
const isEligibleForRewards = stakingRewardsDetails?.isEligibleForRewards;
const accruedServiceStakingRewards =
stakingRewardsDetails?.accruedServiceStakingRewards;

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

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

const updateRewards = useCallback(async (): Promise<void> => {
let stakingRewardsInfoPromise;

// only check for rewards if there's a currentStakingProgram active
if (
activeStakingProgramId &&
service?.chain_configs[CHAIN_CONFIG.OPTIMISM.chainId].chain_data?.multisig &&
service?.chain_configs[CHAIN_CONFIG.OPTIMISM.chainId].chain_data?.token
) {
stakingRewardsInfoPromise = AutonolasService.getAgentStakingRewardsInfo({
agentMultisigAddress:
service.chain_configs[CHAIN_CONFIG.OPTIMISM.chainId].chain_data.multisig!,
serviceId:
service.chain_configs[CHAIN_CONFIG.OPTIMISM.chainId].chain_data.token!,
stakingProgram: activeStakingProgramId,
});
}

// can fallback to default staking program if no current staking program is active
const epochRewardsPromise = AutonolasService.getAvailableRewardsForEpoch(
activeStakingProgramId ?? defaultStakingProgramId,
);

const [stakingRewardsInfo, rewards] = await Promise.all([
stakingRewardsInfoPromise,
epochRewardsPromise,
]);

setIsEligibleForRewards(stakingRewardsInfo?.isEligibleForRewards);
setAccruedServiceStakingRewards(
stakingRewardsInfo?.accruedServiceStakingRewards,
);
setAvailableRewardsForEpoch(rewards);
}, [activeStakingProgramId, defaultStakingProgramId, service]);

// store the first staking reward achieved in the store for notification
useEffect(() => {
if (isEligibleForRewards && !storeState?.firstStakingRewardAchieved) {
electronApi.store?.set?.('firstStakingRewardAchieved', true);
}
if (!isEligibleForRewards) return;
if (storeState?.firstStakingRewardAchieved) return;
electronApi.store?.set?.('firstStakingRewardAchieved', true);
}, [
electronApi.store,
isEligibleForRewards,
storeState?.firstStakingRewardAchieved,
]);

useInterval(
async () => updateRewards(),
isOnline ? FIVE_SECONDS_INTERVAL : null,
);
// refresh rewards data
const updateRewards = useCallback(async () => {
await refetchStakingRewardsDetails();
await refetchAvailableRewardsForEpoch();
}, [refetchStakingRewardsDetails, refetchAvailableRewardsForEpoch]);

return (
<RewardContext.Provider
Expand Down
26 changes: 15 additions & 11 deletions frontend/types/Autonolas.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
export type StakingRewardsInfo = {
// mechRequestCount: number;
serviceInfo: unknown[];
livenessPeriod: number;
livenessRatio: number;
rewardsPerSecond: number;
isEligibleForRewards: boolean;
availableRewardsForEpoch: number;
accruedServiceStakingRewards: number;
minimumStakedAmount: number;
};
import { z } from 'zod';

export const StakingRewardsInfoSchema = z.object({
// mechRequestCount: z.number(),
serviceInfo: z.array(z.unknown()),
livenessPeriod: z.number(),
livenessRatio: z.number(),
rewardsPerSecond: z.number(),
isEligibleForRewards: z.boolean(),
availableRewardsForEpoch: z.number(),
accruedServiceStakingRewards: z.number(),
minimumStakedAmount: z.number(),
});

export type StakingRewardsInfo = z.infer<typeof StakingRewardsInfoSchema>;

export type StakingContractInfo = {
availableRewards: number;
Expand Down

0 comments on commit b5f9e38

Please sign in to comment.