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!: UX improvements regarding agent activity #349

Merged
merged 11 commits into from
Sep 13, 2024
10 changes: 10 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## Proposed changes

## Types of changes

What types of changes does your code introduce?
_Put an `x` in the boxes that apply_

- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
39 changes: 34 additions & 5 deletions frontend/components/MainPage/header/AgentButton.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { Button, ButtonProps, Flex, Popover, Typography } from 'antd';
import { Button, ButtonProps, Flex, Popover, Tooltip, Typography } from 'antd';
import { useCallback, useMemo } from 'react';

import { Chain, DeploymentStatus } from '@/client';
import { COLOR } from '@/constants/colors';
import { useBalance } from '@/hooks/useBalance';
import { useElectronApi } from '@/hooks/useElectronApi';
import { useReward } from '@/hooks/useReward';
import { useServices } from '@/hooks/useServices';
import { useServiceTemplates } from '@/hooks/useServiceTemplates';
import { useStakingContractInfo } from '@/hooks/useStakingContractInfo';
Expand All @@ -21,12 +22,28 @@ import {
CannotStartAgentPopover,
} from './CannotStartAgentPopover';
import { requiredGas } from './constants';
import { LastTransaction } from './LastTransaction';

const { Text } = Typography;
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 and stopped working. It’ll
return to work once the next epoch starts.
</Paragraph>
}
>
<InfoCircleOutlined />
</Tooltip>
);

const AgentStartingButton = () => (
<Popover
trigger={['hover', 'click']}
Expand Down Expand Up @@ -55,6 +72,7 @@ const AgentStoppingButton = () => (

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

Expand All @@ -81,9 +99,20 @@ const AgentRunningButton = () => {
<Button type="default" size="large" onClick={handlePause}>
Pause
</Button>
<Typography.Text type="secondary" className="text-sm loading-ellipses">
Agent is working
</Typography.Text>

<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>
);
};
Expand Down
18 changes: 16 additions & 2 deletions frontend/components/MainPage/header/AgentHead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Badge } from 'antd';
import Image from 'next/image';

import { DeploymentStatus } from '@/client';
import { useReward } from '@/hooks/useReward';
import { useServices } from '@/hooks/useServices';

const badgeOffset: [number, number] = [-5, 32.5];
Expand All @@ -24,13 +25,26 @@ const StoppedAgentHead = () => (
</Badge>
);

const IdleAgentHead = () => (
<Badge dot status="processing" color="green" offset={badgeOffset}>
<Image src="/idle-robot.svg" alt="Idle Robot" width={40} height={40} />
</Badge>
);

export const AgentHead = () => {
const { serviceStatus } = useServices();
const { isEligibleForRewards } = useReward();

if (
serviceStatus === DeploymentStatus.DEPLOYING ||
serviceStatus === DeploymentStatus.STOPPING
)
) {
return <TransitionalAgentHead />;
if (serviceStatus === DeploymentStatus.DEPLOYED) return <DeployedAgentHead />;
}

