Skip to content

Commit

Permalink
feat: Provide the user with the time information regarding when the c…
Browse files Browse the repository at this point in the history
…urrent epoch ends and how long until an eviction expires. (#380)

* feat: eviction time text

* feat: add logic

* feat: add epoch end time

* feat: move to separate component

* feat: personal review

* feat: minor renmae

* refactor: Remove unused import and useMemo in StakingRewardsThisEpoch.tsx
  • Loading branch information
mohandast52 authored Oct 4, 2024
1 parent 1433fc0 commit aca3ee9
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 46 deletions.
44 changes: 33 additions & 11 deletions frontend/components/MainPage/header/CannotStartAgentPopover.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { Popover, PopoverProps, Typography } from 'antd';
import { Flex, Popover, PopoverProps, Typography } from 'antd';

import { COLOR } from '@/constants/colors';
import { UNICODE_SYMBOLS } from '@/constants/symbols';
import { SUPPORT_URL } from '@/constants/urls';
import { useStakingContractInfo } from '@/hooks/useStakingContractInfo';
import { formatToShortDateTime } from '@/utils/time';

const { Paragraph, Text } = Typography;

Expand Down Expand Up @@ -42,16 +43,37 @@ export const CannotStartAgentDueToUnexpectedError = () => (
);

const evictedDescription =
"You didn't run your agent enough and it missed its targets multiple times. Please wait a few days and try to run your agent again.";
const AgentEvictedPopover = () => (
<Popover
{...otherPopoverProps}
title="Your agent is suspended from work"
content={<div style={{ maxWidth: 340 }}>{evictedDescription}</div>}
>
{cannotStartAgentText}
</Popover>
);
"You didn't run your agent enough and it missed its targets multiple times. You can run the agent again when the eviction period ends.";
const AgentEvictedPopover = () => {
const { evictionExpiresAt } = useStakingContractInfo();

return (
<Popover
{...otherPopoverProps}
title="Your agent is evicted"
content={
<Flex
vertical
gap={8}
className="text-sm-all"
style={{ maxWidth: 340 }}
>
<Paragraph className="text-sm m-0">{evictedDescription}</Paragraph>
{evictionExpiresAt && (
<Paragraph className="m-0">
<Text className="text-sm">Eviction ends at</Text>{' '}
<Text strong className="text-sm">
{formatToShortDateTime(evictionExpiresAt * 1000)}
</Text>
</Paragraph>
)}
</Flex>
}
>
{cannotStartAgentText}
</Popover>
);
};

const JoinOlasCommunity = () => (
<div style={{ maxWidth: 340 }}>
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/MainPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { KeepAgentRunningSection } from './sections/KeepAgentRunningSection';
import { MainNeedsFunds } from './sections/NeedsFundsSection';
import { NewStakingProgramAlertSection } from './sections/NewStakingProgramAlertSection';
import { MainOlasBalance } from './sections/OlasBalanceSection';
import { MainRewards } from './sections/RewardsSection';
import { MainRewards } from './sections/RewardsSection/RewardsSection';
import { StakingContractUpdate } from './sections/StakingContractUpdate';

export const Main = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { InfoCircleOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Flex, Modal, Skeleton, Tag, Tooltip, Typography } from 'antd';
import { RightOutlined } from '@ant-design/icons';
import { Button, Flex, Modal, Skeleton, Tag, Typography } from 'antd';
import Image from 'next/image';
import { useCallback, useEffect, useRef, useState } from 'react';

Expand All @@ -11,10 +11,11 @@ import { useReward } from '@/hooks/useReward';
import { useStore } from '@/hooks/useStore';
import { balanceFormat } from '@/utils/numberFormatters';

import { ConfettiAnimation } from '../../Confetti/ConfettiAnimation';
import { CardSection } from '../../styled/CardSection';
import { ConfettiAnimation } from '../../../Confetti/ConfettiAnimation';
import { CardSection } from '../../../styled/CardSection';
import { StakingRewardsThisEpoch } from './StakingRewardsThisEpoch';

const { Text, Title, Paragraph } = Typography;
const { Text, Title } = Typography;

const Loader = () => (
<Flex vertical gap={8}>
Expand All @@ -35,20 +36,7 @@ const DisplayRewards = () => {

return (
<CardSection vertical gap={8} padding="16px 24px" align="start">
<Text type="secondary">
Staking rewards this epoch&nbsp;
<Tooltip
arrow={false}
title={
<Paragraph className="text-sm m-0">
The agent&apos;s working period lasts at least 24 hours, but its
start and end point may not be at the same time every day.
</Paragraph>
}
>
<InfoCircleOutlined />
</Tooltip>
</Text>
<StakingRewardsThisEpoch />

{isBalanceLoaded ? (
<Flex align="center" gap={12}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { useQuery } from '@tanstack/react-query';
import { Popover, Typography } from 'antd';
import { gql, request } from 'graphql-request';
import { z } from 'zod';

import { SUBGRAPH_URL } from '@/constants/urls';
import { useStakingProgram } from '@/hooks/useStakingProgram';
import { formatToTime } from '@/utils/time';

const { Text } = Typography;

const EpochTimeResponseSchema = z.object({
epochLength: z.string(),
blockTimestamp: z.string(),
});
type EpochTimeResponse = z.infer<typeof EpochTimeResponseSchema>;

const useEpochEndTime = () => {
const { activeStakingProgramAddress } = useStakingProgram();
const latestEpochTimeQuery = gql`
query {
checkpoints(
orderBy: epoch
orderDirection: desc
first: 1
where: {
contractAddress: "${activeStakingProgramAddress}"
}
) {
epochLength
blockTimestamp
}
}
`;

const { data, isLoading } = useQuery({
queryKey: ['latestEpochTime'],
queryFn: async () => {
const response = (await request(SUBGRAPH_URL, latestEpochTimeQuery)) as {
checkpoints: EpochTimeResponse[];
};
return EpochTimeResponseSchema.parse(response.checkpoints[0]);
},
select: (data) => {
// last epoch end time + epoch length
return Number(data.blockTimestamp) + Number(data.epochLength);
},
enabled: !!activeStakingProgramAddress,
});

return { data, isLoading };
};

export const StakingRewardsThisEpoch = () => {
const { data: epochEndTimeInMs } = useEpochEndTime();

return (
<Text type="secondary">
Staking rewards this epoch&nbsp;
<Popover
arrow={false}
content={
<>
The epoch ends each day at ~{' '}
<Text className="text-sm" strong>
{epochEndTimeInMs
? `${formatToTime(epochEndTimeInMs * 1000)} (UTC)`
: '--'}
</Text>
</>
}
>
<InfoCircleOutlined />
</Popover>
</Text>
);
};
4 changes: 1 addition & 3 deletions frontend/components/RewardsHistory/useRewardsHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { z } from 'zod';
import { Chain } from '@/client';
import { SERVICE_STAKING_TOKEN_MECH_USAGE_CONTRACT_ADDRESSES } from '@/constants/contractAddresses';
import { STAKING_PROGRAM_META } from '@/constants/stakingProgramMeta';
import { SUBGRAPH_URL } from '@/constants/urls';
import { StakingProgramId } from '@/enums/StakingProgram';
import { useServices } from '@/hooks/useServices';

Expand All @@ -30,9 +31,6 @@ const beta2Address =
SERVICE_STAKING_TOKEN_MECH_USAGE_CONTRACT_ADDRESSES[Chain.GNOSIS]
.pearl_beta_2;

const SUBGRAPH_URL =
'https://api.studio.thegraph.com/query/81855/pearl-staking-rewards-history/version/latest';

const fetchRewardsQuery = gql`
{
allRewards: checkpoints(orderBy: epoch, orderDirection: desc) {
Expand Down
3 changes: 3 additions & 0 deletions frontend/constants/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export const COW_SWAP_GNOSIS_XDAI_OLAS_URL: string =

export const FAQ_URL = 'https://olas.network/operate#faq';

export const SUBGRAPH_URL =
'https://api.studio.thegraph.com/query/81855/pearl-staking-rewards-history/version/latest';

// discord
export const SUPPORT_URL =
'https://discord.com/channels/899649805582737479/1244588374736502847';
Expand Down
7 changes: 5 additions & 2 deletions frontend/hooks/useStakingContractInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ export const useStakingContractInfo = () => {
serviceIds.length < maxNumServices;

const hasEnoughRewardsAndSlots = isRewardsAvailable && hasEnoughServiceSlots;

const isAgentEvicted = serviceStakingState === 2;

const isServiceStaked =
!!serviceStakingStartTime && serviceStakingState === 1;

Expand Down Expand Up @@ -78,10 +76,15 @@ export const useStakingContractInfo = () => {
!isNil(hasEnoughRewardsAndSlots) &&
(isAgentEvicted ? isServiceStakedForMinimumDuration : true);

// Eviction expire time in seconds
const evictionExpiresAt =
(serviceStakingStartTime ?? 0) + (minimumStakingDuration ?? 0);

return {
activeStakingContractInfo,
hasEnoughServiceSlots,
isAgentEvicted,
evictionExpiresAt,
isEligibleForStaking,
isPaused,
isRewardsAvailable,
Expand Down
12 changes: 11 additions & 1 deletion frontend/hooks/useStakingProgram.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useContext, useMemo } from 'react';

import { Chain } from '@/client';
import { SERVICE_STAKING_TOKEN_MECH_USAGE_CONTRACT_ADDRESSES } from '@/constants/contractAddresses';
import { STAKING_PROGRAM_META } from '@/constants/stakingProgramMeta';
import { StakingProgramContext } from '@/context/StakingProgramContext';

Expand All @@ -23,16 +25,24 @@ export const useStakingProgram = () => {
* returns `null` if not actively staked
*/
const activeStakingProgramMeta = useMemo(() => {
if (activeStakingProgramId === undefined) return undefined;
if (activeStakingProgramId === undefined) return;
if (activeStakingProgramId === null) return null;
return STAKING_PROGRAM_META[activeStakingProgramId];
}, [activeStakingProgramId]);

const defaultStakingProgramMeta =
STAKING_PROGRAM_META[defaultStakingProgramId];

const activeStakingProgramAddress = useMemo(() => {
if (!activeStakingProgramId) return;
return SERVICE_STAKING_TOKEN_MECH_USAGE_CONTRACT_ADDRESSES[Chain.GNOSIS][
activeStakingProgramId
];
}, [activeStakingProgramId]);

return {
activeStakingProgramId,
activeStakingProgramAddress,
activeStakingProgramMeta,
defaultStakingProgramId,
defaultStakingProgramMeta,
Expand Down
4 changes: 1 addition & 3 deletions frontend/styles/globals.scss
Original file line number Diff line number Diff line change
Expand Up @@ -172,18 +172,16 @@ button, input, select, textarea, .ant-input-suffix {
margin-right: auto !important;
}

// font size
.text-xl {
font-size: 20px;
}

.text-base {
font-size: 16px !important;
}

.text-sm {
font-size: 14px !important;
}

.text-xs {
font-size: 12px !important;
}
Expand Down
3 changes: 3 additions & 0 deletions frontend/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export const mainTheme: ThemeConfig = {
Typography: {
colorTextDescription: '#4D596A',
},
Popover: {
fontSize: 14,
},
Tag: {
colorSuccess: '#135200',
},
Expand Down
26 changes: 20 additions & 6 deletions frontend/utils/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,36 @@ export const getTimeAgo = (timestampInSeconds: number) => {
* @returns formatted date in the format of 'MMM DD'
* @example 1626825600 => 'Jul 21'
*/
export const formatToMonthDay = (timeInSeconds: number) => {
if (!isNumber(timeInSeconds)) return '--';
return new Date(timeInSeconds).toLocaleDateString('en-US', {
export const formatToMonthDay = (timeInMs: number) => {
if (!isNumber(timeInMs)) return '--';
return new Date(timeInMs).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
};

/**
* @returns formatted time in the format of 'HH:MM AM/PM'
* @example 1626825600 => '12:00 PM'
*/
export const formatToTime = (timeInMs: number) => {
if (!isNumber(timeInMs)) return '--';
return new Date(timeInMs).toLocaleTimeString('en-US', {
hour: 'numeric',
minute: 'numeric',
hour12: true,
timeZone: 'UTC',
});
};

/**
*
* @returns formatted date and time in the format of 'MMM DD, HH:MM AM/PM'
* @example 1626825600 => 'Jul 21, 12:00 PM'
*/
export const formatToShortDateTime = (timeInSeconds?: number) => {
if (!isNumber(timeInSeconds)) return '--';
return new Date(timeInSeconds).toLocaleDateString('en-US', {
export const formatToShortDateTime = (timeInMs?: number) => {
if (!isNumber(timeInMs)) return '--';
return new Date(timeInMs).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: 'numeric',
Expand Down

0 comments on commit aca3ee9

Please sign in to comment.