diff --git a/frontend/client/types.ts b/frontend/client/types.ts index 7f09e8b9..15aae662 100644 --- a/frontend/client/types.ts +++ b/frontend/client/types.ts @@ -1,3 +1,4 @@ +import { AgentType } from '@/enums/Agent'; import { StakingProgramId } from '@/enums/StakingProgram'; import { Address } from '@/types/Address'; @@ -70,6 +71,7 @@ export type EnvVariableAttributes = { }; export type ServiceTemplate = { + agentType: AgentType; name: string; hash: string; description: string; diff --git a/frontend/components/MainPage/header/AgentButton/AgentButton.tsx b/frontend/components/MainPage/header/AgentButton/AgentButton.tsx index e0fc24a3..ba1f8180 100644 --- a/frontend/components/MainPage/header/AgentButton/AgentButton.tsx +++ b/frontend/components/MainPage/header/AgentButton/AgentButton.tsx @@ -4,7 +4,7 @@ import { useMemo } from 'react'; import { MiddlewareDeploymentStatus } from '@/client'; import { useService } from '@/hooks/useService'; import { useServices } from '@/hooks/useServices'; -import { useStakingContractDetails } from '@/hooks/useStakingContractDetails'; +import { useActiveStakingContractInfo } from '@/hooks/useStakingContractDetails'; import { useStakingProgram } from '@/hooks/useStakingProgram'; import { assertRequired } from '@/types/Util'; @@ -21,20 +21,22 @@ export const AgentButton = () => { const { selectedService } = useServices(); const { activeStakingProgramId } = useStakingProgram(); + const serviceConfigId = selectedService?.service_config_id; + const { service, deploymentStatus: serviceStatus, isLoaded, - } = useService({ serviceConfigId: selectedService?.service_config_id }); + } = useService({ serviceConfigId }); assertRequired( + // TODO: review whether this causes agent button to not render activeStakingProgramId, 'Active staking program ID is required', ); - const { isEligibleForStaking, isAgentEvicted } = useStakingContractDetails( - activeStakingProgramId, - ); + const { isEligibleForStaking, isAgentEvicted } = + useActiveStakingContractInfo(); return useMemo(() => { if (!isLoaded) { diff --git a/frontend/components/MainPage/header/AgentButton/AgentNotRunningButton.tsx b/frontend/components/MainPage/header/AgentButton/AgentNotRunningButton.tsx index 08c5ce38..fbe8aaf3 100644 --- a/frontend/components/MainPage/header/AgentButton/AgentNotRunningButton.tsx +++ b/frontend/components/MainPage/header/AgentButton/AgentNotRunningButton.tsx @@ -2,7 +2,9 @@ import { Button, ButtonProps } from 'antd'; import { useCallback, useMemo } from 'react'; import { MiddlewareChain, MiddlewareDeploymentStatus } from '@/client'; +import { MechType } from '@/config/mechs'; import { STAKING_PROGRAMS } from '@/config/stakingPrograms'; +import { SERVICE_TEMPLATES } from '@/constants/serviceTemplates'; import { LOW_MASTER_SAFE_BALANCE } from '@/constants/thresholds'; import { TokenSymbol } from '@/enums/Token'; import { @@ -12,11 +14,9 @@ import { import { useElectronApi } from '@/hooks/useElectronApi'; import { useService } from '@/hooks/useService'; import { useServices } from '@/hooks/useServices'; -import { useServiceTemplates } from '@/hooks/useServiceTemplates'; import { useActiveStakingContractInfo, useStakingContractContext, - useStakingContractDetails, } from '@/hooks/useStakingContractDetails'; import { useStakingProgram } from '@/hooks/useStakingProgram'; import { useStore } from '@/hooks/useStore'; @@ -26,7 +26,7 @@ import { WalletService } from '@/service/Wallet'; import { delayInSeconds } from '@/utils/delay'; /** Button used to start / deploy the agent */ -export const AgentNotRunningButton = (serviceConfigId: string) => { +export const AgentNotRunningButton = () => { const { masterWallets: wallets } = useMasterWalletContext(); const { selectedService, @@ -38,7 +38,6 @@ export const AgentNotRunningButton = (serviceConfigId: string) => { serviceConfigId: isLoaded && selectedService ? selectedService?.service_config_id : '', }); - const { serviceTemplate } = useServiceTemplates(); const { showNotification } = useElectronApi(); const { setIsPaused: setIsBalancePollingPaused, @@ -46,8 +45,9 @@ export const AgentNotRunningButton = (serviceConfigId: string) => { totalEthBalance, updateBalances, } = useBalanceContext(); - const { serviceSafeBalances, isLowBalance } = - useServiceBalances(serviceConfigId); + const { serviceSafeBalances, isLowBalance } = useServiceBalances( + selectedService?.service_config_id, + ); const { storeState } = useStore(); const { isAllStakingContractDetailsRecordLoaded, @@ -57,12 +57,11 @@ export const AgentNotRunningButton = (serviceConfigId: string) => { const { activeStakingProgramId } = useStakingProgram(); const { isEligibleForStaking, isAgentEvicted, isServiceStaked } = useActiveStakingContractInfo(); - const { hasEnoughServiceSlots } = useStakingContractDetails( - activeStakingProgramId, - ); + const { hasEnoughServiceSlots } = useActiveStakingContractInfo(); const requiredStakedOlas = service && + activeStakingProgramId && STAKING_PROGRAMS[service.home_chain_id][activeStakingProgramId] ?.stakingRequirements[TokenSymbol.OLAS]; @@ -102,15 +101,22 @@ export const AgentNotRunningButton = (serviceConfigId: string) => { }, [service]); const deployAndStartService = useCallback(async () => { - await ServicesService.createService({ + if (!activeStakingProgramId) return; + + const middlewareServiceResponse = await ServicesService.createService({ stakingProgramId: activeStakingProgramId, - serviceTemplate, + serviceTemplate: SERVICE_TEMPLATES[0], // TODO: support multi-agent, during optimus week deploy: true, - useMechMarketplace: false, + useMechMarketplace: + STAKING_PROGRAMS[+SERVICE_TEMPLATES[0].home_chain_id][ // TODO: support multi-agent, during optimus week + activeStakingProgramId + ].mechType === MechType.Marketplace, }); - await ServicesService.startService(serviceConfigId); - }, [activeStakingProgramId, serviceTemplate, serviceConfigId]); + await ServicesService.startService( + middlewareServiceResponse.service_config_id, + ); + }, [activeStakingProgramId]); const updateStatesSequentially = useCallback(async () => { await updateServicesState?.(); diff --git a/frontend/components/MainPage/index.tsx b/frontend/components/MainPage/index.tsx index 0b73d4cd..240009ef 100644 --- a/frontend/components/MainPage/index.tsx +++ b/frontend/components/MainPage/index.tsx @@ -9,7 +9,6 @@ import { usePageState } from '@/hooks/usePageState'; import { MainHeader } from './header'; import { AddFundsSection } from './sections/AddFundsSection'; import { AlertSections } from './sections/AlertSections'; -import { GasBalanceSection } from './sections/GasBalanceSection'; import { KeepAgentRunningSection } from './sections/KeepAgentRunningSection'; import { MainNeedsFunds } from './sections/NeedsFundsSection'; import { MainOlasBalance } from './sections/OlasBalanceSection'; @@ -87,11 +86,11 @@ export const Main = () => { - + {/* */} - + {/* */} diff --git a/frontend/components/MainPage/sections/NeedsFundsSection.tsx b/frontend/components/MainPage/sections/NeedsFundsSection.tsx index b153611c..cb5abf40 100644 --- a/frontend/components/MainPage/sections/NeedsFundsSection.tsx +++ b/frontend/components/MainPage/sections/NeedsFundsSection.tsx @@ -6,6 +6,7 @@ import { CustomAlert } from '@/components/Alert'; import { UNICODE_SYMBOLS } from '@/constants/symbols'; import { useElectronApi } from '@/hooks/useElectronApi'; import { useNeedsFunds } from '@/hooks/useNeedsFunds'; +import { useServices } from '@/hooks/useServices'; import { CardSection } from '../../styled/CardSection'; @@ -28,6 +29,9 @@ export const MainNeedsFunds = () => { needsInitialFunding, } = useNeedsFunds(); + const { selectedAgentConfig } = useServices(); + const { homeChainId } = selectedAgentConfig; + const electronApi = useElectronApi(); const message: ReactNode = useMemo( @@ -37,14 +41,14 @@ export const MainNeedsFunds = () => { {!hasEnoughOlasForInitialFunding && (
- {`${UNICODE_SYMBOLS.OLAS}${serviceFundRequirements.olas} OLAS `} + {`${UNICODE_SYMBOLS.OLAS}${serviceFundRequirements[homeChainId].olas} OLAS `} for staking
)} {!hasEnoughEthForInitialFunding && (
- {`$${serviceFundRequirements.eth} XDAI `} + {`$${serviceFundRequirements[homeChainId].eth} XDAI `} for trading
@@ -56,9 +60,10 @@ export const MainNeedsFunds = () => {
), [ + hasEnoughOlasForInitialFunding, serviceFundRequirements, + homeChainId, hasEnoughEthForInitialFunding, - hasEnoughOlasForInitialFunding, ], ); diff --git a/frontend/components/MainPage/sections/OlasBalanceSection.tsx b/frontend/components/MainPage/sections/OlasBalanceSection.tsx index 1132bd5a..5a7ca8ae 100644 --- a/frontend/components/MainPage/sections/OlasBalanceSection.tsx +++ b/frontend/components/MainPage/sections/OlasBalanceSection.tsx @@ -4,8 +4,14 @@ import { useMemo } from 'react'; import styled from 'styled-components'; import { UNICODE_SYMBOLS } from '@/constants/symbols'; +import { TokenSymbol } from '@/enums/Token'; // import { Pages } from '@/enums/PageState'; -import { useBalanceContext } from '@/hooks/useBalanceContext'; +import { + useBalanceContext, + useMasterBalances, + useServiceBalances, +} from '@/hooks/useBalanceContext'; +import { useServices } from '@/hooks/useServices'; // import { usePageState } from '@/hooks/usePageState'; import { balanceFormat } from '@/utils/numberFormatters'; @@ -22,13 +28,52 @@ type MainOlasBalanceProps = { isBorderTopVisible?: boolean }; export const MainOlasBalance = ({ isBorderTopVisible = true, }: MainOlasBalanceProps) => { - const { isLoaded: isBalanceLoaded, totalOlasBalance } = useBalanceContext(); + const { selectedService } = useServices(); + const { isLoaded: isBalanceLoaded } = useBalanceContext(); + const { masterWalletBalances } = useMasterBalances(); + const { serviceStakedBalances, serviceWalletBalances } = useServiceBalances( + selectedService?.service_config_id, + ); // const { goto } = usePageState(); - const balance = useMemo(() => { - if (totalOlasBalance === undefined) return '--'; + const displayedBalance = useMemo(() => { + // olas across master wallets, safes and eoa + const masterWalletOlasBalance = masterWalletBalances?.reduce( + (acc, { symbol, balance }) => { + if (symbol === TokenSymbol.OLAS) { + return acc + Number(balance); + } + return acc; + }, + 0, + ); + + // olas across all service wallets + const serviceWalletOlasBalance = serviceWalletBalances?.reduce( + (acc, { symbol, balance }) => { + if (symbol === TokenSymbol.OLAS) { + return acc + Number(balance); + } + return acc; + }, + 0, + ); + + // olas staked across all services + const serviceStakedOlasBalance = serviceStakedBalances?.reduce( + (acc, { olasBondBalance, olasDepositBalance }) => { + return acc + Number(olasBondBalance) + Number(olasDepositBalance); + }, + 0, + ); + + const totalOlasBalance = + masterWalletOlasBalance + + serviceWalletOlasBalance + + serviceStakedOlasBalance; + return balanceFormat(totalOlasBalance, 2); - }, [totalOlasBalance]); + }, [masterWalletBalances, serviceStakedBalances, serviceWalletBalances]); return ( Current balance {UNICODE_SYMBOLS.OLAS} - {balance} + {displayedBalance} OLAS diff --git a/frontend/components/SettingsPage/index.tsx b/frontend/components/SettingsPage/index.tsx index e12725c9..a3c97cf1 100644 --- a/frontend/components/SettingsPage/index.tsx +++ b/frontend/components/SettingsPage/index.tsx @@ -1,5 +1,6 @@ import { CloseOutlined, SettingOutlined } from '@ant-design/icons'; import { Button, Card, Flex, Typography } from 'antd'; +import { isNil } from 'lodash'; import Link from 'next/link'; import { useMemo } from 'react'; @@ -11,7 +12,9 @@ import { SettingsScreen } from '@/enums/SettingsScreen'; import { useMultisig } from '@/hooks/useMultisig'; import { usePageState } from '@/hooks/usePageState'; import { useSettings } from '@/hooks/useSettings'; -import { useWalletContext } from '@/hooks/useWallet'; +import { useMasterWalletContext } from '@/hooks/useWallet'; +import { Address } from '@/types/Address'; +import { Optional } from '@/types/Util'; import { truncateAddress } from '@/utils/truncate'; import { CustomAlert } from '../Alert'; @@ -83,15 +86,34 @@ export const Settings = () => { }; const SettingsMain = () => { - const { wallets } = useWalletContext(); - const { backupSafeAddress } = useMultisig(); + const { masterEoa, masterSafes } = useMasterWalletContext(); + + const { owners } = useMultisig( + masterSafes?.[0], // TODO: all master safes should have the same address, but dirty implementation + ); + const { goto } = usePageState(); - const truncatedBackupSafeAddress: string | undefined = useMemo(() => { - if (backupSafeAddress) { - return truncateAddress(backupSafeAddress); + const masterSafeBackupAddresses = useMemo>(() => { + if (!owners) return; + if (!masterEoa) return; + + // TODO: handle edge cases where there are multiple owners due to middleware failure, or user interaction via safe.global + return owners.filter((owner) => owner !== masterEoa.address); + }, [owners, masterEoa]); + + const masterSafeBackupAddress = useMemo>(() => { + if (isNil(masterSafeBackupAddresses)) return; + if (!masterSafeBackupAddresses?.[0]) return; + + return masterSafeBackupAddresses[0]; + }, [masterSafeBackupAddresses]); + + const truncatedBackupSafeAddress: Optional = useMemo(() => { + if (masterSafeBackupAddress && masterSafeBackupAddress?.length) { + return truncateAddress(masterSafeBackupAddress); } - }, [backupSafeAddress]); + }, [masterSafeBackupAddress]); return ( { {/* Wallet backup */} Backup wallet - {backupSafeAddress ? ( + {masterSafeBackupAddress ? ( {truncatedBackupSafeAddress} {UNICODE_SYMBOLS.EXTERNAL_LINK} diff --git a/frontend/config/stakingPrograms/gnosis.ts b/frontend/config/stakingPrograms/gnosis.ts index d53dd334..e2a8361e 100644 --- a/frontend/config/stakingPrograms/gnosis.ts +++ b/frontend/config/stakingPrograms/gnosis.ts @@ -34,6 +34,7 @@ export const GNOSIS_STAKING_PROGRAMS: StakingProgramMap = { stakingRequirements: { [TokenSymbol.OLAS]: 20, }, + mechType: MechType.Agent, mech: MECHS[ChainId.Gnosis][MechType.Agent].contract, activityChecker: ACTIVITY_CHECKERS[ChainId.Gnosis][MechType.Agent], contract: new MulticallContract( @@ -48,6 +49,7 @@ export const GNOSIS_STAKING_PROGRAMS: StakingProgramMap = { stakingRequirements: { [TokenSymbol.OLAS]: 40, }, + mechType: MechType.Agent, mech: MECHS[ChainId.Gnosis][MechType.Agent].contract, activityChecker: ACTIVITY_CHECKERS[ChainId.Gnosis][MechType.Agent], contract: new MulticallContract( @@ -62,6 +64,7 @@ export const GNOSIS_STAKING_PROGRAMS: StakingProgramMap = { stakingRequirements: { [TokenSymbol.OLAS]: 100, }, + mechType: MechType.Agent, mech: MECHS[ChainId.Gnosis][MechType.Agent].contract, activityChecker: ACTIVITY_CHECKERS[ChainId.Gnosis][MechType.Agent], contract: new MulticallContract( @@ -76,6 +79,7 @@ export const GNOSIS_STAKING_PROGRAMS: StakingProgramMap = { stakingRequirements: { [TokenSymbol.OLAS]: 100, }, + mechType: MechType.Agent, mech: MECHS[ChainId.Gnosis][MechType.Agent].contract, activityChecker: ACTIVITY_CHECKERS[ChainId.Gnosis][MechType.Agent], contract: new MulticallContract( @@ -90,6 +94,7 @@ export const GNOSIS_STAKING_PROGRAMS: StakingProgramMap = { stakingRequirements: { [TokenSymbol.OLAS]: 100, }, + mechType: MechType.Agent, mech: MECHS[ChainId.Gnosis][MechType.Agent].contract, activityChecker: ACTIVITY_CHECKERS[ChainId.Gnosis][MechType.Agent], contract: new MulticallContract( @@ -104,6 +109,7 @@ export const GNOSIS_STAKING_PROGRAMS: StakingProgramMap = { stakingRequirements: { [TokenSymbol.OLAS]: 10, }, + mechType: MechType.Agent, mech: MECHS[ChainId.Gnosis][MechType.Agent].contract, activityChecker: ACTIVITY_CHECKERS[ChainId.Gnosis][MechType.Agent], contract: new MulticallContract( @@ -118,6 +124,7 @@ export const GNOSIS_STAKING_PROGRAMS: StakingProgramMap = { stakingRequirements: { [TokenSymbol.OLAS]: 40, }, + mechType: MechType.Marketplace, mech: MECHS[ChainId.Gnosis][MechType.Marketplace].contract, activityChecker: ACTIVITY_CHECKERS[ChainId.Gnosis][MechType.Marketplace], contract: new MulticallContract( diff --git a/frontend/config/stakingPrograms/index.ts b/frontend/config/stakingPrograms/index.ts index 6a2e9adf..8c22a192 100644 --- a/frontend/config/stakingPrograms/index.ts +++ b/frontend/config/stakingPrograms/index.ts @@ -5,6 +5,7 @@ import { ChainId } from '@/enums/Chain'; import { StakingProgramId } from '@/enums/StakingProgram'; import { Address } from '@/types/Address'; +import { MechType } from '../mechs'; import { GNOSIS_STAKING_PROGRAMS, GNOSIS_STAKING_PROGRAMS_CONTRACT_ADDRESSES, @@ -26,6 +27,7 @@ export type StakingProgramConfig = { [tokenSymbol: string]: number; }; contract: MulticallContract; + mechType?: MechType; mech?: MulticallContract; activityChecker: MulticallContract; }; diff --git a/frontend/constants/serviceTemplates.ts b/frontend/constants/serviceTemplates.ts index f64f7d9c..987c6a97 100644 --- a/frontend/constants/serviceTemplates.ts +++ b/frontend/constants/serviceTemplates.ts @@ -1,9 +1,11 @@ import { EnvProvisionType, ServiceTemplate } from '@/client'; +import { AgentType } from '@/enums/Agent'; import { ChainId } from '@/enums/Chain'; import { StakingProgramId } from '@/enums/StakingProgram'; export const SERVICE_TEMPLATES: ServiceTemplate[] = [ { + agentType: AgentType.PredictTrader, // TODO: remove if causes errors on middleware name: 'Trader Agent', hash: 'bafybeidicxsruh3r4a2xarawzan6ocwyvpn3ofv42po5kxf7x6ck7kn22u', description: 'Trader agent for omen prediction markets', @@ -15,7 +17,7 @@ export const SERVICE_TEMPLATES: ServiceTemplate[] = [ [ChainId.Gnosis]: { staking_program_id: StakingProgramId.PearlBeta, // default, may be overwritten nft: 'bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq', - rpc: 'http://localhost:8545', + rpc: 'http://localhost:8545', // overwritten agent_id: 14, threshold: 1, use_staking: true, diff --git a/frontend/hooks/useBalanceContext.ts b/frontend/hooks/useBalanceContext.ts index 51392983..5a7f744f 100644 --- a/frontend/hooks/useBalanceContext.ts +++ b/frontend/hooks/useBalanceContext.ts @@ -12,20 +12,15 @@ export const useBalanceContext = () => useContext(BalanceContext); * @param serviceConfigId * @returns */ -export const useServiceBalances = (serviceConfigId: string) => { - const { flatAddresses, serviceSafes } = useService({ +export const useServiceBalances = (serviceConfigId: string | undefined) => { + const { flatAddresses, serviceSafes, serviceEoa } = useService({ serviceConfigId, }); const { walletBalances, lowBalances, stakedBalances } = useBalanceContext(); - const serviceWalletBalances = useMemo( - () => - walletBalances?.filter((balance) => - flatAddresses.includes(balance.walletAddress), - ), - [flatAddresses, walletBalances], - ); - + /** + * Staked balances, only relevant to safes + */ const serviceStakedBalances = useMemo( () => stakedBalances?.filter((balance) => @@ -34,16 +29,23 @@ export const useServiceBalances = (serviceConfigId: string) => { [flatAddresses, stakedBalances], ); + /** Array of cross-chain wallet balances relevant to the service with is considered low */ const serviceLowBalances = useMemo( () => lowBalances?.filter((balance) => balance.walletAddress), [lowBalances], ); + /** + * Boolean indicating if the service has low balances + */ const isLowBalance = useMemo( () => serviceLowBalances?.length > 0, [serviceLowBalances], ); + /** + * Cross-chain unstaked balances in service safes + */ const serviceSafeBalances = useMemo( () => walletBalances?.filter((balance) => @@ -52,10 +54,32 @@ export const useServiceBalances = (serviceConfigId: string) => { [serviceSafes, walletBalances], ); + /** + * Cross-chain unstaked balances in service eoa (signer) + */ + const serviceEoaBalances = useMemo( + () => + walletBalances?.filter( + (balance) => balance.walletAddress === serviceEoa?.address, + ), + [serviceEoa?.address, walletBalances], + ); + + /** + * Balances i.e. native, erc20, etc + * Across all service wallets, including eoa + * @note NOT STAKED BALANCES + */ + const serviceWalletBalances = useMemo( + () => [...serviceSafeBalances, ...serviceEoaBalances], + [serviceEoaBalances, serviceSafeBalances], + ); + return { serviceWalletBalances, serviceStakedBalances, serviceSafeBalances, + serviceEoaBalances, serviceLowBalances, isLowBalance, }; @@ -68,48 +92,62 @@ export const useServiceBalances = (serviceConfigId: string) => { */ export const useMasterBalances = () => { const { masterSafes, masterEoa } = useMasterWalletContext(); - const { walletBalances, lowBalances, stakedBalances } = useBalanceContext(); - - const masterWalletBalances = useMemo( + const { walletBalances, lowBalances } = useBalanceContext(); + + // TODO: unused, check only services stake? + // const masterStakedBalances = useMemo( + // () => + // stakedBalances?.filter((balance) => + // masterSafes?.find((safe) => safe.address === balance.walletAddress), + // ), + // [masterSafes, stakedBalances], + // ); + + // TODO: use flatAddresses for consistency + const masterSafeBalances = useMemo( () => - walletBalances?.filter( - (balance) => - masterSafes?.find((safe) => safe.address === balance.walletAddress) || - masterEoa?.address === balance.walletAddress, + walletBalances?.filter((balance) => + masterSafes?.find(({ address }) => balance.walletAddress === address), ), - [masterEoa?.address, masterSafes, walletBalances], + [masterSafes, walletBalances], ); - const masterStakedBalances = useMemo( + const masterEoaBalances = useMemo( () => - stakedBalances?.filter((balance) => - masterSafes?.find((safe) => safe.address === balance.walletAddress), + walletBalances?.filter( + (balance) => balance.walletAddress === masterEoa?.address, ), - [masterSafes, stakedBalances], + [masterEoa?.address, walletBalances], ); + /** + * Unstaked balances across master safes and eoas + */ + const masterWalletBalances = useMemo( + () => [...masterSafeBalances, ...masterEoaBalances], + [masterEoaBalances, masterSafeBalances], + ); + + /** + * Array of low balances relevant to the master wallets (safes and eoa) + */ const masterLowBalances = useMemo( () => lowBalances?.filter((balance) => balance.walletAddress), [lowBalances], ); + /** + * Boolean indicating if any master wallets have low balances + */ const isLowBalance = useMemo( () => masterLowBalances?.length > 0, [masterLowBalances], ); - // use flatAddresses for consistency - const masterSafeBalances = useMemo( - () => - walletBalances?.filter((balance) => - masterSafes?.find(({ address }) => balance.walletAddress === address), - ), - [masterSafes, walletBalances], - ); return { masterWalletBalances, - masterStakedBalances, masterSafeBalances, + masterEoaBalances, masterLowBalances, isLowBalance, }; diff --git a/frontend/hooks/useMultisig.ts b/frontend/hooks/useMultisig.ts index 3793eb5e..1e6df3a8 100644 --- a/frontend/hooks/useMultisig.ts +++ b/frontend/hooks/useMultisig.ts @@ -15,14 +15,17 @@ import { Address } from '@/types/Address'; * @returns multisig owners * @note extend with further multisig functions as needed */ -export const useMultisig = (safe: Safe) => { +export const useMultisig = (safe?: Safe) => { const { data: owners, isFetched: ownersIsFetched, isPending: ownersIsPending, - } = useQuery({ - queryKey: REACT_QUERY_KEYS.MULTISIG_GET_OWNERS_KEY(safe), + } = useQuery({ + queryKey: safe ? REACT_QUERY_KEYS.MULTISIG_GET_OWNERS_KEY(safe) : [], queryFn: async () => { + if (!safe) { + return null; + } const contract = new Contract( safe.address, GNOSIS_SAFE_ABI, diff --git a/frontend/hooks/useNeedsFunds.ts b/frontend/hooks/useNeedsFunds.ts index fac61fae..46ef87a2 100644 --- a/frontend/hooks/useNeedsFunds.ts +++ b/frontend/hooks/useNeedsFunds.ts @@ -2,24 +2,20 @@ import { formatEther, formatUnits } from 'ethers/lib/utils'; import { useMemo } from 'react'; import { ServiceTemplate } from '@/client'; -import { CHAIN_CONFIG } from '@/config/chains'; import { STAKING_PROGRAMS } from '@/config/stakingPrograms'; +import { getNativeTokenSymbol } from '@/config/tokens'; import { getServiceTemplate } from '@/constants/serviceTemplates'; +import { TokenSymbol } from '@/enums/Token'; -import { useBalanceContext } from './useBalanceContext'; +import { useBalanceContext, useMasterBalances } from './useBalanceContext'; import { useService } from './useService'; import { useStore } from './useStore'; -import { TokenSymbol } from '@/enums/Token'; -import { getNativeTokenSymbol, NATIVE_TOKEN_CONFIG } from '@/config/tokens'; -import { useMasterWalletContext } from './useWallet'; -export const useNeedsFunds = (serviceConfigId: string) => { +export const useNeedsFunds = (serviceConfigId?: string) => { const { storeState } = useStore(); const { service } = useService({ serviceConfigId }); - const { masterSafes } = useMasterWalletContext(); - const { isLoaded: isBalanceLoaded, walletBalances } = - useBalanceContext(); - + const { isLoaded: isBalanceLoaded, walletBalances } = useBalanceContext(); + const { masterSafeBalances } = useMasterBalances(); const isInitialFunded = storeState?.isInitialFunded; @@ -28,35 +24,38 @@ export const useNeedsFunds = (serviceConfigId: string) => { [service], ); - const serviceFundRequirements = useMemo< { + const serviceFundRequirements = useMemo<{ [chainId: number]: { - [tokenSymbol: string]: number; - } + [tokenSymbol: string]: number; + }; }>(() => { if (!serviceTemplate) return {}; const results: { [chainId: number]: { - [tokenSymbol: string]: number; - } + [tokenSymbol: string]: number; + }; } = {}; Object.entries(serviceTemplate.configurations).forEach( ([chainId, config]) => { - const serviceTemplateDefault = serviceTemplate.configurations[+chainId].staking_program_id - const serviceCurrent = service?.chain_configs[+chainId]?.chain_data?.user_params?.staking_program_id - - if (!serviceCurrent && !serviceTemplateDefault) return; - + const templateStakingProgramId = + serviceTemplate.configurations[+chainId].staking_program_id; + const serviceStakingProgramId = + service?.chain_configs[+chainId]?.chain_data?.user_params + ?.staking_program_id; + const stakingProgramId = + serviceStakingProgramId ?? templateStakingProgramId; + + if (!stakingProgramId) return; if (!service?.chain_configs[+chainId]) return; + const gasEstimate = config.monthly_gas_estimate; const monthlyGasEstimate = Number(formatUnits(`${gasEstimate}`, 18)); const minimumStakedAmountRequired = - STAKING_PROGRAMS[+chainId][ - service?.chain_configs[+chainId]?.chain_data?.user_params - ?.staking_program_id ?? - serviceTemplate.configurations[+chainId].staking_program_id - ].stakingRequirements.OLAS; + STAKING_PROGRAMS[+chainId]?.[stakingProgramId]?.stakingRequirements?.[ + TokenSymbol.OLAS + ] || 0; const nativeTokenSymbol = getNativeTokenSymbol(+chainId); @@ -69,48 +68,60 @@ export const useNeedsFunds = (serviceConfigId: string) => { ); return results; - }, [serviceTemplate]); - - const hasEnoughEthForInitialFunding = useMemo( - () => { - if (!serviceFundRequirements) return ; - if (!walletBalances) return ; - - const nativeBalancesByChain = walletBalances.reduce<{[chainId: number]: number}>((acc, {symbol, balance, chainId}) => { - if (getNativeTokenSymbol(chainId) !== symbol) return acc; - - if (!acc[chainId]) acc[chainId] = 0; - acc[chainId] += balance; - - return acc; - }, {}); - - const chainIds = Object.keys(serviceFundRequirements).map(Number); - - return chainIds.every(chainId => { - const nativeTokenSymbol = getNativeTokenSymbol(chainId); - const nativeTokenBalance = nativeBalancesByChain[chainId] || 0; - const nativeTokenRequired = serviceFundRequirements[chainId]?.[nativeTokenSymbol] || 0; - - return nativeTokenBalance >= nativeTokenRequired; - }); - - }, - [], - ); + }, [service?.chain_configs, serviceTemplate]); + + const hasEnoughEthForInitialFunding = useMemo(() => { + if (!serviceFundRequirements) return; + if (!walletBalances) return; + + const nativeBalancesByChain = walletBalances.reduce<{ + [chainId: number]: number; + }>((acc, { symbol, balance, chainId }) => { + if (getNativeTokenSymbol(chainId) !== symbol) return acc; + + if (!acc[chainId]) acc[chainId] = 0; + acc[chainId] += balance; + + return acc; + }, {}); + + const chainIds = Object.keys(serviceFundRequirements).map(Number); + + return chainIds.every((chainId) => { + const nativeTokenSymbol = getNativeTokenSymbol(chainId); + const nativeTokenBalance = nativeBalancesByChain[chainId] || 0; + const nativeTokenRequired = + serviceFundRequirements[chainId]?.[nativeTokenSymbol] || 0; + + return nativeTokenBalance >= nativeTokenRequired; + }); + }, [serviceFundRequirements, walletBalances]); - // TODO: refactor this to use the new balance context const hasEnoughOlasForInitialFunding = useMemo(() => { - const olasInSafe = safeBalance?.OLAS || 0; - const olasStakedBySafe = totalStakedOlasBalance || 0; - const olasRequiredToFundService = serviceFundRequirements.olas || 0; - const olasInSafeAndStaked = olasInSafe + olasStakedBySafe; - return olasInSafeAndStaked >= olasRequiredToFundService; - }, [ - safeBalance?.OLAS, - totalStakedOlasBalance, - serviceFundRequirements?.olas, - ]); + if (!serviceFundRequirements) return; + if (!masterSafeBalances) return; + + const olasBalancesByChain = masterSafeBalances.reduce<{ + [chainId: number]: number; + }>((acc, { symbol, balance, chainId }) => { + if (TokenSymbol.OLAS !== symbol) return acc; + + if (!acc[chainId]) acc[chainId] = 0; + acc[chainId] += balance; + + return acc; + }, {}); + + const chainIds = Object.keys(serviceFundRequirements).map(Number); + + return chainIds.every((chainId) => { + const olasBalance = olasBalancesByChain[chainId] || 0; + const olasRequired = + serviceFundRequirements[chainId]?.[TokenSymbol.OLAS] || 0; + + return olasBalance >= olasRequired; + }); + }, [masterSafeBalances, serviceFundRequirements]); const needsInitialFunding: boolean = useMemo(() => { if (isInitialFunded) return false; diff --git a/frontend/hooks/useStakingContractDetails.ts b/frontend/hooks/useStakingContractDetails.ts index b5b01915..76bff9ba 100644 --- a/frontend/hooks/useStakingContractDetails.ts +++ b/frontend/hooks/useStakingContractDetails.ts @@ -3,6 +3,7 @@ import { useContext } from 'react'; import { StakingContractDetailsContext } from '@/context/StakingContractDetailsProvider'; import { StakingProgramId } from '@/enums/StakingProgram'; +import { StakingState } from '@/types/Autonolas'; import { useServices } from './useServices'; @@ -27,10 +28,20 @@ export const useStakingContractContext = () => { }; }; +/** + * Returns ACTIVE staking contract details + * Has staked service speficic information that `useStakingContractDetails` does not have + + * @note requires serviceConfigId once multiple instances are supported + */ export const useActiveStakingContractInfo = () => { const { activeStakingContractDetails, isActiveStakingContractDetailsLoaded: isActiveStakingContractDetailsLoaded, + allStakingContractDetailsRecord, + refetchActiveStakingContractDetails, + isPaused, + setIsPaused, } = useStakingContractContext(); const { selectedService } = useServices(); @@ -40,8 +51,8 @@ export const useActiveStakingContractInfo = () => { return { allStakingContractDetailsRecord, refetchActiveStakingContractDetails, - setIsPaused, isPaused, + setIsPaused, }; } @@ -54,10 +65,10 @@ export const useActiveStakingContractInfo = () => { maxNumServices, } = activeStakingContractDetails ?? {}; - const isAgentEvicted = serviceStakingState === 2; + const isAgentEvicted = serviceStakingState === StakingState.Evicted; const isServiceStaked = - !!serviceStakingStartTime && serviceStakingState === 1; + !!serviceStakingStartTime && serviceStakingState === StakingState.Staked; const isRewardsAvailable = availableRewards ?? 0 > 0; @@ -102,6 +113,9 @@ export const useActiveStakingContractInfo = () => { evictionExpiresAt, isActiveStakingContractDetailsLoaded, activeStakingContractDetails, + hasEnoughRewardsAndSlots, + hasEnoughServiceSlots, + isRewardsAvailable, }; }; diff --git a/frontend/types/Autonolas.ts b/frontend/types/Autonolas.ts index fe1695d9..1194ece9 100644 --- a/frontend/types/Autonolas.ts +++ b/frontend/types/Autonolas.ts @@ -14,6 +14,12 @@ export const StakingRewardsInfoSchema = z.object({ export type StakingRewardsInfo = z.infer; +export enum StakingState { + NotStaked = 0, + Staked = 1, + Evicted = 2, +} + export type StakingContractDetails = { availableRewards: number; /* number of slots available for staking */ @@ -23,8 +29,8 @@ export type StakingContractDetails = { minimumStakingDuration: number; /** time when service was staked (in seconds) - 0 = never staked */ serviceStakingStartTime: number; - /** 0: not staked, 1: staked, 2: unstaked - current state of the service */ - serviceStakingState: number; + /** 0: not staked, 1: staked, 2: evicted */ + serviceStakingState: StakingState; /** OLAS cost of staking */ minStakingDeposit: number; /** estimated annual percentage yield */