diff --git a/frontend/components/Main/Main.tsx b/frontend/components/Main/Main.tsx
index 84c575d53..81864ae22 100644
--- a/frontend/components/Main/Main.tsx
+++ b/frontend/components/Main/Main.tsx
@@ -8,7 +8,7 @@ import { useBalance, usePageState, useServices } from '@/hooks';
import { KeepAgentRunning } from './KeepAgentRunning';
import { MainAddFunds } from './MainAddFunds';
import { MainGasBalance } from './MainGasBalance';
-import { MainHeader } from './MainHeader';
+import { MainHeader } from './MainHeader/MainHeader';
import { MainNeedsFunds } from './MainNeedsFunds';
import { MainOlasBalance } from './MainOlasBalance';
import { MainRewards } from './MainRewards';
diff --git a/frontend/components/Main/MainHeader/CannotStartAgent.tsx b/frontend/components/Main/MainHeader/CannotStartAgent.tsx
new file mode 100644
index 000000000..4e37f4227
--- /dev/null
+++ b/frontend/components/Main/MainHeader/CannotStartAgent.tsx
@@ -0,0 +1,80 @@
+import { InfoCircleOutlined } from '@ant-design/icons';
+import { Popover, PopoverProps, Typography } from 'antd';
+
+import { COLOR, SUPPORT_URL } from '@/constants';
+import { UNICODE_SYMBOLS } from '@/constants/unicode';
+import { useStakingContractInfo } from '@/hooks/useStakingContractInfo';
+
+const { Paragraph, Text } = Typography;
+
+const cannotStartAgentText = (
+
+ Cannot start agent
+
+
+);
+
+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 = () => (
+ {evictedDescription}}
+ >
+ {cannotStartAgentText}
+
+);
+
+const otherPopoverProps: PopoverProps = {
+ arrow: false,
+ placement: 'bottomRight',
+};
+
+const JoinOlasCommunity = () => (
+
+);
+
+const NoRewardsAvailablePopover = () => (
+ }
+ >
+ {cannotStartAgentText}
+
+);
+
+const NoJobsAvailablePopover = () => (
+ }
+ >
+ {cannotStartAgentText}
+
+);
+
+export const CannotStartAgent = () => {
+ const {
+ canStartAgent,
+ hasEnoughServiceSlots,
+ isRewardsAvailable,
+ isAgentEvicted,
+ } = useStakingContractInfo();
+
+ if (canStartAgent) return null;
+ if (!hasEnoughServiceSlots) return ;
+ if (!isRewardsAvailable) return ;
+ if (isAgentEvicted) return ;
+ throw new Error('Cannot start agent, please contact support');
+};
diff --git a/frontend/components/Main/MainHeader/FirstRunModal.tsx b/frontend/components/Main/MainHeader/FirstRunModal.tsx
new file mode 100644
index 000000000..50b06f92b
--- /dev/null
+++ b/frontend/components/Main/MainHeader/FirstRunModal.tsx
@@ -0,0 +1,52 @@
+import { Button, Flex, Modal, Typography } from 'antd';
+import Image from 'next/image';
+import { FC } from 'react';
+
+import { useReward } from '@/hooks/useReward';
+
+const { Title, Paragraph } = Typography;
+
+type FirstRunModalProps = { open: boolean; onClose: () => void };
+
+export const FirstRunModal: FC = ({ open, onClose }) => {
+ const { minimumStakedAmountRequired } = useReward();
+
+ if (!open) return null;
+ return (
+
+ Got it
+ ,
+ ]}
+ >
+
+
+
+
+ {`Your agent is running and you've staked ${minimumStakedAmountRequired} OLAS!`}
+
+ Your agent is working towards earning rewards.
+
+ Pearl is designed to make it easy for you to earn staking rewards every
+ day. Simply leave the app and agent running in the background for ~1hr a
+ day.
+
+
+ );
+};
diff --git a/frontend/components/Main/MainHeader.tsx b/frontend/components/Main/MainHeader/MainHeader.tsx
similarity index 75%
rename from frontend/components/Main/MainHeader.tsx
rename to frontend/components/Main/MainHeader/MainHeader.tsx
index d00be73d1..24c971486 100644
--- a/frontend/components/Main/MainHeader.tsx
+++ b/frontend/components/Main/MainHeader/MainHeader.tsx
@@ -1,24 +1,47 @@
import { InfoCircleOutlined } from '@ant-design/icons';
-import { Badge, Button, Flex, Modal, Popover, Typography } from 'antd';
-import { formatUnits } from 'ethers/lib/utils';
+import { Badge, Button, Flex, Popover, Typography } from 'antd';
import Image from 'next/image';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Chain, DeploymentStatus } from '@/client';
-import { COLOR, LOW_BALANCE, SERVICE_TEMPLATES } from '@/constants';
+import { COLOR, LOW_BALANCE } from '@/constants';
import { useBalance, useServiceTemplates } from '@/hooks';
import { useElectronApi } from '@/hooks/useElectronApi';
import { useReward } from '@/hooks/useReward';
import { useServices } from '@/hooks/useServices';
+import { useStakingContractInfo } from '@/hooks/useStakingContractInfo';
import { useStore } from '@/hooks/useStore';
import { useWallet } from '@/hooks/useWallet';
import { ServicesService } from '@/service';
import { WalletService } from '@/service/Wallet';
-const { Text, Title, Paragraph } = Typography;
+import { CannotStartAgent } from './CannotStartAgent';
+import { requiredGas, requiredOlas } from './constants';
+import { FirstRunModal } from './FirstRunModal';
+
+const { Text } = Typography;
const LOADING_MESSAGE =
'Starting the agent may take a while, so feel free to minimize the app. We’ll notify you once it’s running. Please, don’t quit the app.';
+const StartingButtonPopover = () => (
+
+
+
+
+ {LOADING_MESSAGE}
+
+ }
+ >
+
+
+);
enum ServiceButtonLoadingState {
Starting,
@@ -26,59 +49,28 @@ enum ServiceButtonLoadingState {
NotLoading,
}
-const FirstRunModal = ({
- open,
- onClose,
-}: {
- open: boolean;
- onClose: () => void;
-}) => {
- const { minimumStakedAmountRequired } = useReward();
+const useSetupTrayIcon = () => {
+ const { safeBalance } = useBalance();
+ const { serviceStatus } = useServices();
+ const { setTrayIcon } = useElectronApi();
- if (!open) return null;
- return (
-
- Got it
- ,
- ]}
- >
-
-
-
-
- {`Your agent is running and you've staked ${minimumStakedAmountRequired} OLAS!`}
-
- Your agent is working towards earning rewards.
-
- Pearl is designed to make it easy for you to earn staking rewards every
- day. Simply leave the app and agent running in the background for ~1hr a
- day.
-
-
- );
+ useEffect(() => {
+ if (safeBalance && safeBalance.ETH < LOW_BALANCE) {
+ setTrayIcon?.('low-gas');
+ } else if (serviceStatus === DeploymentStatus.DEPLOYED) {
+ setTrayIcon?.('running');
+ } else if (serviceStatus === DeploymentStatus.STOPPED) {
+ setTrayIcon?.('paused');
+ }
+ }, [safeBalance, serviceStatus, setTrayIcon]);
+
+ return null;
};
export const MainHeader = () => {
const { storeState } = useStore();
const { services, serviceStatus, setServiceStatus } = useServices();
- const { showNotification, setTrayIcon } = useElectronApi();
+ const { showNotification } = useElectronApi();
const { getServiceTemplates } = useServiceTemplates();
const { wallets, masterSafeAddress } = useWallet();
const {
@@ -94,6 +86,12 @@ export const MainHeader = () => {
const { minimumStakedAmountRequired } = useReward();
+ const { isStakingContractInfoLoading, canStartAgent } =
+ useStakingContractInfo();
+
+ // hook to setup tray icon
+ useSetupTrayIcon();
+
const safeOlasBalanceWithStaked = useMemo(() => {
if (safeBalance?.OLAS === undefined) return;
if (totalOlasStakedBalance === undefined) return;
@@ -108,16 +106,6 @@ export const MainHeader = () => {
[getServiceTemplates],
);
- useEffect(() => {
- if (safeBalance && safeBalance.ETH < LOW_BALANCE) {
- setTrayIcon?.('low-gas');
- } else if (serviceStatus === DeploymentStatus.DEPLOYED) {
- setTrayIcon?.('running');
- } else if (serviceStatus === DeploymentStatus.STOPPED) {
- setTrayIcon?.('paused');
- }
- }, [safeBalance, serviceStatus, setTrayIcon]);
-
const agentHead = useMemo(() => {
if (
serviceButtonState === ServiceButtonLoadingState.Starting ||
@@ -227,25 +215,7 @@ export const MainHeader = () => {
}
if (serviceButtonState === ServiceButtonLoadingState.Starting) {
- return (
-
-
-
-
- {LOADING_MESSAGE}
-
- }
- >
-
-
- );
+ return ;
}
if (serviceStatus === DeploymentStatus.DEPLOYED) {
@@ -272,29 +242,6 @@ export const MainHeader = () => {
);
}
- const olasCostOfBond = Number(
- formatUnits(
- `${SERVICE_TEMPLATES[0].configuration.olas_cost_of_bond}`,
- 18,
- ),
- );
-
- const olasRequiredToStake = Number(
- formatUnits(
- `${SERVICE_TEMPLATES[0].configuration.olas_required_to_stake}`,
- 18,
- ),
- );
-
- const requiredOlas = olasCostOfBond + olasRequiredToStake;
-
- const requiredGas = Number(
- formatUnits(
- `${SERVICE_TEMPLATES[0].configuration.monthly_gas_estimate}`,
- 18,
- ),
- );
-
const isDeployable = (() => {
// case where required values are undefined (not fetched from the server)
if (totalEthBalance === undefined) return false;
@@ -327,7 +274,12 @@ export const MainHeader = () => {
}
return (
-