diff --git a/api.md b/api.md index c199f57fa..5160e55df 100644 --- a/api.md +++ b/api.md @@ -313,6 +313,7 @@ Creates a Gnosis safe for given chain. ```js { "chain": Chain, + "fund_amount": 10000000000000000 } ``` diff --git a/electron/main.js b/electron/main.js index 4157b081d..6cabd7544 100644 --- a/electron/main.js +++ b/electron/main.js @@ -14,7 +14,6 @@ const os = require('os'); const next = require('next/dist/server/next'); const http = require('http'); const AdmZip = require('adm-zip'); -const { validateEnv } = require('./utils/env-validation'); const { setupDarwin, setupUbuntu, setupWindows, Env } = require('./install'); @@ -26,6 +25,7 @@ const { setupStoreIpc } = require('./store'); const { logger } = require('./logger'); const { isDev } = require('./constants'); const { PearlTray } = require('./components/PearlTray'); +const { Scraper } = require('agent-twitter-client'); // Validates environment variables required for Pearl // kills the app/process if required environment variables are unavailable @@ -256,6 +256,24 @@ const createMainWindow = async () => { ipcMain.handle('app-version', () => app.getVersion()); + // Handle twitter login + ipcMain.handle('validate-twitter-login', async (_event, credentials) => { + const scraper = new Scraper(); + + const { username, password, email } = credentials; + if (!username || !password || !email) { + return { success: false, error: 'Missing credentials' }; + } + + try { + await scraper.login(username, password, email); + return { success: true }; + } catch (error) { + console.error('Twitter login error:', error); + return { success: false, error: error.message }; + } + }); + mainWindow.webContents.on('did-fail-load', () => { mainWindow.webContents.reloadIgnoringCache(); }); @@ -277,7 +295,7 @@ const createMainWindow = async () => { try { logger.electron('Setting up store IPC'); - await setupStoreIpc(ipcMain, mainWindow); + setupStoreIpc(ipcMain, mainWindow); } catch (e) { logger.electron('Store IPC failed:', JSON.stringify(e)); } diff --git a/electron/preload.js b/electron/preload.js index f3143a1ad..93e1ff0cd 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -25,4 +25,6 @@ contextBridge.exposeInMainWorld('electronAPI', { saveLogs: (data) => ipcRenderer.invoke('save-logs', data), openPath: (filePath) => ipcRenderer.send('open-path', filePath), getAppVersion: () => ipcRenderer.invoke('app-version'), + validateTwitterLogin: (credentials) => + ipcRenderer.invoke('validate-twitter-login', credentials), }); diff --git a/electron/public/agent-memeooorr-icon.png b/electron/public/agent-memeooorr-icon.png new file mode 100644 index 000000000..fb2f72fad Binary files /dev/null and b/electron/public/agent-memeooorr-icon.png differ diff --git a/electron/public/agent-trader-icon.png b/electron/public/agent-trader-icon.png new file mode 100644 index 000000000..5d59f472a Binary files /dev/null and b/electron/public/agent-trader-icon.png differ diff --git a/electron/store.js b/electron/store.js index eafd9428a..a09727779 100644 --- a/electron/store.js +++ b/electron/store.js @@ -8,6 +8,9 @@ const schema = { environmentName: { type: 'string', default: '' }, currentStakingProgram: { type: 'string', default: '' }, + + // agent settings + lastSelectedAgentType: { type: 'string', default: 'trader' }, }; /** @@ -16,7 +19,7 @@ const schema = { * @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 setupStoreIpc = (ipcMain, mainWindow) => { const store = new Store({ schema }); store.onDidAnyChange((data) => { diff --git a/frontend/abis/memeActivityChecker.ts b/frontend/abis/memeActivityChecker.ts new file mode 100644 index 000000000..859316b57 --- /dev/null +++ b/frontend/abis/memeActivityChecker.ts @@ -0,0 +1,44 @@ +export const MEME_ACTIVITY_CHECKER_ABI = [ + { + inputs: [ + { internalType: 'address', name: '_memeFactory', type: 'address' }, + { internalType: 'uint256', name: '_livenessRatio', type: 'uint256' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { inputs: [], name: 'ZeroAddress', type: 'error' }, + { inputs: [], name: 'ZeroValue', type: 'error' }, + { + inputs: [{ internalType: 'address', name: 'multisig', type: 'address' }], + name: 'getMultisigNonces', + outputs: [{ internalType: 'uint256[]', name: 'nonces', type: 'uint256[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256[]', name: 'curNonces', type: 'uint256[]' }, + { internalType: 'uint256[]', name: 'lastNonces', type: 'uint256[]' }, + { internalType: 'uint256', name: 'ts', type: 'uint256' }, + ], + name: 'isRatioPass', + outputs: [{ internalType: 'bool', name: 'ratioPass', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'livenessRatio', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'memeFactory', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, +]; diff --git a/frontend/client/types.ts b/frontend/client/types.ts index 208584595..7c8113167 100644 --- a/frontend/client/types.ts +++ b/frontend/client/types.ts @@ -90,7 +90,7 @@ export type ConfigurationTemplate = { agent_id: number; threshold: number; use_staking: boolean; - use_mech_marketplace: boolean; + use_mech_marketplace?: boolean; cost_of_bond: number; monthly_gas_estimate: number; fund_requirements: FundRequirementsTemplate; diff --git a/frontend/components/AddressLink.tsx b/frontend/components/AddressLink.tsx index d26fa0235..3f005a59f 100644 --- a/frontend/components/AddressLink.tsx +++ b/frontend/components/AddressLink.tsx @@ -4,18 +4,27 @@ import { EXPLORER_URL_BY_MIDDLEWARE_CHAIN } from '@/constants/urls'; import { Address } from '@/types/Address'; import { truncateAddress } from '@/utils/truncate'; -type AddressLinkProps = { address?: Address; hideLinkArrow?: boolean }; +type AddressLinkProps = { + address?: Address; + hideLinkArrow?: boolean; + + // TODO: mark as required once balance breakdown is updated. + // and remove the default value + middlewareChain?: MiddlewareChain; +}; export const AddressLink = ({ address, hideLinkArrow = false, + middlewareChain = MiddlewareChain.GNOSIS, }: AddressLinkProps) => { if (!address) return null; + if (!middlewareChain) return null; return ( {truncateAddress(address)} diff --git a/frontend/components/AgentSelection.tsx b/frontend/components/AgentSelection.tsx new file mode 100644 index 000000000..e874d9f2b --- /dev/null +++ b/frontend/components/AgentSelection.tsx @@ -0,0 +1,146 @@ +import { Button, Card, Flex, Typography } from 'antd'; +import { entries } from 'lodash'; +import Image from 'next/image'; +import { memo, useCallback } from 'react'; + +import { AGENT_CONFIG } from '@/config/agents'; +import { COLOR } from '@/constants/colors'; +import { AgentType } from '@/enums/Agent'; +import { Pages } from '@/enums/Pages'; +import { SetupScreen } from '@/enums/SetupScreen'; +import { usePageState } from '@/hooks/usePageState'; +import { useServices } from '@/hooks/useServices'; +import { useSetup } from '@/hooks/useSetup'; +import { useMasterWalletContext } from '@/hooks/useWallet'; +import { AgentConfig } from '@/types/Agent'; +import { delayInSeconds } from '@/utils/delay'; + +import { SetupCreateHeader } from './SetupPage/Create/SetupCreateHeader'; +import { CardFlex } from './styled/CardFlex'; + +const { Title, Text } = Typography; + +type EachAgentProps = { + showSelected: boolean; + agentType: AgentType; + agentConfig: AgentConfig; +}; + +const EachAgent = memo( + ({ showSelected, agentType, agentConfig }: EachAgentProps) => { + const { goto: gotoSetup } = useSetup(); + const { goto: gotoPage } = usePageState(); + const { selectedAgentType, updateAgentType } = useServices(); + const { masterSafes, isLoading } = useMasterWalletContext(); + + const isCurrentAgent = showSelected + ? selectedAgentType === agentType + : false; + + const handleSelectAgent = useCallback(async () => { + updateAgentType(agentType); + + // DO NOTE REMOVE THIS DELAY + // NOTE: the delay is added so that agentType is updated in electron store + // and re-rendered with the updated agentType + await delayInSeconds(0.5); + + const isSafeCreated = masterSafes?.find( + (masterSafe) => + masterSafe.evmChainId === AGENT_CONFIG[agentType].evmHomeChainId, + ); + + // If safe is created for the agent type, then go to main page + if (isSafeCreated) { + gotoPage(Pages.Main); + return; + } + + // If safe is NOT created, then go to setup page based on the agent type + if (agentType === AgentType.Memeooorr) { + // if the selected type is Memeooorr - should set up the agent first + gotoPage(Pages.Setup); + gotoSetup(SetupScreen.SetupYourAgent); + } else if (agentType === AgentType.PredictTrader) { + gotoPage(Pages.Setup); + gotoSetup(SetupScreen.SetupEoaFunding); + } + }, [agentType, gotoPage, gotoSetup, masterSafes, updateAgentType]); + + return ( + + + + {agentConfig.displayName} + {isCurrentAgent ? ( + Selected Agent + ) : ( + + )} + + + + + {agentConfig.displayName} + + + {agentConfig.description} + + ); + }, +); + +EachAgent.displayName = 'EachAgent'; + +type AgentSelectionProps = { + showSelected?: boolean; + canGoBack?: boolean; + onPrev?: () => void; +}; + +/** + * Component to select the agent type. + */ +export const AgentSelection = ({ + showSelected = true, + onPrev, +}: AgentSelectionProps) => ( + + + Select your agent + + {entries(AGENT_CONFIG).map(([agentType, agentConfig]) => { + return ( + + ); + })} + +); diff --git a/frontend/components/FeatureNotEnabled.tsx b/frontend/components/FeatureNotEnabled.tsx new file mode 100644 index 000000000..a8d32fe15 --- /dev/null +++ b/frontend/components/FeatureNotEnabled.tsx @@ -0,0 +1,11 @@ +import { Alert } from 'antd'; + +export const FeatureNotEnabled = () => ( + +); diff --git a/frontend/components/MainPage/MainHeader/FirstRunModal.tsx b/frontend/components/MainPage/MainHeader/FirstRunModal.tsx deleted file mode 100644 index f52543f53..000000000 --- a/frontend/components/MainPage/MainHeader/FirstRunModal.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Button, Flex, Modal, Typography } from 'antd'; -import Image from 'next/image'; -import { FC } from 'react'; - -import { MODAL_WIDTH } from '@/constants/width'; -import { TokenSymbol } from '@/enums/Token'; -import { useStakingProgram } from '@/hooks/useStakingProgram'; - -type FirstRunModalProps = { open: boolean; onClose: () => void }; - -export const FirstRunModal: FC = ({ open, onClose }) => { - const { activeStakingProgramMeta } = useStakingProgram(); - - const minimumStakedAmountRequired = - activeStakingProgramMeta?.stakingRequirements?.[TokenSymbol.OLAS]; - - return ( - - Got it - , - ]} - > - - 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/MainPage/header/AgentButton/AgentButton.tsx b/frontend/components/MainPage/header/AgentButton/AgentButton.tsx index 4457edb90..c3db88f29 100644 --- a/frontend/components/MainPage/header/AgentButton/AgentButton.tsx +++ b/frontend/components/MainPage/header/AgentButton/AgentButton.tsx @@ -17,11 +17,8 @@ import { AgentStartingButton } from './AgentStartingButton'; import { AgentStoppingButton } from './AgentStoppingButton'; export const AgentButton = () => { - const { - selectedService, - isFetched: isServicesLoaded, - selectedServiceStatusOverride, - } = useServices(); + const { selectedService, isFetched, selectedServiceStatusOverride } = + useServices(); const { isEligibleForStaking, @@ -30,7 +27,7 @@ export const AgentButton = () => { } = useActiveStakingContractInfo(); const button = useMemo(() => { - if (!isServicesLoaded || !isSelectedStakingContractDetailsLoaded) { + if (!isFetched || !isSelectedStakingContractDetailsLoaded) { return ( + + + ); +}; diff --git a/frontend/components/MainPage/sections/AlertSections/LowFunds/LowFunds.tsx b/frontend/components/MainPage/sections/AlertSections/LowFunds/LowFunds.tsx new file mode 100644 index 000000000..b29a5f542 --- /dev/null +++ b/frontend/components/MainPage/sections/AlertSections/LowFunds/LowFunds.tsx @@ -0,0 +1,87 @@ +import { round } from 'lodash'; +import { useMemo } from 'react'; + +import { CHAIN_CONFIG } from '@/config/chains'; +import { WalletOwnerType, WalletType } from '@/enums/Wallet'; +import { useMasterBalances } from '@/hooks/useBalanceContext'; +import { useNeedsFunds } from '@/hooks/useNeedsFunds'; +import { useServices } from '@/hooks/useServices'; +import { useStakingProgram } from '@/hooks/useStakingProgram'; +import { useStore } from '@/hooks/useStore'; + +import { EmptyFunds } from './EmptyFunds'; +import { LowOperatingBalanceAlert } from './LowOperatingBalanceAlert'; +import { LowSafeSignerBalanceAlert } from './LowSafeSignerBalanceAlert'; +import { MainNeedsFunds } from './MainNeedsFunds'; + +export const LowFunds = () => { + const { storeState } = useStore(); + + const { selectedAgentConfig } = useServices(); + const { selectedStakingProgramId } = useStakingProgram(); + const { isLoaded: isBalanceLoaded, masterEoaNativeGasBalance } = + useMasterBalances(); + + const { nativeBalancesByChain, olasBalancesByChain, isInitialFunded } = + useNeedsFunds(selectedStakingProgramId); + + const chainId = selectedAgentConfig.evmHomeChainId; + + // Check if the safe signer balance is low + const isSafeSignerBalanceLow = useMemo(() => { + if (!isBalanceLoaded) return false; + if (!masterEoaNativeGasBalance) return false; + if (!storeState?.isInitialFunded) return false; + + return ( + masterEoaNativeGasBalance < + selectedAgentConfig.operatingThresholds[WalletOwnerType.Master][ + WalletType.EOA + ][CHAIN_CONFIG[selectedAgentConfig.evmHomeChainId].nativeToken.symbol] + ); + }, [ + isBalanceLoaded, + masterEoaNativeGasBalance, + selectedAgentConfig.evmHomeChainId, + selectedAgentConfig.operatingThresholds, + storeState?.isInitialFunded, + ]); + + // Show the empty funds alert if the agent is not funded + const isEmptyFundsVisible = useMemo(() => { + if (!isBalanceLoaded) return false; + if (!olasBalancesByChain) return false; + if (!nativeBalancesByChain) return false; + + // If the agent is not funded, will be displayed + if (!isInitialFunded) return false; + + if ( + round(nativeBalancesByChain[chainId], 2) === 0 && + round(olasBalancesByChain[chainId], 2) === 0 && + isSafeSignerBalanceLow + ) { + return true; + } + + return false; + }, [ + isBalanceLoaded, + isInitialFunded, + chainId, + nativeBalancesByChain, + olasBalancesByChain, + isSafeSignerBalanceLow, + ]); + + return ( + <> + {isEmptyFundsVisible && } + + + {!isEmptyFundsVisible && isSafeSignerBalanceLow && ( + + )} + + ); +}; diff --git a/frontend/components/MainPage/sections/AlertSections/LowFunds/LowOperatingBalanceAlert.tsx b/frontend/components/MainPage/sections/AlertSections/LowFunds/LowOperatingBalanceAlert.tsx new file mode 100644 index 000000000..de16700ad --- /dev/null +++ b/frontend/components/MainPage/sections/AlertSections/LowFunds/LowOperatingBalanceAlert.tsx @@ -0,0 +1,79 @@ +import { Flex, Typography } from 'antd'; +import { useMemo } from 'react'; + +import { CustomAlert } from '@/components/Alert'; +import { CHAIN_CONFIG } from '@/config/chains'; +import { WalletOwnerType, WalletType } from '@/enums/Wallet'; +import { useMasterBalances } from '@/hooks/useBalanceContext'; +import { useServices } from '@/hooks/useServices'; +import { useStore } from '@/hooks/useStore'; + +import { InlineBanner } from './InlineBanner'; +import { useLowFundsDetails } from './useLowFunds'; + +const { Text, Title } = Typography; + +export const LowOperatingBalanceAlert = () => { + const { storeState } = useStore(); + const { selectedAgentConfig } = useServices(); + const { isLoaded: isBalanceLoaded, masterSafeNativeGasBalance } = + useMasterBalances(); + + const { chainName, tokenSymbol, masterSafeAddress } = useLowFundsDetails(); + + const isLowBalance = useMemo(() => { + if (!masterSafeNativeGasBalance) return false; + return ( + masterSafeNativeGasBalance < + selectedAgentConfig.operatingThresholds[WalletOwnerType.Master][ + WalletType.Safe + ][CHAIN_CONFIG[selectedAgentConfig.evmHomeChainId].nativeToken.symbol] + ); + }, [ + masterSafeNativeGasBalance, + selectedAgentConfig.evmHomeChainId, + selectedAgentConfig.operatingThresholds, + ]); + + if (!isBalanceLoaded) return null; + if (!storeState?.isInitialFunded) return; + if (!isLowBalance) return null; + + return ( + + + Operating balance is too low + + + To run your agent, add at least + {` ${ + selectedAgentConfig.operatingThresholds[WalletOwnerType.Master][ + WalletType.Safe + ][ + CHAIN_CONFIG[selectedAgentConfig.evmHomeChainId].nativeToken + .symbol + ] + } ${tokenSymbol} `} + on {chainName} chain to your safe. + + + Your agent is at risk of missing its targets, which would result in + several days' suspension. + + + {masterSafeAddress && ( + + )} + + } + /> + ); +}; diff --git a/frontend/components/MainPage/sections/AlertSections/LowFunds/LowSafeSignerBalanceAlert.tsx b/frontend/components/MainPage/sections/AlertSections/LowFunds/LowSafeSignerBalanceAlert.tsx new file mode 100644 index 000000000..96a5122a7 --- /dev/null +++ b/frontend/components/MainPage/sections/AlertSections/LowFunds/LowSafeSignerBalanceAlert.tsx @@ -0,0 +1,48 @@ +import { Flex, Typography } from 'antd'; + +import { CustomAlert } from '@/components/Alert'; +import { WalletOwnerType, WalletType } from '@/enums/Wallet'; +import { useServices } from '@/hooks/useServices'; + +import { InlineBanner } from './InlineBanner'; +import { useLowFundsDetails } from './useLowFunds'; + +const { Text, Title } = Typography; + +export const LowSafeSignerBalanceAlert = () => { + const { chainName, tokenSymbol, masterEoaAddress } = useLowFundsDetails(); + const { selectedAgentConfig } = useServices(); + + return ( + + + Safe signer balance is too low + + + To keep your agent operational, add + {` ${selectedAgentConfig.operatingThresholds[WalletOwnerType.Master][WalletType.EOA][tokenSymbol]} ${tokenSymbol} `} + on {chainName} chain to the safe signer. + + + Your agent is at risk of missing its targets, which would result in + several days' suspension. + + + {masterEoaAddress && ( + + )} + + } + /> + ); +}; diff --git a/frontend/components/MainPage/sections/AlertSections/LowFunds/MainNeedsFunds.tsx b/frontend/components/MainPage/sections/AlertSections/LowFunds/MainNeedsFunds.tsx new file mode 100644 index 000000000..bc5c9763b --- /dev/null +++ b/frontend/components/MainPage/sections/AlertSections/LowFunds/MainNeedsFunds.tsx @@ -0,0 +1,61 @@ +import { Flex, Typography } from 'antd'; +import { useEffect } from 'react'; + +import { CustomAlert } from '@/components/Alert'; +import { useElectronApi } from '@/hooks/useElectronApi'; +import { useNeedsFunds } from '@/hooks/useNeedsFunds'; +import { useStakingProgram } from '@/hooks/useStakingProgram'; + +import { FundsToActivate } from './FundsToActivate'; + +const { Title } = Typography; + +export const MainNeedsFunds = () => { + const { selectedStakingProgramId } = useStakingProgram(); + + const { + hasEnoughEthForInitialFunding, + hasEnoughOlasForInitialFunding, + isInitialFunded, + needsInitialFunding, + } = useNeedsFunds(selectedStakingProgramId); + + // update the store when the agent is funded + const electronApi = useElectronApi(); + useEffect(() => { + if ( + hasEnoughEthForInitialFunding && + hasEnoughOlasForInitialFunding && + !isInitialFunded + ) { + electronApi.store?.set?.('isInitialFunded', true); + } + }, [ + electronApi.store, + hasEnoughEthForInitialFunding, + hasEnoughOlasForInitialFunding, + isInitialFunded, + ]); + + if (!needsInitialFunding) return null; + + return ( + + + Fund your agent + + + + + } + type="primary" + /> + ); +}; diff --git a/frontend/components/MainPage/sections/AlertSections/LowFunds/useLowFunds.ts b/frontend/components/MainPage/sections/AlertSections/LowFunds/useLowFunds.ts new file mode 100644 index 000000000..b97c8f5d8 --- /dev/null +++ b/frontend/components/MainPage/sections/AlertSections/LowFunds/useLowFunds.ts @@ -0,0 +1,37 @@ +import { useMemo } from 'react'; + +import { CHAIN_CONFIG } from '@/config/chains'; +import { useServices } from '@/hooks/useServices'; +import { useMasterWalletContext } from '@/hooks/useWallet'; + +/** + * helper hook specific to get low funds details such as + * - chain information + * - master/eoa safe addresses + */ +export const useLowFundsDetails = () => { + const { selectedAgentConfig } = useServices(); + const homeChainId = selectedAgentConfig.evmHomeChainId; + + const { masterEoa, masterSafes } = useMasterWalletContext(); + + // master safe details + const selectedMasterSafe = useMemo(() => { + if (!masterSafes) return; + if (!homeChainId) return; + + return masterSafes.find( + (masterSafe) => masterSafe.evmChainId === homeChainId, + ); + }, [masterSafes, homeChainId]); + + // current chain details + const { name: chainName, nativeToken } = CHAIN_CONFIG[homeChainId]; + + return { + chainName, + tokenSymbol: nativeToken.symbol, + masterSafeAddress: selectedMasterSafe?.address, + masterEoaAddress: masterEoa?.address, + }; +}; diff --git a/frontend/components/MainPage/sections/AlertSections/LowTradingBalanceAlert.tsx b/frontend/components/MainPage/sections/AlertSections/LowTradingBalanceAlert.tsx deleted file mode 100644 index 1993859ac..000000000 --- a/frontend/components/MainPage/sections/AlertSections/LowTradingBalanceAlert.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Flex, Typography } from 'antd'; - -import { CustomAlert } from '@/components/Alert'; -import { LOW_MASTER_SAFE_BALANCE } from '@/constants/thresholds'; -import { useBalanceContext } from '@/hooks/useBalanceContext'; -import { useStore } from '@/hooks/useStore'; - -const { Text, Title } = Typography; - -export const LowTradingBalanceAlert = () => { - const { isLoaded: isBalanceLoaded, isLowBalance } = useBalanceContext(); - const { storeState } = useStore(); - - if (!isBalanceLoaded) return null; - if (!storeState?.isInitialFunded) return; - if (!isLowBalance) return null; - - return ( - - - Trading balance is too low - - - {`To run your agent, add at least $${LOW_MASTER_SAFE_BALANCE} XDAI to your account.`} - - - Your agent is at risk of missing its targets, which would result in - several days' suspension. - - - } - /> - ); -}; diff --git a/frontend/components/MainPage/sections/AlertSections/index.tsx b/frontend/components/MainPage/sections/AlertSections/index.tsx index a8c70417c..4799f03b9 100644 --- a/frontend/components/MainPage/sections/AlertSections/index.tsx +++ b/frontend/components/MainPage/sections/AlertSections/index.tsx @@ -1,20 +1,23 @@ import { CardSection } from '@/components/styled/CardSection'; +import { useFeatureFlag } from '@/hooks/useFeatureFlag'; import { AddBackupWalletAlert } from './AddBackupWalletAlert'; import { AvoidSuspensionAlert } from './AvoidSuspensionAlert'; -import { LowTradingBalanceAlert } from './LowTradingBalanceAlert'; +import { LowFunds } from './LowFunds/LowFunds'; import { NewStakingProgramAlert } from './NewStakingProgramAlert'; import { NoAvailableSlotsOnTheContract } from './NoAvailableSlotsOnTheContract'; import { UpdateAvailableAlert } from './UpdateAvailableAlert'; export const AlertSections = () => { + const isLowFundsEnabled = useFeatureFlag('low-funds'); + return ( - + {isLowFundsEnabled && } ); diff --git a/frontend/components/MainPage/sections/GasBalanceSection.tsx b/frontend/components/MainPage/sections/GasBalanceSection.tsx index a523c6b92..3428e0267 100644 --- a/frontend/components/MainPage/sections/GasBalanceSection.tsx +++ b/frontend/components/MainPage/sections/GasBalanceSection.tsx @@ -14,6 +14,7 @@ import { useElectronApi } from '@/hooks/useElectronApi'; import { useServices } from '@/hooks/useServices'; import { useStore } from '@/hooks/useStore'; import { useMasterWalletContext } from '@/hooks/useWallet'; +import { asMiddlewareChain } from '@/utils/middlewareHelpers'; import { CardSection } from '../../styled/CardSection'; @@ -57,7 +58,7 @@ const BalanceStatus = () => { if (!storeState?.isInitialFunded) return; if (isMasterSafeLowOnNativeGas && !isLowBalanceNotificationShown) { - showNotification('Trading balance is too low.'); + showNotification('Operating balance is too low.'); setIsLowBalanceNotificationShown(true); } @@ -116,6 +117,22 @@ export const GasBalanceSection = () => { return masterSafes.find((wallet) => wallet.evmChainId === homeChainId); }, [homeChainId, masterSafes]); + const activityLink = useMemo(() => { + if (!masterSafe) return; + + const link = + EXPLORER_URL_BY_MIDDLEWARE_CHAIN[asMiddlewareChain(homeChainId)] + + '/address/' + + masterSafe.address; + + return ( + + Track activity on blockchain explorer{' '} + + + ); + }, [masterSafe, homeChainId]); + return ( { padding="16px 24px" > - Trading balance  + Operating balance  {masterSafe && ( - Your agent uses this balance to fund trading activity on-chain. + Your agent uses this balance to fund on-chain activity.
- - Track activity on blockchain explorer{' '} - - + {activityLink} } > diff --git a/frontend/components/MainPage/sections/NeedsFundsSection.tsx b/frontend/components/MainPage/sections/NeedsFundsSection.tsx deleted file mode 100644 index 0f447a6c0..000000000 --- a/frontend/components/MainPage/sections/NeedsFundsSection.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Flex, Typography } from 'antd'; -import { ReactNode, useEffect, useMemo } from 'react'; -import styled from 'styled-components'; - -import { CustomAlert } from '@/components/Alert'; -import { getNativeTokenSymbol } from '@/config/tokens'; -import { UNICODE_SYMBOLS } from '@/constants/symbols'; -import { TokenSymbol } from '@/enums/Token'; -import { useElectronApi } from '@/hooks/useElectronApi'; -import { useNeedsFunds } from '@/hooks/useNeedsFunds'; -import { useServices } from '@/hooks/useServices'; -import { useStakingProgram } from '@/hooks/useStakingProgram'; - -import { CardSection } from '../../styled/CardSection'; - -const { Text } = Typography; -const COVER_PREV_BLOCK_BORDER_STYLE = { marginTop: '-1px' }; - -const FundingValue = styled.div` - font-size: 24px; - font-weight: 700; - line-height: 32px; - letter-spacing: -0.72px; -`; - -export const MainNeedsFunds = () => { - const { selectedStakingProgramId } = useStakingProgram(); - - const { - hasEnoughEthForInitialFunding, - hasEnoughOlasForInitialFunding, - serviceFundRequirements, - isInitialFunded, - needsInitialFunding, - } = useNeedsFunds(selectedStakingProgramId); - - const { selectedAgentConfig } = useServices(); - const { evmHomeChainId: homeChainId } = selectedAgentConfig; - const nativeTokenSymbol = getNativeTokenSymbol(homeChainId); - - const electronApi = useElectronApi(); - - const message: ReactNode = useMemo( - () => ( - - Your agent needs funds - - {!hasEnoughOlasForInitialFunding && ( -
- {`${UNICODE_SYMBOLS.OLAS}${serviceFundRequirements[homeChainId][TokenSymbol.OLAS]} OLAS `} - for staking -
- )} - {!hasEnoughEthForInitialFunding && ( -
- - {`$${serviceFundRequirements[homeChainId][nativeTokenSymbol]} ${nativeTokenSymbol} `} - - for trading -
- )} -
-
    -
  • Use the address in the “Add Funds” section below.
  • -
-
- ), - [ - hasEnoughEthForInitialFunding, - hasEnoughOlasForInitialFunding, - homeChainId, - nativeTokenSymbol, - serviceFundRequirements, - ], - ); - - useEffect(() => { - if ( - hasEnoughEthForInitialFunding && - hasEnoughOlasForInitialFunding && - !isInitialFunded - ) { - electronApi.store?.set?.('isInitialFunded', true); - } - }, [ - electronApi.store, - hasEnoughEthForInitialFunding, - hasEnoughOlasForInitialFunding, - isInitialFunded, - ]); - - if (!needsInitialFunding) return null; - - return ( - - - - ); -}; diff --git a/frontend/components/MainPage/sections/OlasBalanceSection.tsx b/frontend/components/MainPage/sections/OlasBalanceSection.tsx index cb034bae9..c7c1a09ad 100644 --- a/frontend/components/MainPage/sections/OlasBalanceSection.tsx +++ b/frontend/components/MainPage/sections/OlasBalanceSection.tsx @@ -1,5 +1,4 @@ -import { RightOutlined } from '@ant-design/icons'; -import { Flex, Skeleton, Typography } from 'antd'; +import { Button, Flex, Skeleton, Typography } from 'antd'; import { sum } from 'lodash'; import { useMemo } from 'react'; import styled from 'styled-components'; @@ -12,6 +11,7 @@ import { useMasterBalances, useServiceBalances, } from '@/hooks/useBalanceContext'; +import { useFeatureFlag } from '@/hooks/useFeatureFlag'; import { usePageState } from '@/hooks/usePageState'; import { useServices } from '@/hooks/useServices'; import { balanceFormat } from '@/utils/numberFormatters'; @@ -26,6 +26,7 @@ const Balance = styled.span` `; type MainOlasBalanceProps = { isBorderTopVisible?: boolean }; + export const MainOlasBalance = ({ isBorderTopVisible = true, }: MainOlasBalanceProps) => { @@ -36,6 +37,7 @@ export const MainOlasBalance = ({ selectedService?.service_config_id, ); const { goto } = usePageState(); + const isBalanceBreakdownEnabled = useFeatureFlag('manage-wallet'); const displayedBalance = useMemo(() => { // olas across master wallets, safes and eoa @@ -87,21 +89,24 @@ export const MainOlasBalance = ({ > {isBalanceLoaded ? ( - Current balance + + Current balance + {isBalanceBreakdownEnabled && ( + + )} + + {UNICODE_SYMBOLS.OLAS} {displayedBalance} OLAS - - goto(Pages.YourWalletBreakdown)} - > - See breakdown - - ) : ( diff --git a/frontend/components/MainPage/sections/RewardsSection/NotifyRewardsModal.tsx b/frontend/components/MainPage/sections/RewardsSection/NotifyRewardsModal.tsx index 7d5cf8bd2..fc8599b7f 100644 --- a/frontend/components/MainPage/sections/RewardsSection/NotifyRewardsModal.tsx +++ b/frontend/components/MainPage/sections/RewardsSection/NotifyRewardsModal.tsx @@ -3,6 +3,7 @@ import Image from 'next/image'; import { useCallback, useEffect, useRef, useState } from 'react'; import { NA } from '@/constants/symbols'; +import { OPERATE_URL } from '@/constants/urls'; import { useBalanceContext } from '@/hooks/useBalanceContext'; import { useElectronApi } from '@/hooks/useElectronApi'; import { useReward } from '@/hooks/useReward'; @@ -17,7 +18,6 @@ const getFormattedReward = (reward: number | undefined) => reward === undefined ? NA : `~${balanceFormat(reward, 2)}`; const SHARE_TEXT = `I just earned my first reward through the Operate app powered by #olas!\n\nDownload the Pearl app:`; -const OPERATE_URL = 'https://olas.network/operate?pearl=first-reward'; export const NotifyRewardsModal = () => { const { isEligibleForRewards, availableRewardsForEpochEth } = useReward(); @@ -60,7 +60,7 @@ export const NotifyRewardsModal = () => { const onTwitterShare = useCallback(() => { const encodedText = encodeURIComponent(SHARE_TEXT); - const encodedURL = encodeURIComponent(OPERATE_URL); + const encodedURL = encodeURIComponent(`${OPERATE_URL}?pearl=first-reward`); window.open( `https://twitter.com/intent/tweet?text=${encodedText}&url=${encodedURL}`, diff --git a/frontend/components/MainPage/sections/RewardsSection/RewardsStreak.tsx b/frontend/components/MainPage/sections/RewardsSection/RewardsStreak.tsx index 620871029..746dfb385 100644 --- a/frontend/components/MainPage/sections/RewardsSection/RewardsStreak.tsx +++ b/frontend/components/MainPage/sections/RewardsSection/RewardsStreak.tsx @@ -1,11 +1,13 @@ -import { RightOutlined } from '@ant-design/icons'; -import { Flex, Skeleton, Typography } from 'antd'; +import { RightOutlined, ShareAltOutlined } from '@ant-design/icons'; +import { Button, Flex, Skeleton, Tooltip, Typography } from 'antd'; +import { useCallback } from 'react'; import styled from 'styled-components'; import { FireNoStreak } from '@/components/custom-icons/FireNoStreak'; import { FireStreak } from '@/components/custom-icons/FireStreak'; import { COLOR } from '@/constants/colors'; import { NA } from '@/constants/symbols'; +import { OPERATE_URL } from '@/constants/urls'; import { Pages } from '@/enums/Pages'; import { useBalanceContext } from '@/hooks/useBalanceContext'; import { usePageState } from '@/hooks/usePageState'; @@ -19,6 +21,7 @@ const RewardsStreakFlex = styled(Flex)` height: 40px; background: ${COLOR.GRAY_1}; border-radius: 6px; + margin-bottom: 16px; `; const Streak = () => { @@ -30,6 +33,22 @@ const Streak = () => { isError, } = useRewardsHistory(); + // Graph does not account for the current day, + // so we need to add 1 to the streak, if the user is eligible for rewards + const optimisticStreak = isEligibleForRewards ? streak + 1 : streak; + + const onStreakShare = useCallback(() => { + const encodedText = encodeURIComponent( + `🎉 I've just completed a ${optimisticStreak}-day streak with my agent on Pearl and earned OLAS every single day! 🏆 How long can you keep your streak going? \n\nDownload the Pearl app:`, + ); + const encodedURL = encodeURIComponent(`${OPERATE_URL}?pearl=share-streak`); + + window.open( + `https://twitter.com/intent/tweet?text=${encodedText}&url=${encodedURL}`, + '_blank', + ); + }, [optimisticStreak]); + // If rewards history is loading for the first time // or balances are not fetched yet - show loading state if (isRewardsHistoryLoading || !isBalanceLoaded) { @@ -40,15 +59,22 @@ const Streak = () => { return NA; } - // Graph does not account for the current day, - // so we need to add 1 to the streak, if the user is eligible for rewards - const optimisticStreak = isEligibleForRewards ? streak + 1 : streak; - return ( - + {optimisticStreak > 0 ? ( <> {optimisticStreak} day streak + + +
+ ); +}; diff --git a/frontend/components/ManageStakingPage/StakingContractSection/CantMigrateAlert.tsx b/frontend/components/ManageStakingPage/StakingContractSection/CantMigrateAlert.tsx index e45283ac3..c504e44e3 100644 --- a/frontend/components/ManageStakingPage/StakingContractSection/CantMigrateAlert.tsx +++ b/frontend/components/ManageStakingPage/StakingContractSection/CantMigrateAlert.tsx @@ -3,10 +3,11 @@ import { isEmpty, isNil } from 'lodash'; import { useMemo } from 'react'; import { CustomAlert } from '@/components/Alert'; +import { CHAIN_CONFIG } from '@/config/chains'; import { getNativeTokenSymbol } from '@/config/tokens'; -import { LOW_MASTER_SAFE_BALANCE } from '@/constants/thresholds'; import { StakingProgramId } from '@/enums/StakingProgram'; import { TokenSymbol } from '@/enums/Token'; +import { WalletOwnerType, WalletType } from '@/enums/Wallet'; import { useBalanceContext, useMasterBalances, @@ -72,7 +73,10 @@ const AlertInsufficientMigrationFunds = ({ const homeChainId = selectedAgentConfig.evmHomeChainId; const nativeTokenSymbol = getNativeTokenSymbol(homeChainId); const requiredNativeTokenDeposit = isInitialFunded - ? LOW_MASTER_SAFE_BALANCE - (safeBalance[nativeTokenSymbol] || 0) // is already funded allow minimal maintenance + ? selectedAgentConfig.operatingThresholds[WalletOwnerType.Master][ + WalletType.Safe + ][CHAIN_CONFIG[selectedAgentConfig.evmHomeChainId].nativeToken.symbol] - + (safeBalance[nativeTokenSymbol] || 0) // is already funded allow minimal maintenance : (serviceFundRequirements[homeChainId]?.[nativeTokenSymbol] || 0) - (safeBalance[nativeTokenSymbol] || 0); // otherwise require full initial funding requirements diff --git a/frontend/components/ManageStakingPage/StakingContractSection/MigrateButton.tsx b/frontend/components/ManageStakingPage/StakingContractSection/MigrateButton.tsx index 5242f2bfe..1486fe2b1 100644 --- a/frontend/components/ManageStakingPage/StakingContractSection/MigrateButton.tsx +++ b/frontend/components/ManageStakingPage/StakingContractSection/MigrateButton.tsx @@ -35,11 +35,9 @@ export const MigrateButton = ({ setPaused: setIsServicePollingPaused, isFetched: isServicesLoaded, selectedService, - selectedAgentConfig, selectedAgentType, overrideSelectedServiceStatus, } = useServices(); - const { evmHomeChainId: homeChainId } = selectedAgentConfig; const serviceConfigId = isServicesLoaded && selectedService ? selectedService.service_config_id @@ -137,7 +135,6 @@ export const MigrateButton = ({ useMechMarketplace: stakingProgramIdToMigrateTo === StakingProgramId.PearlBetaMechMarketplace, - chainId: homeChainId, }; if (selectedService) { diff --git a/frontend/components/ManageStakingPage/StakingContractSection/StakingContractDetails.tsx b/frontend/components/ManageStakingPage/StakingContractSection/StakingContractDetails.tsx index 17506754e..634ee165a 100644 --- a/frontend/components/ManageStakingPage/StakingContractSection/StakingContractDetails.tsx +++ b/frontend/components/ManageStakingPage/StakingContractSection/StakingContractDetails.tsx @@ -62,7 +62,17 @@ export const StakingContractDetails = ({ ); } + if (!list || list.length === 0) { + return ( + + ); + } + return ( - + ); }; diff --git a/frontend/components/SettingsPage/DebugInfoSection.tsx b/frontend/components/SettingsPage/DebugInfoSection.tsx index 231997ffe..8010d9f62 100644 --- a/frontend/components/SettingsPage/DebugInfoSection.tsx +++ b/frontend/components/SettingsPage/DebugInfoSection.tsx @@ -9,6 +9,7 @@ import { Tooltip, Typography, } from 'antd'; +import { isAddress } from 'ethers/lib/utils'; import { isEmpty, isNil } from 'lodash'; import { Fragment, useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; @@ -195,6 +196,8 @@ export const DebugInfoSection = () => { if (!isNil(serviceAddresses)) { serviceAddresses?.forEach((serviceWallet) => { + if (!isAddress(serviceWallet.address)) return; + if (serviceWallet.type === WalletType.EOA) { result.push({ title: 'Agent Instance EOA', diff --git a/frontend/components/SettingsPage/index.tsx b/frontend/components/SettingsPage/index.tsx index e1123d10d..72e95d316 100644 --- a/frontend/components/SettingsPage/index.tsx +++ b/frontend/components/SettingsPage/index.tsx @@ -1,22 +1,19 @@ import { CloseOutlined, SettingOutlined } from '@ant-design/icons'; import { Button, Card, Flex, Skeleton, Typography } from 'antd'; import { isEmpty, isNil } from 'lodash'; -import Link from 'next/link'; import { useMemo } from 'react'; -import { MiddlewareChain } from '@/client'; -import { UNICODE_SYMBOLS } from '@/constants/symbols'; -import { EXPLORER_URL_BY_MIDDLEWARE_CHAIN } from '@/constants/urls'; import { Pages } from '@/enums/Pages'; import { SettingsScreen } from '@/enums/SettingsScreen'; import { useMultisig } from '@/hooks/useMultisig'; import { usePageState } from '@/hooks/usePageState'; +import { useServices } from '@/hooks/useServices'; import { useSettings } from '@/hooks/useSettings'; import { useMasterWalletContext } from '@/hooks/useWallet'; import { Address } from '@/types/Address'; import { Optional } from '@/types/Util'; -import { truncateAddress } from '@/utils/truncate'; +import { AddressLink } from '../AddressLink'; import { CustomAlert } from '../Alert'; import { CardTitle } from '../Card/CardTitle'; import { CardSection } from '../styled/CardSection'; @@ -86,6 +83,7 @@ export const Settings = () => { }; const SettingsMain = () => { + const { selectedService } = useServices(); const { masterEoa, masterSafes } = useMasterWalletContext(); const { owners, ownersIsFetched } = useMultisig( @@ -111,22 +109,24 @@ const SettingsMain = () => { return masterSafeBackupAddresses[0]; }, [masterSafeBackupAddresses]); - const truncatedBackupSafeAddress: Optional = useMemo(() => { - if (masterSafeBackupAddress && masterSafeBackupAddress?.length) { - return truncateAddress(masterSafeBackupAddress); - } - }, [masterSafeBackupAddress]); + const walletBackup = useMemo(() => { + if (!ownersIsFetched) return ; + if (!masterSafeBackupAddress) return ; + if (!selectedService?.home_chain) return null; + + return ( + + ); + }, [masterSafeBackupAddress, ownersIsFetched, selectedService?.home_chain]); return ( } bordered={false} - styles={{ - body: { - paddingTop: 0, - paddingBottom: 0, - }, - }} + styles={{ body: { paddingTop: 0, paddingBottom: 0 } }} extra={ + + + ); +}; + +export const SetupYourAgent = () => { + const { selectedAgentType } = useServices(); + const serviceTemplate = SERVICE_TEMPLATES.find( + (template) => template.agentType === selectedAgentType, + ); + + if (!serviceTemplate) { + return ( + Please select an agent type first!} + className="mb-8" + /> + ); + } + + return ( + + + + Set up your agent + + Provide your agent with a persona, access to an LLM and an X account. + + + + + + + You won’t be able to update your agent’s configuration after this + step. + + + + ); +}; diff --git a/frontend/components/SetupPage/SetupYourAgent/validation.ts b/frontend/components/SetupPage/SetupYourAgent/validation.ts new file mode 100644 index 000000000..542baf0ae --- /dev/null +++ b/frontend/components/SetupPage/SetupYourAgent/validation.ts @@ -0,0 +1,71 @@ +import { ServiceTemplate } from '@/client'; +import { StakingProgramId } from '@/enums/StakingProgram'; +import { ServicesService } from '@/service/Services'; + +/** + * Validate the Google Gemini API key + */ +export const validateGeminiApiKey = async (apiKey: string) => { + if (!apiKey) return false; + + try { + // sample request to fetch the models + const apiUrl = + 'https://generativelanguage.googleapis.com/v1/models?key=' + apiKey; + const response = await fetch(apiUrl); + + return response.ok; + } catch (error) { + console.error('Error validating Gemini API key:', error); + return false; + } +}; + +/** + * Validate the Twitter credentials + */ +export const validateTwitterCredentials = async ( + email: string, + username: string, + password: string, + validateTwitterLogin: ({ + username, + password, + email, + }: { + email: string; + username: string; + password: string; + }) => Promise<{ success: boolean }>, +) => { + if (!email || !username || !password) return false; + + try { + const isValidated = await validateTwitterLogin({ + username, + password, + email, + }); + if (isValidated.success) { + return true; + } + + console.error('Error validating Twitter credentials:', isValidated); + return false; + } catch (error) { + console.error('Unexpected error validating Twitter credentials:', error); + return false; + } +}; + +export const onDummyServiceCreation = async ( + stakingProgramId: StakingProgramId, + serviceTemplateConfig: ServiceTemplate, +) => { + await ServicesService.createService({ + serviceTemplate: serviceTemplateConfig, + deploy: true, + useMechMarketplace: true, + stakingProgramId, + }); +}; diff --git a/frontend/components/SetupPage/index.tsx b/frontend/components/SetupPage/index.tsx index 935ac2810..a1d3f37dc 100644 --- a/frontend/components/SetupPage/index.tsx +++ b/frontend/components/SetupPage/index.tsx @@ -3,6 +3,7 @@ import { useContext, useMemo } from 'react'; import { SetupContext } from '@/context/SetupProvider'; import { SetupScreen } from '@/enums/SetupScreen'; +import { AgentSelection } from '../AgentSelection'; import { SetupBackupSigner } from './Create/SetupBackupSigner'; import { SetupCreateSafe } from './Create/SetupCreateSafe'; import { SetupEoaFunding } from './Create/SetupEoaFunding'; @@ -15,6 +16,7 @@ import { SetupRestoreViaSeed, } from './SetupRestore'; import { SetupWelcome } from './SetupWelcome'; +import { SetupYourAgent } from './SetupYourAgent/SetupYourAgent'; const UnexpectedError = () => (
Something went wrong!
@@ -22,10 +24,12 @@ const UnexpectedError = () => ( export const Setup = () => { const { setupObject } = useContext(SetupContext); + const setupScreen = useMemo(() => { switch (setupObject.state) { case SetupScreen.Welcome: return ; + // Create account case SetupScreen.SetupPassword: return ; @@ -39,6 +43,11 @@ export const Setup = () => { return ; case SetupScreen.SetupCreateSafe: return ; + case SetupScreen.AgentSelection: + return ; + case SetupScreen.SetupYourAgent: + return ; + // Restore account case SetupScreen.Restore: return ; diff --git a/frontend/components/YourWalletPage/index.tsx b/frontend/components/YourWalletPage/index.tsx index 163fdc388..003bcc3f2 100644 --- a/frontend/components/YourWalletPage/index.tsx +++ b/frontend/components/YourWalletPage/index.tsx @@ -22,6 +22,7 @@ import { useBalanceContext, useMasterBalances, } from '@/hooks/useBalanceContext'; +import { useFeatureFlag } from '@/hooks/useFeatureFlag'; import { usePageState } from '@/hooks/usePageState'; import { useServices } from '@/hooks/useServices'; import { useMasterWalletContext } from '@/hooks/useWallet'; @@ -29,6 +30,7 @@ import { type Address } from '@/types/Address'; import { Optional } from '@/types/Util'; import { balanceFormat } from '@/utils/numberFormatters'; +import { FeatureNotEnabled } from '../FeatureNotEnabled'; import { Container, infoBreakdownParentStyle } from './styles'; import { SignerTitle } from './Titles'; import { YourAgentWallet } from './YourAgent'; @@ -41,8 +43,6 @@ const yourWalletTheme: ThemeConfig = { }, }; -const YourWalletTitle = () => ; - const Address = () => { const { masterSafes } = useMasterWalletContext(); @@ -204,15 +204,15 @@ const MasterEoaSignerNativeBalance = () => { }; export const YourWalletPage = () => { - const { goto } = usePageState(); - + const isBalanceBreakdownEnabled = useFeatureFlag('manage-wallet'); const { services } = useServices(); + const { goto } = usePageState(); return ( } + title={} extra={