diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04432570..a07e5fe4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: - name: Get trader bin run: | - trader_version=$(poetry run python -c "import yaml; config = yaml.safe_load(open('templates/trader.yaml')); print(config['trader_version'])") + trader_version=$(poetry run python -c "import yaml; config = yaml.safe_load(open('templates/trader.yaml')); print(config['service_version'])") echo $trader_version mkdir dist && curl -L -o dist/aea_bin "https://github.com/valory-xyz/trader/releases/download/${trader_version}/trader_bin_${{ env.OS_ARCH }}" diff --git a/.gitleaksignore b/.gitleaksignore index 446eb5a4..33c8c27c 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -27,4 +27,6 @@ d8149e9b5b7bd6a7ed7bc1039900702f1d4f287b:operate/services/manage.py:generic-api- 99c0f139b037da2587708212fcf6d0e20786d0ba:operate/services/manage.py:generic-api-key:406 99c0f139b037da2587708212fcf6d0e20786d0ba:operate/services/manage.py:generic-api-key:454 99c0f139b037da2587708212fcf6d0e20786d0ba:operate/services/manage.py:generic-api-key:455 -91ec07457f69e9a29f63693ac8ef887e4b5f49f0:operate/services/manage.py:generic-api-key:454 \ No newline at end of file +91ec07457f69e9a29f63693ac8ef887e4b5f49f0:operate/services/manage.py:generic-api-key:454 +410bea2bd02ff54da69387fe8f3b58793e09f7b0:operate/services/manage.py:generic-api-key:421 +410bea2bd02ff54da69387fe8f3b58793e09f7b0:operate/services/manage.py:generic-api-key:422 \ No newline at end of file diff --git a/electron/install.js b/electron/install.js index 9da1dcad..a43856a9 100644 --- a/electron/install.js +++ b/electron/install.js @@ -14,7 +14,7 @@ const { paths } = require('./constants'); * - use "" (nothing as a suffix) for latest release candidate, for example "0.1.0rc26" * - use "alpha" for alpha release, for example "0.1.0rc26-alpha" */ -const OlasMiddlewareVersion = '0.1.0rc111'; +const OlasMiddlewareVersion = '0.1.0rc117'; const path = require('path'); const { app } = require('electron'); diff --git a/electron/main.js b/electron/main.js index 5a01cae7..e60d56fd 100644 --- a/electron/main.js +++ b/electron/main.js @@ -195,7 +195,7 @@ const HEIGHT = 700; /** * Creates the main window */ -const createMainWindow = () => { +const createMainWindow = async () => { const width = isDev ? 840 : APP_WIDTH; mainWindow = new BrowserWindow({ title: 'Pearl', @@ -216,12 +216,6 @@ const createMainWindow = () => { mainWindow.setMenuBarVisibility(true); - if (isDev) { - mainWindow.loadURL(`http://localhost:${appConfig.ports.dev.next}`); - } else { - mainWindow.loadURL(`http://localhost:${appConfig.ports.prod.next}`); - } - ipcMain.on('close-app', () => { mainWindow.close(); }); @@ -264,15 +258,23 @@ const createMainWindow = () => { event.preventDefault(); mainWindow.hide(); }); - - const storeInitialValues = { - environmentName: process.env.IS_STAGING ? 'staging' : '', - }; - setupStoreIpc(ipcMain, mainWindow, storeInitialValues); + + try { + logger.electron('Setting up store IPC'); + await setupStoreIpc(ipcMain, mainWindow); + } catch (e) { + logger.electron('Store IPC failed:', JSON.stringify(e)); + } if (isDev) { mainWindow.webContents.openDevTools(); } + + if (isDev) { + mainWindow.loadURL(`http://localhost:${appConfig.ports.dev.next}`); + } else { + mainWindow.loadURL(`http://localhost:${appConfig.ports.prod.next}`); + } }; async function launchDaemon() { @@ -494,7 +496,7 @@ ipcMain.on('check', async function (event, _argument) { } event.sender.send('response', 'Launching App'); - createMainWindow(); + await createMainWindow(); createTray(); splashWindow.destroy(); } catch (e) { diff --git a/electron/store.js b/electron/store.js index afbc7f62..eafd9428 100644 --- a/electron/store.js +++ b/electron/store.js @@ -1,24 +1,22 @@ +const Store = require('electron-store'); // set schema to validate store data -const defaultSchema = { - environmentName: { type: 'string', default: '' }, - isInitialFunded: { type: 'boolean', default: false }, +const schema = { + isInitialFunded: { type: 'boolean', default: false }, // TODO: reconsider this default, can be problematic if user has already funded prior to implementation firstStakingRewardAchieved: { type: 'boolean', default: false }, firstRewardNotificationShown: { type: 'boolean', default: false }, agentEvictionAlertShown: { type: 'boolean', default: false }, -}; - -const setupStoreIpc = async (ipcChannel, mainWindow, storeInitialValues) => { - const Store = (await import('electron-store')).default; - // set default values for store - const schema = Object.assign({}, defaultSchema); - Object.keys(schema).forEach((key) => { - if (storeInitialValues[key] !== undefined) { - schema[key].default = storeInitialValues[key]; - } - }); + environmentName: { type: 'string', default: '' }, + currentStakingProgram: { type: 'string', default: '' }, +}; - /** @type import Store from 'electron-store' */ +/** + * Sets up the IPC communication and initializes the Electron store with default values and schema. + * @param {Electron.IpcMain} ipcMain - The IPC channel for communication. + * @param {Electron.BrowserWindow} mainWindow - The main Electron browser window. + * @returns {Promise} - A promise that resolves once the store is set up. + */ +const setupStoreIpc = async (ipcMain, mainWindow) => { const store = new Store({ schema }); store.onDidAnyChange((data) => { @@ -27,11 +25,11 @@ const setupStoreIpc = async (ipcChannel, mainWindow, storeInitialValues) => { }); // exposed to electron browser window - ipcChannel.handle('store', () => store.store); - ipcChannel.handle('store-get', (_, key) => store.get(key)); - ipcChannel.handle('store-set', (_, key, value) => store.set(key, value)); - ipcChannel.handle('store-delete', (_, key) => store.delete(key)); - ipcChannel.handle('store-clear', (_) => store.clear()); + ipcMain.handle('store', () => store.store); + ipcMain.handle('store-get', (_, key) => store.get(key)); + ipcMain.handle('store-set', (_, key, value) => store.set(key, value)); + ipcMain.handle('store-delete', (_, key) => store.delete(key)); + ipcMain.handle('store-clear', (_) => store.clear()); }; module.exports = { setupStoreIpc }; diff --git a/frontend/client/types.ts b/frontend/client/types.ts index e7eeb2d8..fc5d7fe7 100644 --- a/frontend/client/types.ts +++ b/frontend/client/types.ts @@ -1,3 +1,4 @@ +import { StakingProgram } from '@/enums/StakingProgram'; import { Address } from '@/types/Address'; import { Chain, DeploymentStatus, Ledger } from './enums'; @@ -20,6 +21,19 @@ export type ChainData = { instances?: Address[]; token?: number; multisig?: Address; + on_chain_state: number; + staked: boolean; + user_params: { + cost_of_bond: number; + fund_requirements: { + agent: number; + safe: number; + }; + nft: string; + staking_program_id: StakingProgram; + threshold: number; + use_staking: true; + }; }; export type Service = { @@ -27,8 +41,12 @@ export type Service = { hash: string; keys: ServiceKeys[]; readme?: string; - ledger: LedgerConfig; - chain_data: ChainData; + chain_configs: { + [chainId: number]: { + ledger_config: LedgerConfig; + chain_data: ChainData; + }; + }; }; export type ServiceTemplate = { @@ -36,20 +54,20 @@ export type ServiceTemplate = { hash: string; image: string; description: string; - configuration: ConfigurationTemplate; + service_version: string; + home_chain_id: string; + configurations: { [key: string]: ConfigurationTemplate }; deploy?: boolean; }; export type ConfigurationTemplate = { + rpc?: string; // added on deployment + staking_program_id?: StakingProgram; // added on deployment nft: string; - trader_version: string; - rpc?: string; // added by user agent_id: number; threshold: number; use_staking: boolean; cost_of_bond: number; - olas_cost_of_bond: number; - olas_required_to_stake: number; monthly_gas_estimate: number; fund_requirements: FundRequirementsTemplate; }; diff --git a/frontend/components/Alert/AlertTitle.tsx b/frontend/components/Alert/AlertTitle.tsx new file mode 100644 index 00000000..5afecb32 --- /dev/null +++ b/frontend/components/Alert/AlertTitle.tsx @@ -0,0 +1,5 @@ +import { Typography } from 'antd'; + +export const AlertTitle = ({ children }: { children: React.ReactNode }) => ( + {children} +); diff --git a/frontend/components/Alert/index.tsx b/frontend/components/Alert/index.tsx index 3cadd98b..5a022210 100644 --- a/frontend/components/Alert/index.tsx +++ b/frontend/components/Alert/index.tsx @@ -14,7 +14,7 @@ const icons = { error: , }; -export const Alert = ({ +export const CustomAlert = ({ type, fullWidth, ...rest diff --git a/frontend/components/HelpAndSupport/index.tsx b/frontend/components/HelpAndSupportPage/index.tsx similarity index 97% rename from frontend/components/HelpAndSupport/index.tsx rename to frontend/components/HelpAndSupportPage/index.tsx index 278246e9..1aec818a 100644 --- a/frontend/components/HelpAndSupport/index.tsx +++ b/frontend/components/HelpAndSupportPage/index.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useState } from 'react'; import { UNICODE_SYMBOLS } from '@/constants/symbols'; import { FAQ_URL, SUPPORT_URL } from '@/constants/urls'; -import { PageState } from '@/enums/PageState'; +import { Pages } from '@/enums/PageState'; import { useElectronApi } from '@/hooks/useElectronApi'; import { useLogs } from '@/hooks/useLogs'; import { usePageState } from '@/hooks/usePageState'; @@ -78,7 +78,7 @@ export const HelpAndSupport = () => { , + ]} + > + + OLAS logo + + + {`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/AgentButton/index.tsx b/frontend/components/MainPage/header/AgentButton.tsx similarity index 87% rename from frontend/components/Main/MainHeader/AgentButton/index.tsx rename to frontend/components/MainPage/header/AgentButton.tsx index 356a45c1..6fcdbdea 100644 --- a/frontend/components/Main/MainHeader/AgentButton/index.tsx +++ b/frontend/components/MainPage/header/AgentButton.tsx @@ -4,12 +4,13 @@ import { useCallback, useMemo } from 'react'; import { Chain, DeploymentStatus } from '@/client'; import { COLOR } from '@/constants/colors'; -import { LOW_BALANCE } from '@/constants/thresholds'; +import { StakingProgram } from '@/enums/StakingProgram'; import { useBalance } from '@/hooks/useBalance'; import { useElectronApi } from '@/hooks/useElectronApi'; import { useServices } from '@/hooks/useServices'; import { useServiceTemplates } from '@/hooks/useServiceTemplates'; import { useStakingContractInfo } from '@/hooks/useStakingContractInfo'; +import { useStakingProgram } from '@/hooks/useStakingProgram'; import { useStore } from '@/hooks/useStore'; import { useWallet } from '@/hooks/useWallet'; import { ServicesService } from '@/service/Services'; @@ -17,10 +18,10 @@ import { WalletService } from '@/service/Wallet'; import { getMinimumStakedAmountRequired } from '@/utils/service'; import { - CannotStartAgent, CannotStartAgentDueToUnexpectedError, -} from '../CannotStartAgent'; -import { requiredGas, requiredOlas } from '../constants'; + CannotStartAgentPopover, +} from './CannotStartAgentPopover'; +import { requiredGas } from './constants'; const { Text } = Typography; @@ -101,11 +102,22 @@ const AgentNotRunningButton = () => { const { setIsPaused: setIsBalancePollingPaused, safeBalance, + isLowBalance, totalOlasStakedBalance, totalEthBalance, } = useBalance(); const { storeState } = useStore(); const { isEligibleForStaking, isAgentEvicted } = useStakingContractInfo(); + const { activeStakingProgram, defaultStakingProgram } = useStakingProgram(); + + // const minStakingDeposit = + // stakingContractInfoRecord?.[activeStakingProgram ?? defaultStakingProgram] + // ?.minStakingDeposit; + + const requiredOlas = getMinimumStakedAmountRequired( + serviceTemplate, + activeStakingProgram ?? defaultStakingProgram, + ); const safeOlasBalance = safeBalance?.OLAS; const safeOlasBalanceWithStaked = @@ -143,6 +155,7 @@ const AgentNotRunningButton = () => { // Then create / deploy the service try { await ServicesService.createService({ + stakingProgram: activeStakingProgram ?? defaultStakingProgram, // overwrite with StakingProgram.Alpha to test migration serviceTemplate, deploy: true, }); @@ -160,8 +173,10 @@ const AgentNotRunningButton = () => { if (!service) { showNotification?.('Your agent is now running!'); } else { - const minimumStakedAmountRequired = - getMinimumStakedAmountRequired(serviceTemplate); + const minimumStakedAmountRequired = getMinimumStakedAmountRequired( + serviceTemplate, + StakingProgram.Beta, // users should always deploy on Beta if they are yet to start their agent + ); showNotification?.( `Your agent is running and you've staked ${minimumStakedAmountRequired} OLAS!`, @@ -184,6 +199,8 @@ const AgentNotRunningButton = () => { setServiceStatus, masterSafeAddress, showNotification, + activeStakingProgram, + defaultStakingProgram, serviceTemplate, service, ]); @@ -194,7 +211,7 @@ const AgentNotRunningButton = () => { const isServiceInactive = serviceStatus === DeploymentStatus.BUILT || serviceStatus === DeploymentStatus.STOPPED; - if (isServiceInactive && safeBalance && safeBalance.ETH < LOW_BALANCE) { + if (isServiceInactive && isLowBalance) { return false; } @@ -202,6 +219,8 @@ const AgentNotRunningButton = () => { if (serviceStatus === DeploymentStatus.DEPLOYING) return false; if (serviceStatus === DeploymentStatus.STOPPING) return false; + if (!requiredOlas) return false; + // case where service exists & user has initial funded if (service && storeState?.isInitialFunded) { if (!safeOlasBalanceWithStaked) return false; @@ -217,14 +236,15 @@ const AgentNotRunningButton = () => { return hasEnoughOlas && hasEnoughEth; }, [ - isAgentEvicted, - isEligibleForStaking, - safeOlasBalanceWithStaked, - service, serviceStatus, + service, storeState?.isInitialFunded, + isEligibleForStaking, + isAgentEvicted, + safeOlasBalanceWithStaked, + requiredOlas, totalEthBalance, - safeBalance, + isLowBalance, ]); const buttonProps: ButtonProps = { @@ -260,7 +280,8 @@ export const AgentButton = () => { return ; } - if (!isEligibleForStaking && isAgentEvicted) return ; + if (!isEligibleForStaking && isAgentEvicted) + return ; if ( !service || diff --git a/frontend/components/Main/MainHeader/AgentHead.tsx b/frontend/components/MainPage/header/AgentHead.tsx similarity index 100% rename from frontend/components/Main/MainHeader/AgentHead.tsx rename to frontend/components/MainPage/header/AgentHead.tsx diff --git a/frontend/components/Main/MainHeader/CannotStartAgent.tsx b/frontend/components/MainPage/header/CannotStartAgentPopover.tsx similarity index 98% rename from frontend/components/Main/MainHeader/CannotStartAgent.tsx rename to frontend/components/MainPage/header/CannotStartAgentPopover.tsx index 556512f5..0f880a7a 100644 --- a/frontend/components/Main/MainHeader/CannotStartAgent.tsx +++ b/frontend/components/MainPage/header/CannotStartAgentPopover.tsx @@ -86,7 +86,7 @@ const NoJobsAvailablePopover = () => ( ); -export const CannotStartAgent = () => { +export const CannotStartAgentPopover = () => { const { isEligibleForStaking, hasEnoughServiceSlots, diff --git a/frontend/components/MainPage/header/constants.ts b/frontend/components/MainPage/header/constants.ts new file mode 100644 index 00000000..101e6159 --- /dev/null +++ b/frontend/components/MainPage/header/constants.ts @@ -0,0 +1,11 @@ +import { formatUnits } from 'ethers/lib/utils'; + +import { CHAINS } from '@/constants/chains'; +import { SERVICE_TEMPLATES } from '@/constants/serviceTemplates'; + +export const requiredGas = Number( + formatUnits( + `${SERVICE_TEMPLATES[0].configurations[CHAINS.GNOSIS.chainId].monthly_gas_estimate}`, + 18, + ), +); diff --git a/frontend/components/Main/MainHeader/index.tsx b/frontend/components/MainPage/header/index.tsx similarity index 83% rename from frontend/components/Main/MainHeader/index.tsx rename to frontend/components/MainPage/header/index.tsx index 78ffde06..d09f681e 100644 --- a/frontend/components/Main/MainHeader/index.tsx +++ b/frontend/components/MainPage/header/index.tsx @@ -2,22 +2,21 @@ import { Flex } from 'antd'; import { useCallback, useEffect, useState } from 'react'; import { DeploymentStatus } from '@/client'; -import { LOW_BALANCE } from '@/constants/thresholds'; import { useBalance } from '@/hooks/useBalance'; import { useElectronApi } from '@/hooks/useElectronApi'; import { useServices } from '@/hooks/useServices'; +import { FirstRunModal } from '../modals/FirstRunModal'; import { AgentButton } from './AgentButton'; import { AgentHead } from './AgentHead'; -import { FirstRunModal } from './FirstRunModal'; const useSetupTrayIcon = () => { - const { safeBalance } = useBalance(); + const { isLowBalance } = useBalance(); const { serviceStatus } = useServices(); const { setTrayIcon } = useElectronApi(); useEffect(() => { - if (safeBalance && safeBalance.ETH < LOW_BALANCE) { + if (isLowBalance) { setTrayIcon?.('low-gas'); } else if (serviceStatus === DeploymentStatus.DEPLOYED) { setTrayIcon?.('running'); @@ -26,7 +25,7 @@ const useSetupTrayIcon = () => { } else if (serviceStatus === DeploymentStatus.BUILT) { setTrayIcon?.('logged-out'); } - }, [safeBalance, serviceStatus, setTrayIcon]); + }, [isLowBalance, serviceStatus, setTrayIcon]); return null; }; diff --git a/frontend/components/Main/index.tsx b/frontend/components/MainPage/index.tsx similarity index 53% rename from frontend/components/Main/index.tsx rename to frontend/components/MainPage/index.tsx index 1d2c4a5d..f97acf15 100644 --- a/frontend/components/Main/index.tsx +++ b/frontend/components/MainPage/index.tsx @@ -2,23 +2,27 @@ import { QuestionCircleOutlined, SettingOutlined } from '@ant-design/icons'; import { Button, Card, Flex } from 'antd'; import { useEffect } from 'react'; -import { PageState } from '@/enums/PageState'; +import { Pages } from '@/enums/PageState'; +import { StakingProgram } from '@/enums/StakingProgram'; import { useBalance } from '@/hooks/useBalance'; import { usePageState } from '@/hooks/usePageState'; import { useServices } from '@/hooks/useServices'; +import { useStakingProgram } from '@/hooks/useStakingProgram'; -import { KeepAgentRunning } from './KeepAgentRunning'; -import { MainAddFunds } from './MainAddFunds'; -import { MainGasBalance } from './MainGasBalance'; -import { MainHeader } from './MainHeader'; -import { MainNeedsFunds } from './MainNeedsFunds'; -import { MainOlasBalance } from './MainOlasBalance'; -import { MainRewards } from './MainRewards'; +import { MainHeader } from './header'; +import { AddFundsSection } from './sections/AddFundsSection'; +import { GasBalanceSection } from './sections/GasBalanceSection'; +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'; export const Main = () => { const { goto } = usePageState(); const { updateServicesState } = useServices(); const { updateBalances, isLoaded, setIsLoaded } = useBalance(); + const { activeStakingProgram: currentStakingProgram } = useStakingProgram(); useEffect(() => { if (!isLoaded) { @@ -36,25 +40,28 @@ export const Main = () => { type="default" size="large" icon={} - onClick={() => goto(PageState.HelpAndSupport)} + onClick={() => goto(Pages.HelpAndSupport)} /> , + ]} + > + + {/* Robot head */} + + Pearl agent head + + + You switched staking contract succesfully! + + + Your agent is now staked on {activeStakingProgramMeta.name}. + + {/* TODO: Add relevant block explorer domain */} + + View full contract details {UNICODE_SYMBOLS.EXTERNAL_LINK} + + + + ); +}; diff --git a/frontend/components/Main/MainAddFunds.tsx b/frontend/components/MainPage/sections/AddFundsSection.tsx similarity index 96% rename from frontend/components/Main/MainAddFunds.tsx rename to frontend/components/MainPage/sections/AddFundsSection.tsx index 06fd3d4d..97bf8564 100644 --- a/frontend/components/Main/MainAddFunds.tsx +++ b/frontend/components/MainPage/sections/AddFundsSection.tsx @@ -22,8 +22,8 @@ import { Address } from '@/types/Address'; import { copyToClipboard } from '@/utils/copyToClipboard'; import { truncateAddress } from '@/utils/truncate'; -import { Alert } from '../Alert'; -import { CardSection } from '../styled/CardSection'; +import { CustomAlert } from '../../Alert'; +import { CardSection } from '../../styled/CardSection'; const { Text } = Typography; @@ -33,7 +33,7 @@ const CustomizedCardSection = styled(CardSection)<{ border?: boolean }>` } `; -export const MainAddFunds = () => { +export const AddFundsSection = () => { const [isAddFundsVisible, setIsAddFundsVisible] = useState(false); const { masterSafeAddress } = useWallet(); @@ -92,7 +92,7 @@ export const MainAddFunds = () => { const AddFundsWarningAlertSection = () => ( - { - const { isBalanceLoaded, safeBalance } = useBalance(); + const { isBalanceLoaded, isLowBalance } = useBalance(); const { storeState } = useStore(); const { showNotification } = useElectronApi(); @@ -44,35 +43,34 @@ const BalanceStatus = () => { // show notification if balance is too low useEffect(() => { if (!isBalanceLoaded) return; - if (!safeBalance) return; if (!showNotification) return; if (!storeState?.isInitialFunded) return; - if (safeBalance.ETH < LOW_BALANCE && !isLowBalanceNotificationShown) { + if (isLowBalance && !isLowBalanceNotificationShown) { showNotification('Trading balance is too low.'); setIsLowBalanceNotificationShown(true); } // If it has already been shown and the balance has increased, // should show the notification again if it goes below the threshold. - if (safeBalance.ETH >= LOW_BALANCE && isLowBalanceNotificationShown) { + if (!isLowBalance && isLowBalanceNotificationShown) { setIsLowBalanceNotificationShown(false); } }, [ isBalanceLoaded, isLowBalanceNotificationShown, - safeBalance, + isLowBalance, showNotification, storeState?.isInitialFunded, ]); const status = useMemo(() => { - if (!safeBalance || safeBalance.ETH < LOW_BALANCE) { + if (isLowBalance) { return { statusName: 'Too low', StatusComponent: EmptyDot }; } return { statusName: 'Fine', StatusComponent: FineDot }; - }, [safeBalance]); + }, [isLowBalance]); const { statusName, StatusComponent } = status; return ( @@ -91,7 +89,7 @@ const TooltipContent = styled.div` } `; -export const MainGasBalance = () => { +export const GasBalanceSection = () => { const { masterSafeAddress } = useWallet(); const { isBalanceLoaded } = useBalance(); diff --git a/frontend/components/Main/KeepAgentRunning.tsx b/frontend/components/MainPage/sections/KeepAgentRunningSection.tsx similarity index 83% rename from frontend/components/Main/KeepAgentRunning.tsx rename to frontend/components/MainPage/sections/KeepAgentRunningSection.tsx index 1cc98b33..90803c2f 100644 --- a/frontend/components/Main/KeepAgentRunning.tsx +++ b/frontend/components/MainPage/sections/KeepAgentRunningSection.tsx @@ -4,14 +4,14 @@ import { DeploymentStatus } from '@/client'; import { useServices } from '@/hooks/useServices'; import { useStore } from '@/hooks/useStore'; -import { Alert } from '../Alert'; -import { CardSection } from '../styled/CardSection'; +import { CustomAlert } from '../../Alert'; +import { CardSection } from '../../styled/CardSection'; const { Text } = Typography; const COVER_BLOCK_BORDERS_STYLE = { marginBottom: '-1px' }; -export const KeepAgentRunning = () => { +export const KeepAgentRunningSection = () => { const { storeState } = useStore(); const { serviceStatus } = useServices(); @@ -20,7 +20,7 @@ export const KeepAgentRunning = () => { return ( - { const serviceFundRequirements = useMemo(() => { const monthlyGasEstimate = Number( - formatUnits(`${serviceTemplate.configuration.monthly_gas_estimate}`, 18), + formatUnits( + `${serviceTemplate.configurations[CHAINS.GNOSIS.chainId].monthly_gas_estimate}`, + 18, + ), ); const minimumStakedAmountRequired = @@ -152,7 +156,7 @@ export const MainNeedsFunds = () => { return ( - + ); }; diff --git a/frontend/components/MainPage/sections/NewStakingProgramAlertSection.tsx b/frontend/components/MainPage/sections/NewStakingProgramAlertSection.tsx new file mode 100644 index 00000000..bce9d098 --- /dev/null +++ b/frontend/components/MainPage/sections/NewStakingProgramAlertSection.tsx @@ -0,0 +1,36 @@ +import { Button, Flex, Typography } from 'antd'; + +import { Pages } from '@/enums/PageState'; +import { usePageState } from '@/hooks/usePageState'; + +import { CustomAlert } from '../../Alert'; +import { CardSection } from '../../styled/CardSection'; + +const { Text } = Typography; + +export const NewStakingProgramAlertSection = () => { + const { goto } = usePageState(); + + return ( + + + A new staking contract is available for your agent! + + + } + /> + + ); +}; diff --git a/frontend/components/Main/MainOlasBalance.tsx b/frontend/components/MainPage/sections/OlasBalanceSection.tsx similarity index 93% rename from frontend/components/Main/MainOlasBalance.tsx rename to frontend/components/MainPage/sections/OlasBalanceSection.tsx index fa6d87b9..0c5571da 100644 --- a/frontend/components/Main/MainOlasBalance.tsx +++ b/frontend/components/MainPage/sections/OlasBalanceSection.tsx @@ -3,17 +3,17 @@ import { Button, Flex, Skeleton, Tooltip, Typography } from 'antd'; import { useMemo } from 'react'; import styled from 'styled-components'; -import { Alert } from '@/components/Alert'; +import { CustomAlert } from '@/components/Alert'; import { COLOR } from '@/constants/colors'; import { UNICODE_SYMBOLS } from '@/constants/symbols'; -import { LOW_BALANCE } from '@/constants/thresholds'; +import { LOW_MASTER_SAFE_BALANCE } from '@/constants/thresholds'; import { useBalance } from '@/hooks/useBalance'; import { useElectronApi } from '@/hooks/useElectronApi'; import { useReward } from '@/hooks/useReward'; import { useStore } from '@/hooks/useStore'; import { balanceFormat } from '@/utils/numberFormatters'; -import { CardSection } from '../styled/CardSection'; +import { CardSection } from '../../styled/CardSection'; const { Text, Title } = Typography; const Balance = styled.span` @@ -122,17 +122,16 @@ const MainOlasBalanceAlert = styled.div` `; const LowTradingBalanceAlert = () => { - const { isBalanceLoaded, safeBalance } = useBalance(); + const { isBalanceLoaded, isLowBalance } = useBalance(); const { storeState } = useStore(); if (!isBalanceLoaded) return null; - if (!safeBalance) return null; if (!storeState?.isInitialFunded) return; - if (safeBalance.ETH >= LOW_BALANCE) return null; + if (!isLowBalance) return null; return ( - { Trading balance is too low - {`To run your agent, add at least $${LOW_BALANCE} XDAI to your account.`} + {`To run your agent, add at least $${LOW_MASTER_SAFE_BALANCE} XDAI to your account.`} Do it quickly to avoid your agent missing its targets and getting @@ -160,7 +159,7 @@ const AvoidSuspensionAlert = () => { return ( - { + if (status === StakingProgramStatus.New) { + return New; + } else if (status === StakingProgramStatus.Selected) { + return Selected; + } + return null; +}; diff --git a/frontend/components/ManageStakingPage/StakingContractSection/alerts.tsx b/frontend/components/ManageStakingPage/StakingContractSection/alerts.tsx new file mode 100644 index 00000000..f2c1cf6c --- /dev/null +++ b/frontend/components/ManageStakingPage/StakingContractSection/alerts.tsx @@ -0,0 +1,62 @@ +import { Flex, Typography } from 'antd'; + +import { CustomAlert } from '@/components/Alert'; +import { UNICODE_SYMBOLS } from '@/constants/symbols'; + +const { Text } = Typography; + +export const AlertInsufficientMigrationFunds = ({ + totalOlasBalance, +}: { + totalOlasBalance: number; +}) => ( + + + Insufficient amount of funds to switch + + + Add funds to your account to meet the program requirements. + + Your current OLAS balance:{' '} + {totalOlasBalance} OLAS + + + } + /> +); + +export const AlertNoSlots = () => ( + No slots currently available - try again later.} + /> +); + +export const AlertUpdateToMigrate = () => ( + + App update required + + {/* + TODO: Define version requirement in some JSON store? + How do we access this date on a previous version? + */} + + Update Pearl to the latest version to switch to the staking contract. + + {/* TODO: trigger update through IPC */} + + Update Pearl to the latest version {UNICODE_SYMBOLS.EXTERNAL_LINK} + + + } + /> +); diff --git a/frontend/components/ManageStakingPage/StakingContractSection/index.tsx b/frontend/components/ManageStakingPage/StakingContractSection/index.tsx new file mode 100644 index 00000000..363f7ce5 --- /dev/null +++ b/frontend/components/ManageStakingPage/StakingContractSection/index.tsx @@ -0,0 +1,301 @@ +import { Button, Flex, Popover, theme, Typography } from 'antd'; +import { useMemo } from 'react'; + +import { DeploymentStatus } from '@/client'; +import { CardSection } from '@/components/styled/CardSection'; +import { STAKING_PROGRAM_META } from '@/constants/stakingProgramMeta'; +import { UNICODE_SYMBOLS } from '@/constants/symbols'; +import { Pages } from '@/enums/PageState'; +import { StakingProgram } from '@/enums/StakingProgram'; +import { StakingProgramStatus } from '@/enums/StakingProgramStatus'; +import { useBalance } from '@/hooks/useBalance'; +import { useModals } from '@/hooks/useModals'; +import { usePageState } from '@/hooks/usePageState'; +import { useServices } from '@/hooks/useServices'; +import { useServiceTemplates } from '@/hooks/useServiceTemplates'; +import { useStakingContractInfo } from '@/hooks/useStakingContractInfo'; +import { useStakingProgram } from '@/hooks/useStakingProgram'; +import { ServicesService } from '@/service/Services'; +import { Address } from '@/types/Address'; +import { getMinimumStakedAmountRequired } from '@/utils/service'; + +import { + AlertInsufficientMigrationFunds, + AlertNoSlots, + AlertUpdateToMigrate, +} from './alerts'; +import { StakingContractTag } from './StakingContractTag'; + +// const { Text } = Typography; + +const { useToken } = theme; + +// const CustomDivider = styled(Divider)` +// flex: auto; +// width: max-content; +// min-width: 0; +// margin: 0; +// `; + +// const ContractParameter = ({ +// label, +// value, +// }: { +// label: string; +// value: string; +// }) => ( +// +// {label} +// +// {value} +// +// ); + +export const StakingContractSection = ({ + stakingProgram, + contractAddress, +}: { + stakingProgram: StakingProgram; + contractAddress: Address; +}) => { + const { goto } = usePageState(); + const { setServiceStatus, serviceStatus, setIsServicePollingPaused } = + useServices(); + const { serviceTemplate } = useServiceTemplates(); + const { setMigrationModalOpen } = useModals(); + const { activeStakingProgram, defaultStakingProgram, updateStakingProgram } = + useStakingProgram(); + const { stakingContractInfoRecord } = useStakingContractInfo(); + const { token } = useToken(); + const { totalOlasBalance, isBalanceLoaded } = useBalance(); + const { isServiceStakedForMinimumDuration } = useStakingContractInfo(); + + const stakingContractInfoForStakingProgram = + stakingContractInfoRecord?.[stakingProgram]; + + const activeStakingProgramMeta = STAKING_PROGRAM_META[stakingProgram]; + + const isSelected = + activeStakingProgram && activeStakingProgram === stakingProgram; + + const hasEnoughRewards = true; + //(stakingContractInfoForStakingProgram?.availableRewards ?? 0) > 0; + + const minimumOlasRequiredToMigrate = useMemo( + () => getMinimumStakedAmountRequired(serviceTemplate, StakingProgram.Beta), + [serviceTemplate], + ); + + const hasEnoughOlasToMigrate = useMemo(() => { + if (totalOlasBalance === undefined) return false; + if (!minimumOlasRequiredToMigrate) return false; + return totalOlasBalance >= minimumOlasRequiredToMigrate; + }, [minimumOlasRequiredToMigrate, totalOlasBalance]); + + const hasEnoughSlots = + stakingContractInfoForStakingProgram?.maxNumServices && + stakingContractInfoForStakingProgram?.serviceIds && + stakingContractInfoForStakingProgram?.maxNumServices > + stakingContractInfoForStakingProgram?.serviceIds?.length; + + // TODO: compatibility needs to be implemented + const isAppVersionCompatible = true; // contract.appVersion === 'rc105'; + + const isMigratable = + !isSelected && + activeStakingProgram === StakingProgram.Alpha && // TODO: make more elegant + isBalanceLoaded && + hasEnoughSlots && + hasEnoughRewards && + hasEnoughOlasToMigrate && + isAppVersionCompatible && + serviceStatus !== DeploymentStatus.DEPLOYED && + serviceStatus !== DeploymentStatus.DEPLOYING && + serviceStatus !== DeploymentStatus.STOPPING && + isServiceStakedForMinimumDuration; + + const cantMigrateReason = useMemo(() => { + if (isSelected) { + return 'Contract is already selected'; + } + + if (!hasEnoughRewards) { + return 'No available rewards'; + } + + if (activeStakingProgram !== StakingProgram.Alpha) { + return 'Can only migrate from Alpha to Beta'; + } + + if (!isBalanceLoaded) { + return 'Loading balance...'; + } + + if (!hasEnoughSlots) { + return 'No available staking slots'; + } + + if (!hasEnoughOlasToMigrate) { + return `Insufficient OLAS to migrate, ${minimumOlasRequiredToMigrate} OLAS required in total.`; + } + + if (!isAppVersionCompatible) { + return 'Pearl update required to migrate'; + } + + if (serviceStatus === DeploymentStatus.DEPLOYED) { + return 'Service is currently running'; + } + + if (serviceStatus === DeploymentStatus.DEPLOYING) { + return 'Service is currently deploying'; + } + + if (serviceStatus === DeploymentStatus.STOPPING) { + return 'Service is currently stopping'; + } + + if (!isServiceStakedForMinimumDuration) { + return 'Service has not been staked for the minimum duration'; + } + }, [ + activeStakingProgram, + hasEnoughOlasToMigrate, + hasEnoughRewards, + hasEnoughSlots, + isAppVersionCompatible, + isBalanceLoaded, + isSelected, + isServiceStakedForMinimumDuration, + serviceStatus, + ]); + + const cantMigrateAlert = useMemo(() => { + if (isSelected || !isBalanceLoaded) { + return null; + } + + if (!hasEnoughSlots) { + return ; + } + + if (!hasEnoughOlasToMigrate) { + return ( + + ); + } + + if (!isAppVersionCompatible) { + return ; + } + }, [ + isSelected, + isBalanceLoaded, + totalOlasBalance, + hasEnoughSlots, + hasEnoughOlasToMigrate, + isAppVersionCompatible, + ]); + + const contractTagStatus = useMemo(() => { + if (activeStakingProgram === stakingProgram) + return StakingProgramStatus.Selected; + + // Pearl is not staked, set as Selected if default (Beta) + if (!activeStakingProgram && stakingProgram === defaultStakingProgram) + return StakingProgramStatus.Selected; + + // Otherwise, highlight Beta as New + if (stakingProgram === StakingProgram.Beta) return StakingProgramStatus.New; + + // Otherwise, no tag + return; + }, [activeStakingProgram, defaultStakingProgram, stakingProgram]); + + return ( + + {/* Title */} + + {`${activeStakingProgramMeta.name} contract`} + {/* TODO: pass `status` attribute */} + + {!isSelected && ( + // here instead of isSelected we should check that the contract is not the old staking contract + // but the one from staking factory (if we want to open govern) + + Contract details {UNICODE_SYMBOLS.EXTERNAL_LINK} + + )} + + + {/* TODO: fix */} + + {/* Contract details + {stakingContractInfo?.availableRewards && ( + + )} + + {stakingContractInfo?.minStakingDeposit && ( + + )} */} + + {cantMigrateAlert} + {/* Switch to program button */} + {!isSelected && ( + + + + )} + + ); +}; diff --git a/frontend/components/ManageStakingPage/WhatAreStakingContracts.tsx b/frontend/components/ManageStakingPage/WhatAreStakingContracts.tsx new file mode 100644 index 00000000..f06f2ace --- /dev/null +++ b/frontend/components/ManageStakingPage/WhatAreStakingContracts.tsx @@ -0,0 +1,43 @@ +import { Collapse, Flex, Typography } from 'antd'; + +import { CardSection } from '../styled/CardSection'; + +const { Text } = Typography; + +const collapseItems = [ + { + key: 1, + label: What are staking contracts?, + children: ( + + + When your agent goes to work, it participates in staking contracts. + + + Staking contracts define what the agent needs to do, how much OLAS + needs to be staked etc. + + + Your agent can only participate in one staking contract at a time. + + + You need to run your agent for max 1 hour a day, regardless of the + staking contract. + + + ), + }, +]; + +export const WhatAreStakingContractsSection = () => { + return ( + + + + ); +}; diff --git a/frontend/components/ManageStakingPage/index.tsx b/frontend/components/ManageStakingPage/index.tsx new file mode 100644 index 00000000..310d19ec --- /dev/null +++ b/frontend/components/ManageStakingPage/index.tsx @@ -0,0 +1,40 @@ +import { CloseOutlined } from '@ant-design/icons'; +import { Button, Card } from 'antd'; + +import { Chain } from '@/client'; +import { SERVICE_STAKING_TOKEN_MECH_USAGE_CONTRACT_ADDRESSES } from '@/constants/contractAddresses'; +import { Pages } from '@/enums/PageState'; +import { StakingProgram } from '@/enums/StakingProgram'; +import { usePageState } from '@/hooks/usePageState'; + +import { CardTitle } from '../Card/CardTitle'; +import { StakingContractSection } from './StakingContractSection'; +import { WhatAreStakingContractsSection } from './WhatAreStakingContracts'; + +export const ManageStakingPage = () => { + const { goto } = usePageState(); + return ( + } + bordered={false} + extra={ + + + ); +}; diff --git a/frontend/components/Settings/index.tsx b/frontend/components/SettingsPage/index.tsx similarity index 84% rename from frontend/components/Settings/index.tsx rename to frontend/components/SettingsPage/index.tsx index 81a0a016..3170d1e5 100644 --- a/frontend/components/Settings/index.tsx +++ b/frontend/components/SettingsPage/index.tsx @@ -4,18 +4,19 @@ import Link from 'next/link'; import { useMemo } from 'react'; import { UNICODE_SYMBOLS } from '@/constants/symbols'; -import { PageState } from '@/enums/PageState'; +import { Pages } from '@/enums/PageState'; import { SettingsScreen } from '@/enums/SettingsScreen'; import { useMasterSafe } from '@/hooks/useMasterSafe'; import { usePageState } from '@/hooks/usePageState'; import { useSettings } from '@/hooks/useSettings'; import { truncateAddress } from '@/utils/truncate'; -import { Alert } from '../Alert'; +import { CustomAlert } from '../Alert'; import { CardTitle } from '../Card/CardTitle'; import { CardSection } from '../styled/CardSection'; -import { DebugInfoCard } from './DebugInfoCard'; -import { SettingsAddBackupWallet } from './SettingsAddBackupWallet'; +import { AddBackupWalletPage } from './AddBackupWalletPage'; +import { DebugInfoSection } from './DebugInfoSection'; +import { SettingsStakingContractSection } from './SettingsStakingContractSection'; const { Text, Paragraph } = Typography; @@ -37,7 +38,7 @@ export const Settings = () => { case SettingsScreen.Main: return ; case SettingsScreen.AddBackupWallet: - return ; + return ; default: return null; } @@ -64,17 +65,18 @@ const SettingsMain = () => {