if (serviceStatus === DeploymentStatus.DEPLOYED) {
// If the agent is eligible for rewards, agent is idle
return isEligibleForRewards ? <IdleAgentHead /> : <DeployedAgentHead />;
}
return <StoppedAgentHead />;
};
79 changes: 79 additions & 0 deletions frontend/components/MainPage/header/LastTransaction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Skeleton, Typography } from 'antd';
import { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { useInterval } from 'usehooks-ts';

import { useAddress } from '@/hooks/useAddress';
import { getLatestTransaction } from '@/service/Ethers';
import { TransactionInfo } from '@/types/TransactionInfo';
import { getTimeAgo } from '@/utils/time';

const { Text } = Typography;

const Loader = styled(Skeleton.Input)`
line-height: 1;
span {
width: 120px !important;
height: 12px !important;
margin-top: 6px !important;
}
`;

const POLLING_INTERVAL = 60 * 1000; // 1 minute

/**
* Component to display the last transaction time and link to the transaction on GnosisScan
* by agent safe.
*/
export const LastTransaction = () => {
const { multisigAddress } = useAddress();

const [isFetching, setIsFetching] = useState(true);
const [transaction, setTransaction] = useState<TransactionInfo | null>(null);

const fetchTransaction = useCallback(async () => {
if (!multisigAddress) return;

getLatestTransaction(multisigAddress)
.then((tx) => setTransaction(tx))
.catch((error) =>
console.error('Failed to get latest transaction', error),
)
.finally(() => setIsFetching(false));
}, [multisigAddress]);

// Fetch the latest transaction on mount
useEffect(() => {
fetchTransaction();
}, [fetchTransaction]);

// Poll for the latest transaction
useInterval(() => fetchTransaction(), POLLING_INTERVAL);

if (isFetching) {
return <Loader active size="small" />;
}

if (!transaction) {
return (
<Text type="secondary" className="text-xs">
No transactions recently!
</Text>
);
}

return (
<Text type="secondary" className="text-xs">
Last txn:&nbsp;
<Text
type="secondary"
className="text-xs pointer hover-underline"
onClick={() =>
window.open(`https://gnosisscan.io/tx/${transaction.hash}`)
}
>
{getTimeAgo(transaction.timestamp)} ↗
</Text>
</Text>
);
};
2 changes: 1 addition & 1 deletion frontend/components/MainPage/sections/RewardsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const DisplayRewards = () => {
return (
<CardSection vertical gap={8} padding="16px 24px" align="start">
<Text type="secondary">
Staking rewards this work period&nbsp;
Staking rewards this epoch&nbsp;
<Tooltip
arrow={false}
title={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const StakingContractDetails = ({
const details = stakingContractInfoRecord[stakingProgramId];
return [
{
left: 'Rewards per work period',
left: 'Rewards per epoch',
right: `~ ${details.rewardsPerWorkPeriod?.toFixed(2)} OLAS`,
},
{
Expand Down
14 changes: 4 additions & 10 deletions frontend/components/SettingsPage/DebugInfoSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ import {
import { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';

import { CHAINS } from '@/constants/chains';
import { COLOR } from '@/constants/colors';
import { UNICODE_SYMBOLS } from '@/constants/symbols';
import { Token } from '@/enums/Token';
import { useAddress } from '@/hooks/useAddress';
import { useBalance } from '@/hooks/useBalance';
import { useServices } from '@/hooks/useServices';
import { useWallet } from '@/hooks/useWallet';
import { WalletAddressNumberRecord } from '@/types/Records';
import { copyToClipboard } from '@/utils/copyToClipboard';
Expand Down Expand Up @@ -143,15 +142,14 @@ const DebugItem = ({

export const DebugInfoSection = () => {
const { wallets, masterEoaAddress, masterSafeAddress } = useWallet();
const { services } = useServices();
const { instanceAddress, multisigAddress } = useAddress();
const { walletBalances } = useBalance();

const [isModalOpen, setIsModalOpen] = useState(false);
const showModal = useCallback(() => setIsModalOpen(true), []);
const handleCancel = useCallback(() => setIsModalOpen(false), []);

const data = useMemo(() => {
if (!services) return null;
if (!wallets?.length) return null;

const result = [];
Expand All @@ -170,18 +168,13 @@ export const DebugInfoSection = () => {
});
}

const instanceAddress =
services[0]?.chain_configs?.[CHAINS.GNOSIS.chainId]?.chain_data
?.instances?.[0];
if (instanceAddress) {
result.push({
title: 'Agent Instance EOA',
...getItemData(walletBalances, instanceAddress!),
});
}

const multisigAddress =
services[0]?.chain_configs?.[CHAINS.GNOSIS.chainId]?.chain_data?.multisig;
if (multisigAddress) {
result.push({
title: 'Agent Safe',
Expand All @@ -193,7 +186,8 @@ export const DebugInfoSection = () => {
}, [
masterEoaAddress,
masterSafeAddress,
services,
instanceAddress,
multisigAddress,
walletBalances,
wallets?.length,
]);
Expand Down
17 changes: 17 additions & 0 deletions frontend/hooks/useAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CHAINS } from '@/constants/chains';

import { useServices } from './useServices';

export const useAddress = () => {
const { service } = useServices();

/** agent safe multisig address */
const multisigAddress =
service?.chain_configs?.[CHAINS.GNOSIS.chainId]?.chain_data?.multisig;

/** agent instance EOA address */
const instanceAddress =
service?.chain_configs?.[CHAINS.GNOSIS.chainId]?.chain_data?.instances?.[0];

return { instanceAddress, multisigAddress };
};
9 changes: 9 additions & 0 deletions frontend/public/idle-robot.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 59 additions & 0 deletions frontend/service/Ethers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ContractInterface, ethers, providers, utils } from 'ethers';

import { gnosisProvider } from '@/constants/providers';
import { Address } from '@/types/Address';
import { TransactionInfo } from '@/types/TransactionInfo';

/**
* Returns native balance of the given address
Expand Down Expand Up @@ -89,6 +90,63 @@ const checkRpc = async (rpc: string): Promise<boolean> => {
}
};

const BACK_TRACK_BLOCKS = 9000;
mohandast52 marked this conversation as resolved.
Show resolved Hide resolved
const MAX_ROUNDS = 5;

const getLogsList = async (
contractAddress: Address,
fromBlock: number,
toBlock: number,
roundsLeft: number,
): Promise<providers.Log[]> => {
// Limit the number of recursive calls to prevent too many requests
if (roundsLeft === 0) return [];

const filter = {
address: contractAddress,
fromBlock,
toBlock,
};
const list = await gnosisProvider.getLogs(filter);

if (list.length > 0) return list;

return getLogsList(
contractAddress,
fromBlock - BACK_TRACK_BLOCKS,
fromBlock,
roundsLeft - 1,
);
};

/**
* Get the latest transaction details for the given contract address
*/
export const getLatestTransaction = async (
contractAddress: Address,
): Promise<TransactionInfo | null> => {
const latestBlock = await gnosisProvider.getBlockNumber();

const logs = await getLogsList(
contractAddress,
latestBlock - BACK_TRACK_BLOCKS,
latestBlock,
MAX_ROUNDS,
);

// No transactions found
if (logs.length === 0) return null;

// Get the last log entry and fetch the transaction details
const lastLog = logs[logs.length - 1];
const txHash = lastLog.transactionHash;
const receipt = await gnosisProvider.getTransactionReceipt(txHash);
const block = await gnosisProvider.getBlock(receipt.blockNumber);
const timestamp = block.timestamp;

return { hash: txHash, timestamp };
};

const readContract = ({
address,
abi,
Expand All @@ -105,4 +163,5 @@ export const EthersService = {
getErc20Balance,
checkRpc,
readContract,
getLatestTransaction,
};
Loading
Loading