Skip to content

Commit

Permalink
Merge pull request #439 from valory-xyz/refactor/services-context-hooks
Browse files Browse the repository at this point in the history
Optimistic Services refactor, and small QoL changes to AgentButton, numberFormatters
  • Loading branch information
truemiller authored Nov 13, 2024
2 parents e917cb1 + 7395560 commit 7467418
Show file tree
Hide file tree
Showing 24 changed files with 535 additions and 385 deletions.
2 changes: 1 addition & 1 deletion frontend/abis/erc20.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Abi } from '@/types/Abi';

export const ERC20_BALANCEOF_STRING_FRAGMENT: Abi = [
export const ERC20_BALANCE_OF_STRING_FRAGMENT: Abi = [
'function balanceOf(address owner) view returns (uint256)',
];
5 changes: 2 additions & 3 deletions frontend/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,15 @@ export type ChainData = {
};

export type MiddlewareServiceResponse = {
description: string;
service_config_id: string; // TODO: update with uuid once middleware integrated
name: string;
hash: string;
hash_history: {
[block: string]: string;
};
home_chain_id: number;
keys: ServiceKeys[];
name: string;
service_path?: string;
service_config_id: string;
version: string;
chain_configs: {
[chainId: number]: {
Expand Down
59 changes: 59 additions & 0 deletions frontend/components/MainPage/header/AgentButton/AgentButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Button } from 'antd';
import { useMemo } from 'react';

import { MiddlewareDeploymentStatus } from '@/client';
import { useService } from '@/hooks/useService';
import { useServices } from '@/hooks/useServices';
import { useStakingContractInfo } from '@/hooks/useStakingContractInfo';

import {
CannotStartAgentDueToUnexpectedError,
CannotStartAgentPopover,
} from '../CannotStartAgentPopover';
import { AgentNotRunningButton } from './AgentNotRunningButton';
import { AgentRunningButton } from './AgentRunningButton';
import { AgentStartingButton } from './AgentStartingButton';
import { AgentStoppingButton } from './AgentStoppingButton';

export const AgentButton = () => {
const { selectedService } = useServices();
const {
service,
deploymentStatus: serviceStatus,
isLoaded,
} = useService({ serviceConfigId: selectedService?.service_config_id });
const { isEligibleForStaking, isAgentEvicted } = useStakingContractInfo();

return useMemo(() => {
if (!isLoaded) {
return <Button type="primary" size="large" disabled loading />;
}

if (serviceStatus === MiddlewareDeploymentStatus.STOPPING) {
return <AgentStoppingButton />;
}

if (serviceStatus === MiddlewareDeploymentStatus.DEPLOYING) {
return <AgentStartingButton />;
}

if (serviceStatus === MiddlewareDeploymentStatus.DEPLOYED) {
return <AgentRunningButton />;
}

if (!isEligibleForStaking && isAgentEvicted)
return <CannotStartAgentPopover />;

if (
!service ||
serviceStatus === MiddlewareDeploymentStatus.STOPPED ||
serviceStatus === MiddlewareDeploymentStatus.CREATED ||
serviceStatus === MiddlewareDeploymentStatus.BUILT ||
serviceStatus === MiddlewareDeploymentStatus.DELETED
) {
return <AgentNotRunningButton />;
}

return <CannotStartAgentDueToUnexpectedError />;
}, [isLoaded, serviceStatus, isEligibleForStaking, isAgentEvicted, service]);
};
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { Button, ButtonProps, Flex, Popover, Tooltip, Typography } from 'antd';
import { Button, ButtonProps } from 'antd';
import { useCallback, useMemo } from 'react';

import { MiddlewareChain, MiddlewareDeploymentStatus } from '@/client';
import { COLOR } from '@/constants/colors';
import { STAKING_PROGRAMS } from '@/config/stakingPrograms';
import { DEFAULT_STAKING_PROGRAM_ID } from '@/context/StakingProgramProvider';
import { ChainId } from '@/enums/Chain';
import { StakingProgramId } from '@/enums/StakingProgram';
import { useBalance } from '@/hooks/useBalance';
import { useElectronApi } from '@/hooks/useElectronApi';
import { useReward } from '@/hooks/useReward';
import { useService } from '@/hooks/useService';
import { useServices } from '@/hooks/useServices';
import { useServiceTemplates } from '@/hooks/useServiceTemplates';
import {
Expand All @@ -24,116 +23,22 @@ import { ServicesService } from '@/service/Services';
import { WalletService } from '@/service/Wallet';
import { delayInSeconds } from '@/utils/delay';

import {
CannotStartAgentDueToUnexpectedError,
CannotStartAgentPopover,
} from './CannotStartAgentPopover';
import { requiredGas } from './constants';
import { LastTransaction } from './LastTransaction';

const { Text, Paragraph } = 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 IdleTooltip = () => (
<Tooltip
placement="bottom"
arrow={false}
title={
<Paragraph className="text-sm m-0">
Your agent earned rewards for this epoch, so decided to stop working
until the next epoch.
</Paragraph>
}
>
<InfoCircleOutlined />
</Tooltip>
);

const AgentStartingButton = () => (
<Popover
trigger={['hover', 'click']}
placement="bottomLeft"
showArrow={false}
content={
<Flex vertical={false} gap={8} style={{ maxWidth: 260 }}>
<Text>
<InfoCircleOutlined style={{ color: COLOR.BLUE }} />
</Text>
<Text>{LOADING_MESSAGE}</Text>
</Flex>
}
>
<Button type="default" size="large" ghost disabled loading>
Starting...
</Button>
</Popover>
);

const AgentStoppingButton = () => (
<Button type="default" size="large" ghost disabled loading>
Stopping...
</Button>
);

const AgentRunningButton = () => {
const { showNotification } = useElectronApi();
const { isEligibleForRewards } = useReward();
const { service, setIsServicePollingPaused, setServiceStatus } =
useServices();

const handlePause = useCallback(async () => {
if (!service) return;
// Paused to stop overlapping service poll while waiting for response
setIsServicePollingPaused(true);

// Optimistically update service status
setServiceStatus(MiddlewareDeploymentStatus.STOPPING);
try {
await ServicesService.stopDeployment(service.service_config_id);
} catch (error) {
console.error(error);
showNotification?.('Some error occurred while stopping agent');
} finally {
// Resume polling, will update to correct status regardless of success
setIsServicePollingPaused(false);
}
}, [service, setIsServicePollingPaused, setServiceStatus, showNotification]);

return (
<Flex gap={10} align="center">
<Button type="default" size="large" onClick={handlePause}>
Pause
</Button>

<Flex vertical>
{isEligibleForRewards ? (
<Text type="secondary" className="text-sm">
Agent is idle&nbsp;
<IdleTooltip />
</Text>
) : (
<Text type="secondary" className="text-sm loading-ellipses">
Agent is working
</Text>
)}
<LastTransaction />
</Flex>
</Flex>
);
};

/** Button used to start / deploy the agent */
const AgentNotRunningButton = () => {
export const AgentNotRunningButton = () => {
const { wallets, masterSafeAddress } = useWallet();

const {
service,
serviceStatus,
setServiceStatus,
setIsServicePollingPaused,
updateServicesState,
selectedService,
setPaused: setIsServicePollingPaused,
isLoaded,
refetch: updateServicesState,
} = useServices();

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

const { serviceTemplate } = useServiceTemplates();
const { showNotification } = useElectronApi();
const {
Expand Down Expand Up @@ -166,10 +71,8 @@ const AgentNotRunningButton = () => {
// stakingContractInfoRecord?.[activeStakingProgram ?? defaultStakingProgram]
// ?.minStakingDeposit;

const requiredOlas = getMinimumStakedAmountRequired(
serviceTemplate,
activeStakingProgramId ?? DEFAULT_STAKING_PROGRAM_ID,
);
const requiredOlas =
STAKING_PROGRAMS[activeStakingProgramId]?.minStakingDeposit; // TODO: fix activeStakingProgramId

const safeOlasBalance = safeBalance?.OLAS;
const safeOlasBalanceWithStaked =
Expand All @@ -191,7 +94,7 @@ const AgentNotRunningButton = () => {
setIsStakingContractInfoPollingPaused(true);

// Mock "DEPLOYING" status (service polling will update this once resumed)
setServiceStatus(MiddlewareDeploymentStatus.DEPLOYING);
setDeploymentStatus(MiddlewareDeploymentStatus.DEPLOYING);

// Get the active staking program id; default id if there's no agent yet
const stakingProgramId: StakingProgramId =
Expand Down Expand Up @@ -244,15 +147,15 @@ const AgentNotRunningButton = () => {
}

// Can assume successful deployment
setServiceStatus(MiddlewareDeploymentStatus.DEPLOYED);
setDeploymentStatus(MiddlewareDeploymentStatus.DEPLOYED);

// TODO: remove this workaround, middleware should respond when agent is staked & confirmed running after `createService` call
await delayInSeconds(5);

// update provider states sequentially
// service id is required before activeStakingContractInfo & balances can be updated
try {
await updateServicesState(); // reload the available services
await updateServicesState?.(); // reload the available services
await updateActiveStakingContractInfo(); // reload active staking contract with new service
await updateBalances(); // reload the balances
} catch (error) {
Expand All @@ -268,7 +171,7 @@ const AgentNotRunningButton = () => {
setIsServicePollingPaused,
setIsBalancePollingPaused,
setIsStakingContractInfoPollingPaused,
setServiceStatus,
setDeploymentStatus,
masterSafeAddress,
showNotification,
activeStakingProgramId,
Expand All @@ -284,15 +187,15 @@ const AgentNotRunningButton = () => {
// if the agent is NOT running and the balance is too low,
// user should not be able to start the agent
const isServiceInactive =
serviceStatus === MiddlewareDeploymentStatus.BUILT ||
serviceStatus === MiddlewareDeploymentStatus.STOPPED;
deploymentStatus === MiddlewareDeploymentStatus.BUILT ||
deploymentStatus === MiddlewareDeploymentStatus.STOPPED;
if (isServiceInactive && isLowBalance) {
return false;
}

if (serviceStatus === MiddlewareDeploymentStatus.DEPLOYED) return false;
if (serviceStatus === MiddlewareDeploymentStatus.DEPLOYING) return false;
if (serviceStatus === MiddlewareDeploymentStatus.STOPPING) return false;
if (deploymentStatus === MiddlewareDeploymentStatus.DEPLOYED) return false;
if (deploymentStatus === MiddlewareDeploymentStatus.DEPLOYING) return false;
if (deploymentStatus === MiddlewareDeploymentStatus.STOPPING) return false;

if (!requiredOlas) return false;

Expand All @@ -314,12 +217,7 @@ const AgentNotRunningButton = () => {

return hasEnoughOlas && hasEnoughEth;
}, [
isStakingContractInfoRecordLoaded,
serviceStatus,
isLowBalance,
requiredOlas,
hasEnoughServiceSlots,
isServiceStaked,
deploymentStatus,
service,
storeState?.isInitialFunded,
isEligibleForStaking,
Expand All @@ -339,49 +237,3 @@ const AgentNotRunningButton = () => {

return <Button {...buttonProps}>{buttonText}</Button>;
};

export const AgentButton = () => {
const {
serviceStatus,
isServiceNotRunning,
hasInitialLoaded: isServicesLoaded,
} = useServices();
const { isStakingContractInfoRecordLoaded } = useStakingContractContext();
const { isEligibleForStaking, isAgentEvicted } =
useActiveStakingContractInfo();

return useMemo(() => {
if (!isServicesLoaded || !isStakingContractInfoRecordLoaded) {
return <Button type="primary" size="large" disabled loading />;
}

if (serviceStatus === MiddlewareDeploymentStatus.STOPPING) {
return <AgentStoppingButton />;
}

if (serviceStatus === MiddlewareDeploymentStatus.DEPLOYING) {
return <AgentStartingButton />;
}

if (serviceStatus === MiddlewareDeploymentStatus.DEPLOYED) {
return <AgentRunningButton />;
}

if (!isEligibleForStaking && isAgentEvicted) {
return <CannotStartAgentPopover />;
}

if (isServiceNotRunning) {
return <AgentNotRunningButton />;
}

return <CannotStartAgentDueToUnexpectedError />;
}, [
isServicesLoaded,
isStakingContractInfoRecordLoaded,
serviceStatus,
isEligibleForStaking,
isAgentEvicted,
isServiceNotRunning,
]);
};
Loading

0 comments on commit 7467418

Please sign in to comment.