diff --git a/frontend/components/AddressLink.tsx b/frontend/components/AddressLink.tsx index 3f005a59..9f8c56c6 100644 --- a/frontend/components/AddressLink.tsx +++ b/frontend/components/AddressLink.tsx @@ -7,16 +7,13 @@ import { truncateAddress } from '@/utils/truncate'; type AddressLinkProps = { address?: Address; hideLinkArrow?: boolean; - - // TODO: mark as required once balance breakdown is updated. - // and remove the default value - middlewareChain?: MiddlewareChain; + middlewareChain: MiddlewareChain; }; export const AddressLink = ({ address, hideLinkArrow = false, - middlewareChain = MiddlewareChain.GNOSIS, + middlewareChain, }: AddressLinkProps) => { if (!address) return null; if (!middlewareChain) return null; diff --git a/frontend/components/SettingsPage/index.tsx b/frontend/components/SettingsPage/index.tsx index 06061043..52847048 100644 --- a/frontend/components/SettingsPage/index.tsx +++ b/frontend/components/SettingsPage/index.tsx @@ -18,7 +18,6 @@ import { CustomAlert } from '../Alert'; import { CardTitle } from '../Card/CardTitle'; import { CardSection } from '../styled/CardSection'; import { AddBackupWalletPage } from './AddBackupWalletPage'; -import { DebugInfoSection } from './DebugInfoSection'; const { Text, Paragraph } = Typography; @@ -161,9 +160,6 @@ const SettingsMain = () => { Backup wallet {walletBackup} - - {/* Debug info */} - ); }; diff --git a/frontend/components/YourWalletPage/Titles.tsx b/frontend/components/YourWalletPage/Titles.tsx index c5f46af9..7a6cb8e9 100644 --- a/frontend/components/YourWalletPage/Titles.tsx +++ b/frontend/components/YourWalletPage/Titles.tsx @@ -1,5 +1,6 @@ import { Flex, Typography } from 'antd'; +import { MiddlewareChain } from '@/client'; import { InfoTooltip } from '@/components/InfoTooltip'; import { TokenSymbol } from '@/enums/Token'; import { Address } from '@/types/Address'; @@ -8,11 +9,16 @@ import { AddressLink } from '../AddressLink'; const { Paragraph, Text, Title } = Typography; -type SignerTitleProps = { signerText: string; signerAddress?: Address }; +type SignerTitleProps = { + signerText: string; + signerAddress: Address; + middlewareChain: MiddlewareChain; +}; export const SignerTitle = ({ signerText, signerAddress, + middlewareChain, }: SignerTitleProps) => ( <> Signer  @@ -28,7 +34,10 @@ export const SignerTitle = ({ {signerText} - + @@ -58,7 +67,7 @@ export const NativeTokenTitle = ({ symbol }: { symbol: TokenSymbol }) => (   - {/* TODO: address multi-agent tooltip, specfic to agent config */} + {/* TODO: address multi-agent tooltip, specific to agent config */} {symbol} is used by the agent to engage in prediction markets. This amount will fluctuate based on your agent's performance. diff --git a/frontend/components/YourWalletPage/WithdrawFunds.tsx b/frontend/components/YourWalletPage/WithdrawFunds.tsx index b268b880..28f1ced3 100644 --- a/frontend/components/YourWalletPage/WithdrawFunds.tsx +++ b/frontend/components/YourWalletPage/WithdrawFunds.tsx @@ -5,6 +5,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { COLOR } from '@/constants/colors'; import { useBalanceContext } from '@/hooks/useBalanceContext'; +import { useFeatureFlag } from '@/hooks/useFeatureFlag'; import { useService } from '@/hooks/useService'; import { useServices } from '@/hooks/useServices'; import { useStakingContractCountdown } from '@/hooks/useStakingContractCountdown'; @@ -14,6 +15,7 @@ import { ServicesService } from '@/service/Services'; import { Address } from '@/types/Address'; import { CustomAlert } from '../Alert'; +import { FeatureNotEnabled } from '../FeatureNotEnabled'; const { Text } = Typography; @@ -54,7 +56,6 @@ const CompatibleMessage = () => ( export const WithdrawFunds = () => { const { selectedService, refetch: refetchServices } = useServices(); const { refetch: refetchMasterWallets } = useMasterWalletContext(); - const { updateBalances } = useBalanceContext(); const { service, isServiceRunning } = useService( @@ -64,6 +65,7 @@ export const WithdrawFunds = () => { const { isServiceStakedForMinimumDuration, selectedStakingContractDetails } = useActiveStakingContractDetails(); + // state const [isModalVisible, setIsModalVisible] = useState(false); const [withdrawAddress, setWithdrawAddress] = useState(''); const [isWithdrawalLoading, setIsWithdrawalLoading] = useState(false); @@ -148,6 +150,9 @@ export const WithdrawFunds = () => { [service, isServiceStakedForMinimumDuration, showModal], ); + const isWithdrawFundsEnabled = useFeatureFlag('withdraw-funds'); + if (!isWithdrawFundsEnabled) return ; + return ( <> {isServiceStakedForMinimumDuration ? ( diff --git a/frontend/components/YourWalletPage/YourAgent.tsx b/frontend/components/YourWalletPage/YourAgent.tsx index 271cfa80..c92027d2 100644 --- a/frontend/components/YourWalletPage/YourAgent.tsx +++ b/frontend/components/YourWalletPage/YourAgent.tsx @@ -4,16 +4,17 @@ import Image from 'next/image'; import { useMemo } from 'react'; import styled from 'styled-components'; +import { MiddlewareChain } from '@/client'; import { OLAS_CONTRACTS } from '@/config/olasContracts'; -import { UNICODE_SYMBOLS } from '@/constants/symbols'; -import { EvmChainId } from '@/enums/Chain'; +import { NA, UNICODE_SYMBOLS } from '@/constants/symbols'; +import { BLOCKSCOUT_URL_BY_MIDDLEWARE_CHAIN } from '@/constants/urls'; import { ContractType } from '@/enums/Contract'; import { TokenSymbol } from '@/enums/Token'; -import { AgentSafe, Safe } from '@/enums/Wallet'; import { useBalanceContext, useServiceBalances, } from '@/hooks/useBalanceContext'; +import { useFeatureFlag } from '@/hooks/useFeatureFlag'; import { useReward } from '@/hooks/useReward'; import { useService } from '@/hooks/useService'; import { useServices } from '@/hooks/useServices'; @@ -26,6 +27,7 @@ import { AddressLink } from '../AddressLink'; import { InfoBreakdownList } from '../InfoBreakdown'; import { Container, infoBreakdownParentStyle } from './styles'; import { OlasTitle, OwnershipNftTitle, ServiceNftIdTitle } from './Titles'; +import { useYourWallet } from './useYourWallet'; import { WithdrawFunds } from './WithdrawFunds'; const { Text, Paragraph } = Typography; @@ -39,8 +41,8 @@ const NftCard = styled(Card)` } `; -const SafeAddress = ({ serviceSafe }: { serviceSafe: Safe }) => { - const multisigAddress = serviceSafe.address; +const SafeAddress = ({ address }: { address: Address }) => { + const { middlewareChain } = useYourWallet(); return ( @@ -49,7 +51,12 @@ const SafeAddress = ({ serviceSafe }: { serviceSafe: Safe }) => { { left: 'Wallet Address', leftClassName: 'text-light text-sm', - right: , + right: ( + + ), rightClassName: 'font-normal text-sm', }, ]} @@ -59,11 +66,16 @@ const SafeAddress = ({ serviceSafe }: { serviceSafe: Safe }) => { ); }; -const AgentTitle = ({ serviceSafe }: { serviceSafe: AgentSafe }) => { - const agentName = useMemo( - () => (serviceSafe ? generateName(serviceSafe.address) : '--'), - [serviceSafe], - ); +const AgentTitle = ({ address }: { address: Address }) => { + const { middlewareChain } = useYourWallet(); + + const agentProfileLink = useMemo(() => { + if (!address) return null; + if (middlewareChain === MiddlewareChain.GNOSIS) { + return `https://predict.olas.network/agents/${address}`; + } + return null; + }, [address, middlewareChain]); return ( @@ -89,16 +101,14 @@ const AgentTitle = ({ serviceSafe }: { serviceSafe: AgentSafe }) => { } placement="top" > - {agentName} + {address ? generateName(address) : NA} - {/* TODO: address multi-agent at later point */} - - Agent profile {UNICODE_SYMBOLS.EXTERNAL_LINK} - + + {agentProfileLink && ( + + Agent profile {UNICODE_SYMBOLS.EXTERNAL_LINK} + + )} @@ -106,13 +116,14 @@ const AgentTitle = ({ serviceSafe }: { serviceSafe: AgentSafe }) => { ); }; +type ServiceAndNftDetailsProps = { serviceNftTokenId: number }; const ServiceAndNftDetails = ({ serviceNftTokenId, -}: { - serviceNftTokenId: number; -}) => { +}: ServiceAndNftDetailsProps) => { + const { middlewareChain, evmHomeChainId } = useYourWallet(); + const serviceRegistryL2ContractAddress = - OLAS_CONTRACTS[EvmChainId.Gnosis][ContractType.ServiceRegistryL2].address; + OLAS_CONTRACTS[evmHomeChainId][ContractType.ServiceRegistryL2].address; return ( @@ -129,7 +140,7 @@ const ServiceAndNftDetails = ({ {truncateAddress(serviceRegistryL2ContractAddress as Address)}{' '} @@ -140,7 +151,7 @@ const ServiceAndNftDetails = ({ {serviceNftTokenId} {UNICODE_SYMBOLS.EXTERNAL_LINK} @@ -155,12 +166,13 @@ const ServiceAndNftDetails = ({ const YourAgentWalletBreakdown = () => { const { isLoaded } = useBalanceContext(); const { selectedService } = useServices(); - const { serviceSafes, serviceNftTokenId, serviceEoa } = useService( + const { serviceNftTokenId, serviceEoa } = useService( selectedService?.service_config_id, ); const { serviceSafeBalances, serviceEoaBalances } = useServiceBalances( selectedService?.service_config_id, ); + const { serviceSafe, middlewareChain, evmHomeChainId } = useYourWallet(); const { availableRewardsForEpochEth, @@ -170,23 +182,27 @@ const YourAgentWalletBreakdown = () => { const reward = useMemo(() => { if (!isLoaded) return ; - if (!isEligibleForRewards) return 'Not yet earned'; - return `~${balanceFormat(availableRewardsForEpochEth, 2)} OLAS`; + if (isEligibleForRewards) { + return `~${balanceFormat(availableRewardsForEpochEth, 2)} OLAS`; + } + + return 'Not yet earned'; }, [isLoaded, isEligibleForRewards, availableRewardsForEpochEth]); - const serviceSafeOlasBalances = useMemo( + const serviceSafeOlas = useMemo( () => - serviceSafeBalances?.filter( - (walletBalance) => walletBalance.symbol === TokenSymbol.OLAS, + serviceSafeBalances?.find( + ({ symbol, evmChainId }) => + symbol === TokenSymbol.OLAS && evmChainId === evmHomeChainId, ), - [serviceSafeBalances], + [serviceSafeBalances, evmHomeChainId], ); const serviceSafeRewards = useMemo( () => [ { title: 'Claimed rewards', - value: `${balanceFormat(serviceSafeOlasBalances?.[0]?.balance ?? 0, 2)} OLAS`, + value: `${balanceFormat(serviceSafeOlas?.balance ?? 0, 2)} OLAS`, }, { title: 'Unclaimed rewards', @@ -197,30 +213,31 @@ const YourAgentWalletBreakdown = () => { value: reward, }, ], - [accruedServiceStakingRewards, reward, serviceSafeOlasBalances], + [accruedServiceStakingRewards, reward, serviceSafeOlas], ); const serviceSafeNativeBalances = useMemo( - () => serviceSafeBalances?.filter((balance) => balance.isNative), - [serviceSafeBalances], + () => + serviceSafeBalances?.filter( + ({ isNative, evmChainId }) => isNative && evmChainId === evmHomeChainId, + ), + [serviceSafeBalances, evmHomeChainId], ); const serviceEoaNativeBalances = useMemo( - () => serviceEoaBalances?.filter((balance) => balance.isNative), - [serviceEoaBalances], + () => + serviceEoaBalances?.filter( + ({ isNative, evmChainId }) => isNative && evmChainId === evmHomeChainId, + ), + [serviceEoaBalances, evmHomeChainId], ); - const serviceSafe = useMemo(() => { - if (isNil(serviceSafes) || isEmpty(serviceSafes)) return null; - return serviceSafes[0]; - }, [serviceSafes]); - - if (isNil(serviceSafe)) return null; + if (!serviceSafe) return null; return ( - }> + }> - + {!isEmpty(serviceSafeRewards) && ( @@ -255,7 +272,12 @@ const YourAgentWalletBreakdown = () => { { left: 'Signer', leftClassName: 'text-sm', - right: , + right: ( + + ), rightClassName: 'font-normal text-sm', }, ]} @@ -273,9 +295,13 @@ const YourAgentWalletBreakdown = () => { ); }; -export const YourAgentWallet = () => ( - <> - - - -); +export const YourAgentWallet = () => { + const isWithdrawFundsEnabled = useFeatureFlag('withdraw-funds'); + + return ( + <> + + {isWithdrawFundsEnabled && } + + ); +}; diff --git a/frontend/components/YourWalletPage/index.tsx b/frontend/components/YourWalletPage/index.tsx index 3a221f40..137e5c26 100644 --- a/frontend/components/YourWalletPage/index.tsx +++ b/frontend/components/YourWalletPage/index.tsx @@ -7,7 +7,7 @@ import { ThemeConfig, Typography, } from 'antd'; -import { isEmpty, isNil } from 'lodash'; +import { capitalize, isNil } from 'lodash'; import { useMemo } from 'react'; import { AddressLink } from '@/components/AddressLink'; @@ -15,7 +15,6 @@ import { CardTitle } from '@/components/Card/CardTitle'; import { InfoBreakdownList } from '@/components/InfoBreakdown'; import { CardFlex } from '@/components/styled/CardFlex'; import { getNativeTokenSymbol } from '@/config/tokens'; -import { EvmChainId } from '@/enums/Chain'; import { Pages } from '@/enums/Pages'; import { TokenSymbol } from '@/enums/Token'; import { @@ -33,6 +32,7 @@ import { balanceFormat } from '@/utils/numberFormatters'; import { FeatureNotEnabled } from '../FeatureNotEnabled'; import { Container, infoBreakdownParentStyle } from './styles'; import { SignerTitle } from './Titles'; +import { useYourWallet } from './useYourWallet'; import { YourAgentWallet } from './YourAgent'; const { Text } = Typography; @@ -44,12 +44,10 @@ const yourWalletTheme: ThemeConfig = { }; const Address = () => { - const { masterSafes } = useMasterWalletContext(); + const { isMasterSafeLoading, masterSafeAddress, middlewareChain } = + useYourWallet(); - if (!masterSafes) return ; - if (isEmpty(masterSafes)) return null; - - const masterSafeAddress = masterSafes[0].address; // TODO: handle multiple safes in future + if (isMasterSafeLoading) return ; return ( @@ -58,7 +56,12 @@ const Address = () => { { left: 'Address', leftClassName: 'text-light', - right: , + right: ( + + ), rightClassName: 'font-normal', }, ]} @@ -71,9 +74,13 @@ const Address = () => { const OlasBalance = () => { const { totalStakedOlasBalance } = useBalanceContext(); const { masterWalletBalances } = useMasterBalances(); + const { middlewareChain, evmHomeChainId } = useYourWallet(); const masterSafeOlasBalance = masterWalletBalances - ?.filter((walletBalance) => walletBalance.symbol === TokenSymbol.OLAS) + ?.filter( + ({ symbol, evmChainId }) => + symbol === TokenSymbol.OLAS && evmChainId === evmHomeChainId, + ) .reduce((acc, balance) => acc + balance.balance, 0); const olasBalances = useMemo(() => { @@ -93,7 +100,9 @@ const OlasBalance = () => { return ( - {TokenSymbol.OLAS} + + {TokenSymbol.OLAS} ({capitalize(middlewareChain)}) + ({ left: item.title, @@ -107,49 +116,39 @@ const OlasBalance = () => { }; const MasterSafeNativeBalance = () => { - const { selectedAgentConfig } = useServices(); - const { masterSafes } = useMasterWalletContext(); + const { evmHomeChainId, masterSafeAddress, middlewareChain } = + useYourWallet(); const { masterSafeBalances } = useMasterBalances(); - const selectedMasterSafe = useMemo(() => { - if (!masterSafes) return; - if (!selectedAgentConfig) return; + const nativeTokenSymbol = getNativeTokenSymbol(evmHomeChainId); - return masterSafes.find( - (masterSafe) => - masterSafe.evmChainId === selectedAgentConfig.evmHomeChainId, - ); - }, [masterSafes, selectedAgentConfig]); - - const selectedMasterSafeNativeBalance: Optional = useMemo(() => { - if (isNil(selectedMasterSafe)) return; + const masterSafeNativeBalance: Optional = useMemo(() => { + if (isNil(masterSafeAddress)) return; if (isNil(masterSafeBalances)) return; return masterSafeBalances .filter(({ walletAddress, evmChainId, isNative }) => { return ( - evmChainId === selectedAgentConfig?.evmHomeChainId && // TODO: address multi chain, need to refactor as per product requirement + evmChainId === evmHomeChainId && // TODO: address multi chain, need to refactor as per product requirement isNative && - walletAddress === selectedMasterSafe.address + walletAddress === masterSafeAddress ); }) .reduce((acc, { balance }) => acc + balance, 0); - }, [ - masterSafeBalances, - selectedAgentConfig?.evmHomeChainId, - selectedMasterSafe, - ]); - - const nativeTokenSymbol = getNativeTokenSymbol(EvmChainId.Gnosis); + }, [masterSafeBalances, masterSafeAddress, evmHomeChainId]); return ( {getNativeTokenSymbol(EvmChainId.Gnosis)}, + left: ( + + {nativeTokenSymbol} ({capitalize(middlewareChain)}) + + ), leftClassName: 'text-light', - right: `${balanceFormat(selectedMasterSafeNativeBalance, 2)} ${nativeTokenSymbol}`, + right: `${balanceFormat(masterSafeNativeBalance, 2)} ${nativeTokenSymbol}`, }, ]} parentStyle={infoBreakdownParentStyle} @@ -161,7 +160,9 @@ const MasterSafeNativeBalance = () => { const MasterEoaSignerNativeBalance = () => { const { masterEoa } = useMasterWalletContext(); const { masterWalletBalances } = useMasterBalances(); - const { selectedAgentConfig } = useServices(); + const { evmHomeChainId, middlewareChain } = useYourWallet(); + + const nativeTokenSymbol = getNativeTokenSymbol(evmHomeChainId); const masterEoaBalance: Optional = useMemo(() => { if (isNil(masterEoa)) return; @@ -172,29 +173,25 @@ const MasterEoaSignerNativeBalance = () => { ({ walletAddress, isNative, evmChainId }) => walletAddress === masterEoa.address && isNative && - selectedAgentConfig?.evmHomeChainId === evmChainId, // TODO: address multi chain, need to refactor as per product requirement + evmHomeChainId === evmChainId, // TODO: address multi chain, need to refactor as per product requirement ) .reduce((acc, { balance }) => acc + balance, 0); - }, [masterEoa, masterWalletBalances, selectedAgentConfig?.evmHomeChainId]); - - const nativeTokenSymbol = useMemo( - () => getNativeTokenSymbol(EvmChainId.Gnosis), // TODO: support multi chain - [], - ); + }, [masterEoa, masterWalletBalances, evmHomeChainId]); return ( ), leftClassName: 'text-light', - right: `${balanceFormat(masterEoaBalance, 2)} ${nativeTokenSymbol}`, + right: `${balanceFormat(masterEoaBalance, 3)} ${nativeTokenSymbol}`, }, ]} parentStyle={infoBreakdownParentStyle} diff --git a/frontend/components/YourWalletPage/useYourWallet.ts b/frontend/components/YourWalletPage/useYourWallet.ts new file mode 100644 index 00000000..51469e05 --- /dev/null +++ b/frontend/components/YourWalletPage/useYourWallet.ts @@ -0,0 +1,34 @@ +import { useMemo } from 'react'; + +import { useService } from '@/hooks/useService'; +import { useServices } from '@/hooks/useServices'; +import { useMasterWalletContext } from '@/hooks/useWallet'; + +export const useYourWallet = () => { + const { selectedAgentConfig, selectedService } = useServices(); + const { serviceSafes } = useService(selectedService?.service_config_id); + const { isLoading: isMasterSafeLoading, masterSafes } = + useMasterWalletContext(); + + const evmHomeChainId = selectedAgentConfig?.evmHomeChainId; + + // master safe + const masterSafe = useMemo(() => { + return masterSafes?.find(({ evmChainId }) => evmChainId === evmHomeChainId); + }, [masterSafes, evmHomeChainId]); + + // agent safe + const serviceSafe = useMemo(() => { + return serviceSafes?.find( + ({ evmChainId }) => evmChainId === evmHomeChainId, + ); + }, [serviceSafes, evmHomeChainId]); + + return { + middlewareChain: selectedAgentConfig?.middlewareHomeChainId, + evmHomeChainId, + isMasterSafeLoading, + masterSafeAddress: masterSafe?.address, + serviceSafe, + }; +}; diff --git a/frontend/constants/urls.ts b/frontend/constants/urls.ts index 581b4e96..2847cd21 100644 --- a/frontend/constants/urls.ts +++ b/frontend/constants/urls.ts @@ -52,6 +52,14 @@ export const EXPLORER_URL_BY_MIDDLEWARE_CHAIN: Record< [MiddlewareChain.BASE]: BASE_EXPLORER_URL, }; +export const BLOCKSCOUT_URL_BY_MIDDLEWARE_CHAIN: Record< + string | MiddlewareChain, + Url +> = { + [MiddlewareChain.GNOSIS]: 'https://gnosis.blockscout.com', + [MiddlewareChain.BASE]: 'https://base.blockscout.com', +}; + export const SWAP_URL_BY_EVM_CHAIN: Record = { [EvmChainId.Gnosis]: COW_SWAP_GNOSIS_XDAI_OLAS_URL, // [EvmChainId.OPTIMISM]: COW_SWAP_GNOSIS_XDAI_OLAS_URL, diff --git a/frontend/hooks/useFeatureFlag.ts b/frontend/hooks/useFeatureFlag.ts index bfd9ec18..cba25363 100644 --- a/frontend/hooks/useFeatureFlag.ts +++ b/frontend/hooks/useFeatureFlag.ts @@ -6,8 +6,9 @@ import { assertRequired } from '@/types/Util'; import { useServices } from './useServices'; const FeatureFlagsSchema = z.enum([ - 'last-transactions', 'manage-wallet', + 'withdraw-funds', + 'last-transactions', 'rewards-streak', 'staking-contract-section', 'low-funds', @@ -27,13 +28,15 @@ const FeaturesConfigSchema = z.record( const FEATURES_CONFIG = FeaturesConfigSchema.parse({ [AgentType.PredictTrader]: { 'manage-wallet': true, + 'withdraw-funds': true, 'last-transactions': true, 'rewards-streak': true, 'staking-contract-section': true, 'low-funds': true, }, [AgentType.Memeooorr]: { - 'manage-wallet': false, + 'manage-wallet': true, + 'withdraw-funds': false, 'last-transactions': false, 'rewards-streak': false, 'staking-contract-section': false, diff --git a/frontend/utils/numberFormatters.ts b/frontend/utils/numberFormatters.ts index 06a2ba04..c7eb43f8 100644 --- a/frontend/utils/numberFormatters.ts +++ b/frontend/utils/numberFormatters.ts @@ -5,7 +5,7 @@ import { BigNumberish, ethers } from 'ethers'; */ export const balanceFormat = ( balance: number | undefined, - decimals: 2, + decimals = 2, ): string => { if (balance === undefined) return '--'; return Intl.NumberFormat('en-US', {