diff --git a/netlify/shared/moralisBalances.mts b/netlify/shared/moralisBalances.mts index 43708b60f8..74b4131bd1 100644 --- a/netlify/shared/moralisBalances.mts +++ b/netlify/shared/moralisBalances.mts @@ -3,7 +3,7 @@ import type { Store } from '@netlify/blobs'; import Moralis from 'moralis'; import { isAddress } from 'viem'; import type { Address } from 'viem'; -import { moralisSupportedChainIds } from '../../src/providers/NetworkConfig/NetworkConfigProvider'; +import { moralisSupportedChainIds } from '../../src/providers/NetworkConfig/useNetworkConfigStore'; export interface BalanceDataWithMetadata { data: T[]; diff --git a/src/components/DAOTreasury/components/Transactions.tsx b/src/components/DAOTreasury/components/Transactions.tsx index 5f57a8c197..8843309779 100644 --- a/src/components/DAOTreasury/components/Transactions.tsx +++ b/src/components/DAOTreasury/components/Transactions.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { getAddress } from 'viem'; import { useDateTimeDisplay } from '../../../helpers/dateTime'; import { useFractal } from '../../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { TokenEventType, TransferDisplayData, TransferType } from '../../../types'; import { DecentTooltip } from '../../ui/DecentTooltip'; @@ -14,7 +14,7 @@ import { BarLoader } from '../../ui/loaders/BarLoader'; function TransferRow({ displayData }: { displayData: TransferDisplayData }) { const { t } = useTranslation(['treasury', 'common']); - const { etherscanBaseURL } = useNetworkConfig(); + const { etherscanBaseURL } = useNetworkConfigStore(); return ( !asset.tokenAddress); const { handleUnstake, handleClaimUnstakedETH } = useLidoStaking(); const { canUserCreateProposal } = useCanUserCreateProposal(); - const { staking } = useNetworkConfig(); + const { staking } = useNetworkConfigStore(); const publicClient = usePublicClient(); // --- Lido Stake button setup --- diff --git a/src/components/DaoCreator/StepController.tsx b/src/components/DaoCreator/StepController.tsx index 3c13b9306a..2d1e83fcd6 100644 --- a/src/components/DaoCreator/StepController.tsx +++ b/src/components/DaoCreator/StepController.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; import { toast } from 'sonner'; import { useAccount } from 'wagmi'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { ChildERC20Steps, ChildERC721Steps, @@ -25,7 +25,7 @@ import useStepRedirect from './hooks/useStepRedirect'; function StepController(props: Omit) { const { t } = useTranslation('daoCreate'); - const { createOptions } = useNetworkConfig(); + const { createOptions } = useNetworkConfigStore(); const location = useLocation(); const { values, mode, setFieldValue } = props; diff --git a/src/components/DaoCreator/StepWrapper.tsx b/src/components/DaoCreator/StepWrapper.tsx index 7696f9e447..aab1e2cd14 100644 --- a/src/components/DaoCreator/StepWrapper.tsx +++ b/src/components/DaoCreator/StepWrapper.tsx @@ -4,7 +4,7 @@ import { ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { BASE_ROUTES, DAO_ROUTES } from '../../constants/routes'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../store/daoInfo/useDaoInfoStore'; import { CreatorSteps } from '../../types'; import PageHeader from '../ui/page/Header/PageHeader'; @@ -49,7 +49,7 @@ export function StepWrapper({ shouldWrapChildren = true, }: IStepWrapper) { const { safe } = useDaoInfoStore(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { t } = useTranslation(['breadcrumbs']); const navigate = useNavigate(); diff --git a/src/components/DaoCreator/formComponents/EstablishEssentials.tsx b/src/components/DaoCreator/formComponents/EstablishEssentials.tsx index 36e576f5fc..aa917e26c9 100644 --- a/src/components/DaoCreator/formComponents/EstablishEssentials.tsx +++ b/src/components/DaoCreator/formComponents/EstablishEssentials.tsx @@ -1,14 +1,21 @@ -import { Box, Input, RadioGroup } from '@chakra-ui/react'; +import { Box, Flex, Icon, Image, Input, RadioGroup } from '@chakra-ui/react'; +import { CheckCircle } from '@phosphor-icons/react'; import debounce from 'lodash.debounce'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useChainId, useSwitchChain } from 'wagmi'; import { createAccountSubstring } from '../../../hooks/utils/useGetAccountName'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { + supportedNetworks, + useNetworkConfigStore, +} from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { GovernanceType, ICreationStepProps, VotingStrategyType } from '../../../types'; +import { getChainIdFromPrefix, getNetworkIcon } from '../../../utils/url'; import { InputComponent, LabelComponent } from '../../ui/forms/InputComponent'; import LabelWrapper from '../../ui/forms/LabelWrapper'; import { RadioWithText } from '../../ui/forms/Radio/RadioWithText'; +import { DropdownMenu } from '../../ui/menus/DropdownMenu'; import { StepButtons } from '../StepButtons'; import { StepWrapper } from '../StepWrapper'; @@ -65,7 +72,9 @@ export function EstablishEssentials(props: ICreationStepProps) { setFieldValue('essentials.governance', value); }; - const { createOptions } = useNetworkConfig(); + const { createOptions, setCurrentConfig, chain, getConfigByChainId, addressPrefix } = + useNetworkConfigStore(); + const walletChainID = useChainId(); const [snapshotENSInput, setSnapshotENSInput] = useState(''); @@ -81,6 +90,24 @@ export function EstablishEssentials(props: ICreationStepProps) { debounceENSInput(snapshotENSInput); }, [debounceENSInput, snapshotENSInput]); + const dropdownItems = supportedNetworks.map(network => ({ + value: network.chain.id.toString(), + label: network.chain.name, + icon: getNetworkIcon(network.addressPrefix), + selected: chain.id === network.chain.id, + })); + + const { switchChain } = useSwitchChain({ + mutation: { + onError: () => { + if (chain.id !== walletChainID) { + const chainId = getChainIdFromPrefix(addressPrefix); + switchChain({ chainId }); + } + }, + }, + }); + return ( <> + + + item.selected)} + onSelect={item => { + setCurrentConfig(getConfigByChainId(Number(item.value))); + }} + title={t('networks')} + isDisabled={false} + renderItem={(item, isSelected) => { + return ( + <> + + + {item.label} + + {isSelected && ( + + )} + + ); + }} + /> + + (); const [hasErrorLoading, setErrorLoading] = useState(false); - const { addressPrefix, subgraph } = useNetworkConfig(); - const chainId = useChainId(); + const { addressPrefix, subgraph, chain } = useNetworkConfigStore(); const publicClient = usePublicClient(); const { getAddressContractType } = useAddressContractType(); @@ -168,7 +167,7 @@ export function DaoHierarchyNode({ if (safeAddress) { const cachedNode = getValue({ cacheName: CacheKeys.HIERARCHY_DAO_INFO, - chainId, + chainId: chain.id, daoAddress: safeAddress, }); if (cachedNode) { @@ -182,7 +181,7 @@ export function DaoHierarchyNode({ setValue( { cacheName: CacheKeys.HIERARCHY_DAO_INFO, - chainId, + chainId: chain.id, daoAddress: safeAddress, }, _node, diff --git a/src/components/ProposalBuilder/index.tsx b/src/components/ProposalBuilder/index.tsx index c8893b956a..a5409d19e4 100644 --- a/src/components/ProposalBuilder/index.tsx +++ b/src/components/ProposalBuilder/index.tsx @@ -11,7 +11,7 @@ import useSubmitProposal from '../../hooks/DAO/proposal/useSubmitProposal'; import useCreateProposalSchema from '../../hooks/schemas/proposalBuilder/useCreateProposalSchema'; import { useCanUserCreateProposal } from '../../hooks/utils/useCanUserSubmitProposal'; import { useFractal } from '../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { useProposalActionsStore } from '../../store/actions/useProposalActionsStore'; import { useDaoInfoStore } from '../../store/daoInfo/useDaoInfoStore'; import { CreateProposalSteps, ProposalExecuteData } from '../../types'; @@ -58,7 +58,7 @@ export function ProposalBuilder({ const { safe } = useDaoInfoStore(); const safeAddress = safe?.address; - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { submitProposal, pendingCreateTx } = useSubmitProposal(); const { canUserCreateProposal } = useCanUserCreateProposal(); const { createProposalValidation } = useCreateProposalSchema(); diff --git a/src/components/ProposalTemplates/ProposalTemplateCard.tsx b/src/components/ProposalTemplates/ProposalTemplateCard.tsx index 9d6d4e19b5..b96855fa60 100644 --- a/src/components/ProposalTemplates/ProposalTemplateCard.tsx +++ b/src/components/ProposalTemplates/ProposalTemplateCard.tsx @@ -7,7 +7,7 @@ import { DAO_ROUTES } from '../../constants/routes'; import useRemoveProposalTemplate from '../../hooks/DAO/proposal/useRemoveProposalTemplate'; import useSubmitProposal from '../../hooks/DAO/proposal/useSubmitProposal'; import { useCanUserCreateProposal } from '../../hooks/utils/useCanUserSubmitProposal'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../store/daoInfo/useDaoInfoStore'; import { ProposalTemplate } from '../../types/proposalBuilder'; import ContentBox from '../ui/containers/ContentBox'; @@ -29,7 +29,7 @@ export default function ProposalTemplateCard({ const { t } = useTranslation('proposalTemplate'); const { safe } = useDaoInfoStore(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { prepareRemoveProposalTemplateProposal } = useRemoveProposalTemplate(); const { submitProposal } = useSubmitProposal(); diff --git a/src/components/Proposals/MultisigProposalDetails/TxActions.tsx b/src/components/Proposals/MultisigProposalDetails/TxActions.tsx index 216c3146d6..a08e560a44 100644 --- a/src/components/Proposals/MultisigProposalDetails/TxActions.tsx +++ b/src/components/Proposals/MultisigProposalDetails/TxActions.tsx @@ -14,7 +14,7 @@ import { useAsyncRequest } from '../../../hooks/utils/useAsyncRequest'; import { useTransaction } from '../../../hooks/utils/useTransaction'; import { useFractal } from '../../../providers/App/AppProvider'; import { useSafeAPI } from '../../../providers/App/hooks/useSafeAPI'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { MultisigProposal, FractalProposalState } from '../../../types'; import { DecentTooltip } from '../../ui/DecentTooltip'; @@ -42,7 +42,7 @@ export function TxActions({ proposal }: { proposal: MultisigProposal }) { } }, [proposal.state]); - const { chain } = useNetworkConfig(); + const { chain } = useNetworkConfigStore(); const { t } = useTranslation(['proposal', 'common', 'transaction']); const [asyncRequest, asyncRequestPending] = useAsyncRequest(); diff --git a/src/components/Proposals/ProposalCard/ProposalCard.tsx b/src/components/Proposals/ProposalCard/ProposalCard.tsx index 55cbf89f83..532c31da8f 100644 --- a/src/components/Proposals/ProposalCard/ProposalCard.tsx +++ b/src/components/Proposals/ProposalCard/ProposalCard.tsx @@ -3,7 +3,7 @@ import { format } from 'date-fns'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { DAO_ROUTES } from '../../../constants/routes'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { AzoriusProposal, FractalProposal, SnapshotProposal } from '../../../types'; import { DEFAULT_DATE_FORMAT } from '../../../utils'; @@ -15,7 +15,7 @@ import { ProposalCountdown } from '../../ui/proposal/ProposalCountdown'; function ProposalCard({ proposal }: { proposal: FractalProposal }) { const { safe } = useDaoInfoStore(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { t } = useTranslation('common'); if (!safe?.address) { diff --git a/src/components/Roles/RoleDetails.tsx b/src/components/Roles/RoleDetails.tsx index 24e942dc7d..4f404e68a6 100644 --- a/src/components/Roles/RoleDetails.tsx +++ b/src/components/Roles/RoleDetails.tsx @@ -10,7 +10,7 @@ import useAvatar from '../../hooks/utils/useAvatar'; import { useCanUserCreateProposal } from '../../hooks/utils/useCanUserSubmitProposal'; import { useCopyText } from '../../hooks/utils/useCopyText'; import { useGetAccountName } from '../../hooks/utils/useGetAccountName'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../store/daoInfo/useDaoInfoStore'; import { paymentSorterByActiveStatus, @@ -79,7 +79,7 @@ export default function RolesDetails({ }) { const { safe } = useDaoInfoStore(); const navigate = useNavigate(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const permissionsContainerRef = useRef(null); const roleHatWearer = 'wearer' in roleHat ? roleHat.wearer : roleHat.wearerAddress; diff --git a/src/components/Roles/RolePaymentDetails.tsx b/src/components/Roles/RolePaymentDetails.tsx index 7c17c37ee9..574bcf1d46 100644 --- a/src/components/Roles/RolePaymentDetails.tsx +++ b/src/components/Roles/RolePaymentDetails.tsx @@ -8,7 +8,7 @@ import { Address, getAddress, Hex } from 'viem'; import { useAccount, usePublicClient } from 'wagmi'; import { DETAILS_BOX_SHADOW, isDemoMode } from '../../constants/common'; import { DAO_ROUTES } from '../../constants/routes'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../store/daoInfo/useDaoInfoStore'; import { useRolesStore } from '../../store/roles/useRolesStore'; import { BigIntValuePair } from '../../types'; @@ -141,7 +141,7 @@ export function RolePaymentDetails({ const { t } = useTranslation(['roles']); const { safe } = useDaoInfoStore(); const { address: connectedAccount } = useAccount(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { refreshWithdrawableAmount } = useRolesStore(); const navigate = useNavigate(); const publicClient = usePublicClient(); diff --git a/src/components/Roles/RoleTerm.tsx b/src/components/Roles/RoleTerm.tsx index 5435953bbc..6b342afc6e 100644 --- a/src/components/Roles/RoleTerm.tsx +++ b/src/components/Roles/RoleTerm.tsx @@ -12,7 +12,7 @@ import useAvatar from '../../hooks/utils/useAvatar'; import { useCopyText } from '../../hooks/utils/useCopyText'; import { useGetAccountName } from '../../hooks/utils/useGetAccountName'; import { useTransaction } from '../../hooks/utils/useTransaction'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { useRolesStore } from '../../store/roles/useRolesStore'; import { RoleFormTermStatus } from '../../types/roles'; import { DEFAULT_DATE_TIME_FORMAT_NO_TZ } from '../../utils'; @@ -284,7 +284,7 @@ export default function RoleTerm({ const { t } = useTranslation(['roles']); const { contracts: { hatsProtocol }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const roleHat = useMemo(() => { if (!hatId) return undefined; diff --git a/src/components/Roles/forms/RoleFormAssetSelector.tsx b/src/components/Roles/forms/RoleFormAssetSelector.tsx index 0f7e2db665..a283537c0b 100644 --- a/src/components/Roles/forms/RoleFormAssetSelector.tsx +++ b/src/components/Roles/forms/RoleFormAssetSelector.tsx @@ -1,18 +1,5 @@ -import { - Button, - Divider, - Flex, - FormControl, - Icon, - Image, - Menu, - MenuButton, - MenuItem, - MenuList, - Show, - Text, -} from '@chakra-ui/react'; -import { CaretDown, CheckCircle } from '@phosphor-icons/react'; +import { Flex, FormControl, Image, Text, Icon } from '@chakra-ui/react'; +import { CheckCircle } from '@phosphor-icons/react'; import { Field, FieldInputProps, @@ -23,136 +10,44 @@ import { } from 'formik'; import { useTranslation } from 'react-i18next'; import { getAddress } from 'viem'; -import { CARD_SHADOW } from '../../../constants/common'; import { useFractal } from '../../../providers/App/AppProvider'; import { BigIntValuePair } from '../../../types'; import { RoleFormValues } from '../../../types/roles'; import { formatCoin, formatUSD } from '../../../utils'; import { MOCK_MORALIS_ETH_ADDRESS } from '../../../utils/address'; -import DraggableDrawer from '../../ui/containers/DraggableDrawer'; import { BigIntInput } from '../../ui/forms/BigIntInput'; import LabelWrapper from '../../ui/forms/LabelWrapper'; -import { EaseOutComponent } from '../../ui/utils/EaseOutComponent'; +import { DropdownMenu } from '../../ui/menus/DropdownMenu'; -function AssetsList({ formIndex }: { formIndex: number }) { - const { t } = useTranslation('roles'); +export function AssetSelector({ formIndex, disabled }: { formIndex: number; disabled?: boolean }) { + const { t } = useTranslation(['roles', 'treasury', 'modals']); + const { values, setFieldValue } = useFormikContext(); const { treasury: { assetsFungible }, } = useFractal(); + const fungibleAssetsWithBalance = assetsFungible.filter( asset => parseFloat(asset.balance) > 0 && - asset.tokenAddress.toLowerCase() !== MOCK_MORALIS_ETH_ADDRESS.toLowerCase(), // Can't stream native token + asset.tokenAddress.toLowerCase() !== MOCK_MORALIS_ETH_ADDRESS.toLowerCase(), ); - const { values, setFieldValue } = useFormikContext(); - const selectedAsset = values.roleEditing?.payments?.[formIndex]?.asset; - if (fungibleAssetsWithBalance.length === 0) { - return ( - - - {t('emptyRolesAssets')} - - - ); - } - - return ( - <> - {fungibleAssetsWithBalance.map((asset, index) => { - const isSelected = selectedAsset?.address === asset.tokenAddress; - return ( - { - setFieldValue(`roleEditing.payments.${formIndex}.asset`, { - name: asset.name, - address: getAddress(asset.tokenAddress), - symbol: asset.symbol, - logo: asset.logo ?? '', - decimals: asset.decimals, - }); - }} - > - - - - - {asset?.symbol} - - - - {formatCoin(asset?.balance, true, asset?.decimals, asset?.symbol, true)} - - {asset?.usdValue && ( - <> - - {'•'} - - - {formatUSD(asset?.usdValue)} - - - )} - - - - {isSelected && ( - - )} - - ); - })} - - ); -} - -export function AssetSelector({ formIndex, disabled }: { formIndex: number; disabled?: boolean }) { - const { t } = useTranslation(['roles', 'treasury', 'modals']); - const { values, setFieldValue } = useFormikContext(); const selectedAsset = values.roleEditing?.payments?.[formIndex]?.asset; + const dropdownItems = fungibleAssetsWithBalance.map(asset => ({ + value: asset.tokenAddress, + label: asset.symbol, + icon: asset.logo ?? asset.thumbnail ?? '/images/coin-icon-default.svg', + selected: selectedAsset?.address === getAddress(asset.tokenAddress), + assetData: { + name: asset.name, + balance: asset.balance, + decimals: asset.decimals, + usdValue: asset.usdValue, + symbol: asset.symbol, + }, + })); + return ( <> {() => ( - - {({ isOpen, onClose }) => ( - <> - + + items={dropdownItems} + selectedItem={dropdownItems.find(item => item.selected)} + onSelect={item => { + const chosenAsset = fungibleAssetsWithBalance.find( + asset => getAddress(asset.tokenAddress) === getAddress(item.value), + ); + if (chosenAsset) { + setFieldValue(`roleEditing.payments.${formIndex}.asset`, { + name: chosenAsset.name, + address: getAddress(chosenAsset.tokenAddress), + symbol: chosenAsset.symbol, + logo: chosenAsset.logo ?? '', + decimals: chosenAsset.decimals, + }); + } else { + setFieldValue(`roleEditing.payments.${formIndex}.asset`, undefined); + } + }} + title={t('titleAssets', { ns: 'treasury' })} + isDisabled={disabled} + selectPlaceholder={t('selectLabel', { ns: 'modals' })} + emptyMessage={t('emptyRolesAssets', { ns: 'roles' })} + renderItem={(item, isSelected) => { + const { balance, decimals, usdValue, symbol } = item.assetData; + const balanceText = formatCoin(balance, true, decimals, symbol, true); + + return ( + <> - - + + - {selectedAsset?.symbol ?? t('selectLabel', { ns: 'modals' })} + {item.label} - - - - - - {}} - onClose={onClose} - closeOnOverlayClick - headerContent={ - - {t('titleAssets', { ns: 'treasury' })} + + {balanceText} - + {usdValue && ( + <> + + {'•'} + + + {formatUSD(usdValue)} + + + )} - } - > - - - - - - - - - {t('titleAssets', { ns: 'treasury' })} - - - - - - - - )} - + + {isSelected && ( + + )} + + ); + }} + /> )} @@ -299,13 +169,14 @@ export function AssetSelector({ formIndex, disabled }: { formIndex: number; disa const paymentAmountBigIntError = meta.error as FormikErrors; const paymentAmountBigIntTouched = meta.touched; const inputDisabled = !values?.roleEditing?.payments?.[formIndex]?.asset || disabled; + return ( @@ -316,11 +187,11 @@ export function AssetSelector({ formIndex, disabled }: { formIndex: number; disa onChange={valuePair => { setFieldValue(`roleEditing.payments.${formIndex}.amount`, valuePair, true); }} - decimalPlaces={values?.roleEditing?.payments?.[formIndex]?.asset?.decimals} + decimalPlaces={selectedAsset?.decimals} onBlur={() => { setFieldTouched(`roleEditing.payments.${formIndex}.amount`, true); }} - cursor={disabled ? 'not-allowed' : 'pointer'} + cursor={inputDisabled ? 'not-allowed' : 'pointer'} placeholder="0" /> diff --git a/src/components/Roles/forms/RoleFormCreateProposal.tsx b/src/components/Roles/forms/RoleFormCreateProposal.tsx index 8c19af9c5f..512c7ae0b3 100644 --- a/src/components/Roles/forms/RoleFormCreateProposal.tsx +++ b/src/components/Roles/forms/RoleFormCreateProposal.tsx @@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom'; import { getAddress, Hex, zeroAddress } from 'viem'; import { CARD_SHADOW } from '../../../constants/common'; import { DAO_ROUTES } from '../../../constants/routes'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { EditedRole, @@ -112,7 +112,7 @@ export function RoleFormCreateProposal({ close }: { close: () => void }) { const { safe } = useDaoInfoStore(); const navigate = useNavigate(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const safeAddress = safe?.address; diff --git a/src/components/Roles/forms/RoleFormTabs.tsx b/src/components/Roles/forms/RoleFormTabs.tsx index 418c6dbe87..04412455b5 100644 --- a/src/components/Roles/forms/RoleFormTabs.tsx +++ b/src/components/Roles/forms/RoleFormTabs.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { Blocker, useNavigate } from 'react-router-dom'; import { Hex } from 'viem'; import { DAO_ROUTES } from '../../../constants/routes'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { useRolesStore } from '../../../store/roles/useRolesStore'; import { EditBadgeStatus, RoleFormValues, RoleHatFormValue } from '../../../types/roles'; @@ -27,7 +27,7 @@ export function RoleFormTabs({ const { hatsTree } = useRolesStore(); const { safe } = useDaoInfoStore(); const navigate = useNavigate(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { editedRoleData, isRoleUpdated, existingRoleHat } = useRoleFormEditedRole({ hatsTree }); const { t } = useTranslation(['roles']); const { values, setFieldValue, errors, setTouched } = useFormikContext(); diff --git a/src/components/SafeSettings/SettingsNavigation.tsx b/src/components/SafeSettings/SettingsNavigation.tsx index 7826c61b84..6fddcbfa1b 100644 --- a/src/components/SafeSettings/SettingsNavigation.tsx +++ b/src/components/SafeSettings/SettingsNavigation.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { Link, useLocation, useMatch } from 'react-router-dom'; import { DAO_ROUTES } from '../../constants/routes'; import { useFractal } from '../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../store/daoInfo/useDaoInfoStore'; import { AzoriusGovernance } from '../../types'; import { BarLoader } from '../ui/loaders/BarLoader'; @@ -81,7 +81,7 @@ function SettingsLink({ export default function SettingsNavigation() { const { t } = useTranslation('settings'); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { governance } = useFractal(); const { safe, modules } = useDaoInfoStore(); const azoriusGovernance = governance as AzoriusGovernance; diff --git a/src/components/SafeSettings/Signers/modals/RemoveSignerModal.tsx b/src/components/SafeSettings/Signers/modals/RemoveSignerModal.tsx index a3ee814f92..85e7eddd32 100644 --- a/src/components/SafeSettings/Signers/modals/RemoveSignerModal.tsx +++ b/src/components/SafeSettings/Signers/modals/RemoveSignerModal.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Address } from 'viem'; import { useEnsName } from 'wagmi'; -import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore'; import { SENTINEL_MODULE } from '../../../../utils/address'; import SupportTooltip from '../../../ui/badges/SupportTooltip'; @@ -32,7 +32,7 @@ function RemoveSignerModal({ const [threshold, setThreshold] = useState(defaultNewThreshold); const [nonce, setNonce] = useState(safe!.nextNonce); - const { chain } = useNetworkConfig(); + const { chain } = useNetworkConfigStore(); const { data: ensName } = useEnsName({ address: selectedSigner, chainId: chain.id, diff --git a/src/components/ui/cards/DAOInfoCard.tsx b/src/components/ui/cards/DAOInfoCard.tsx index be13e79780..88f74ee1e4 100644 --- a/src/components/ui/cards/DAOInfoCard.tsx +++ b/src/components/ui/cards/DAOInfoCard.tsx @@ -1,10 +1,11 @@ -import { Box, Center, Flex, Link, Text } from '@chakra-ui/react'; +import { Box, Center, Flex, Image, Link, Text } from '@chakra-ui/react'; import { Link as RouterLink } from 'react-router-dom'; import { DAO_ROUTES } from '../../../constants/routes'; import { useAccountFavorites } from '../../../hooks/DAO/loaders/useFavorites'; import { useGetAccountName } from '../../../hooks/utils/useGetAccountName'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; +import { getNetworkIcon } from '../../../utils/url'; import { SnapshotButton } from '../badges/Snapshot'; import { FavoriteIcon } from '../icons/FavoriteIcon'; import AddressCopier from '../links/AddressCopier'; @@ -16,7 +17,7 @@ import { ManageDAOMenu } from '../menus/ManageDAO/ManageDAOMenu'; */ export function DAOInfoCard() { const node = useDaoInfoStore(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); // for non Fractal Safes const displayedAddress = node.safe?.address; const { displayName } = useGetAccountName(displayedAddress); @@ -48,19 +49,13 @@ export function DAOInfoCard() { alignItems="center" columnGap="0.5rem" justifyContent="space-between" - mt="0.5rem" flexGrow={1} > - {/* FAVORITE ICON */} - toggleFavorite(displayedAddress, daoName)} - /> - + {/* PARENT TAG */} {!!node.subgraphInfo && node.subgraphInfo.childAddresses.length > 0 && ( )} - {/* SETTINGS MENU BUTTON */} - + + {/* FAVORITE ICON */} + toggleFavorite(displayedAddress, daoName)} + /> + {/* SETTINGS MENU BUTTON */} + + {/* DAO NAME AND ACTIONS */} diff --git a/src/components/ui/forms/ABISelector.tsx b/src/components/ui/forms/ABISelector.tsx index 886df3b685..bfbd9e64f6 100644 --- a/src/components/ui/forms/ABISelector.tsx +++ b/src/components/ui/forms/ABISelector.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { isAddress } from 'viem'; import { useEnsAddress, usePublicClient } from 'wagmi'; import { logError } from '../../../helpers/errorLogging'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { LabelComponent } from './InputComponent'; export type ABIElement = { @@ -26,7 +26,7 @@ interface IABISelector { export default function ABISelector({ target, onChange }: IABISelector) { const [abi, setABI] = useState([]); - const { etherscanAPIUrl } = useNetworkConfig(); + const { etherscanAPIUrl } = useNetworkConfigStore(); const { t } = useTranslation('common'); const { data: ensAddress } = useEnsAddress({ name: target?.toLowerCase() }); const client = usePublicClient(); diff --git a/src/components/ui/links/EtherscanLink.tsx b/src/components/ui/links/EtherscanLink.tsx index 02a8c577f3..a58b245ad6 100644 --- a/src/components/ui/links/EtherscanLink.tsx +++ b/src/components/ui/links/EtherscanLink.tsx @@ -1,7 +1,7 @@ import { Box, LinkProps } from '@chakra-ui/react'; import { useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import ModalTooltip from '../modals/ModalTooltip'; import ExternalLink from './ExternalLink'; @@ -20,7 +20,7 @@ export default function EtherscanLink({ secondaryValue, ...rest }: EtherscanLinkProps) { - const { etherscanBaseURL } = useNetworkConfig(); + const { etherscanBaseURL } = useNetworkConfigStore(); const { t } = useTranslation(); const containerRef = useRef(null); diff --git a/src/components/ui/menus/AccountDisplay/NetworkSelector.tsx b/src/components/ui/menus/AccountDisplay/NetworkSelector.tsx deleted file mode 100644 index 567d4061d2..0000000000 --- a/src/components/ui/menus/AccountDisplay/NetworkSelector.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Box, Flex, Icon, Text } from '@chakra-ui/react'; -import { CaretDown } from '@phosphor-icons/react'; -import { RefObject } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useSwitchChain } from 'wagmi'; -import { - supportedNetworks, - useNetworkConfig, -} from '../../../../providers/NetworkConfig/NetworkConfigProvider'; -import { OptionMenu } from '../OptionMenu'; - -/** - * Network display for menu - */ -export function NetworkSelector({ - containerRef, -}: { - containerRef: RefObject; -}) { - const { t } = useTranslation('menu'); - const { chain } = useNetworkConfig(); - const { switchChain } = useSwitchChain(); - const networksOptions = supportedNetworks.map(network => ({ - optionKey: network.chain.name, - onClick: () => switchChain({ chainId: network.chain.id }), - })); - - return ( - - - - {t('network')} - - - - {chain.name} - - - } - /> - - - - ); -} diff --git a/src/components/ui/menus/AccountDisplay/WalletMenu.tsx b/src/components/ui/menus/AccountDisplay/WalletMenu.tsx index 242767bbee..072615d26a 100644 --- a/src/components/ui/menus/AccountDisplay/WalletMenu.tsx +++ b/src/components/ui/menus/AccountDisplay/WalletMenu.tsx @@ -1,16 +1,14 @@ import { MenuList } from '@chakra-ui/react'; import { Link, Plugs } from '@phosphor-icons/react'; import { useWeb3Modal } from '@web3modal/wagmi/react'; -import { RefObject } from 'react'; import { useTranslation } from 'react-i18next'; import { useAccount, useDisconnect } from 'wagmi'; import { NEUTRAL_2_82_TRANSPARENT } from '../../../../constants/common'; import Divider from '../../utils/Divider'; import { ConnectedWalletMenuItem } from './ConnectedWalletMenuItem'; import { MenuItemButton } from './MenuItemButton'; -import { NetworkSelector } from './NetworkSelector'; -export function WalletMenu({ containerRef }: { containerRef: RefObject }) { +export function WalletMenu() { const user = useAccount(); const { disconnect } = useDisconnect(); const { open } = useWeb3Modal(); @@ -34,8 +32,6 @@ export function WalletMenu({ containerRef }: { containerRef: RefObject )} - - {!user.address && ( - + ); diff --git a/src/components/ui/menus/CreateProposalMenu/index.tsx b/src/components/ui/menus/CreateProposalMenu/index.tsx index ed0638f0ca..4652fea29e 100644 --- a/src/components/ui/menus/CreateProposalMenu/index.tsx +++ b/src/components/ui/menus/CreateProposalMenu/index.tsx @@ -4,13 +4,13 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { Address } from 'viem'; import { DAO_ROUTES } from '../../../../constants/routes'; -import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { OptionMenu } from '../OptionMenu'; export function CreateProposalMenu({ safeAddress }: { safeAddress: Address }) { const { t } = useTranslation('proposal'); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const navigate = useNavigate(); diff --git a/src/components/ui/menus/DAOSearch/SearchDisplay.tsx b/src/components/ui/menus/DAOSearch/SearchDisplay.tsx index b696e7a3be..55d47c5690 100644 --- a/src/components/ui/menus/DAOSearch/SearchDisplay.tsx +++ b/src/components/ui/menus/DAOSearch/SearchDisplay.tsx @@ -4,7 +4,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Address } from 'viem'; import { SafeDisplayRow } from '../../../../pages/home/SafeDisplayRow'; -import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { getNetworkConfig } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore'; import { ErrorBoundary } from '../../utils/ErrorBoundary'; import { MySafesErrorFallback } from '../../utils/MySafesErrorFallback'; @@ -14,12 +14,18 @@ interface ISearchDisplay { errorMessage: string | undefined; address: Address | undefined; onClickView: Function; + chainId: number; } -export function SearchDisplay({ loading, errorMessage, address, onClickView }: ISearchDisplay) { +export function SearchDisplay({ + loading, + errorMessage, + address, + onClickView, + chainId, +}: ISearchDisplay) { const { t } = useTranslation(['common', 'dashboard']); const node = useDaoInfoStore(); - const { addressPrefix } = useNetworkConfig(); const isCurrentSafe = useMemo( () => !!node && !!node?.safe?.address && node.safe.address === address, @@ -85,7 +91,7 @@ export function SearchDisplay({ loading, errorMessage, address, onClickView }: I { onClickView(); }} diff --git a/src/components/ui/menus/DAOSearch/index.tsx b/src/components/ui/menus/DAOSearch/index.tsx index a1a2409f02..a3dc58a0fe 100644 --- a/src/components/ui/menus/DAOSearch/index.tsx +++ b/src/components/ui/menus/DAOSearch/index.tsx @@ -21,7 +21,7 @@ export function DAOSearch() { const { t } = useTranslation(['dashboard']); const [localInput, setLocalInput] = useState(''); const [typing, setTyping] = useState(false); - const { errorMessage, isLoading, address, setSearchString } = useSearchDao(); + const { errorMessage, isLoading, setSearchString, resolvedAddressesWithPrefix } = useSearchDao(); const { isOpen, onOpen, onClose } = useDisclosure(); const ref = useRef(null); @@ -64,9 +64,8 @@ export function DAOSearch() { const showResults = useMemo(() => { if (typing) return false; if (isLoading) return true; - const hasMessage = errorMessage !== undefined || address !== undefined; - return hasMessage; - }, [address, errorMessage, typing, isLoading]); + return errorMessage === undefined; + }, [errorMessage, typing, isLoading]); useEffect(() => { if (localInput) { @@ -149,12 +148,16 @@ export function DAOSearch() { w="full" position="absolute" > - + {resolvedAddressesWithPrefix.map(resolved => ( + + ))} diff --git a/src/components/ui/menus/DropdownMenu.tsx b/src/components/ui/menus/DropdownMenu.tsx new file mode 100644 index 0000000000..9694f412f1 --- /dev/null +++ b/src/components/ui/menus/DropdownMenu.tsx @@ -0,0 +1,306 @@ +import { + Button, + Divider, + Flex, + Icon, + Image, + Menu, + MenuButton, + MenuItem, + MenuList, + Show, + Text, +} from '@chakra-ui/react'; +import { CaretDown, CheckCircle } from '@phosphor-icons/react'; +import { CARD_SHADOW } from '../../../constants/common'; +import DraggableDrawer from '../containers/DraggableDrawer'; +import { EaseOutComponent } from '../utils/EaseOutComponent'; +type DropdownItem = T & { + value: string; + label: string; + icon?: string; + selected?: boolean; +}; +interface DropdownMenuProps { + items: DropdownItem[]; + selectedItem?: DropdownItem; + title?: string; + onSelect: (item: DropdownItem) => void; + isDisabled?: boolean; + selectPlaceholder?: string; + emptyMessage?: string; + /** + * Optional custom renderer for each item. + * If provided, the returned node should include everything inside the MenuItem, + * including the selected icon if desired. + */ + renderItem?: (item: DropdownItem, isSelected: boolean) => React.ReactNode; +} + +export function DropdownMenu({ + items, + selectedItem, + title, + onSelect, + isDisabled, + selectPlaceholder = 'Select', + emptyMessage = 'No items available', + renderItem, +}: DropdownMenuProps) { + return ( + + {({ isOpen, onClose }) => ( + <> + + + {selectedItem?.icon && ( + + )} + + + {selectedItem?.label ?? selectPlaceholder} + + + + + + + {/* Mobile view: Draggable Drawer */} + + {}} + onClose={onClose} + closeOnOverlayClick + headerContent={ + + {title && {title}} + + + } + > + + {items.length === 0 && ( + + + {emptyMessage} + + + )} + {items.map((item, index) => { + const isSelected = !!item.selected; + return ( + { + onSelect(item); + onClose(); + }} + > + {renderItem ? ( + renderItem(item, isSelected) + ) : ( + <> + + {item.icon && ( + + )} + + {item.label} + + + {isSelected && ( + + )} + + )} + + ); + })} + + + + + {/* Desktop view: MenuList */} + + + + {title && ( + <> + + {title} + + + + )} + {items.length === 0 && ( + + + {emptyMessage} + + + )} + {items.map((item, index) => { + const isSelected = !!item.selected; + return ( + onSelect(item)} + > + {renderItem ? ( + renderItem(item, isSelected) + ) : ( + <> + + {item.icon && ( + + )} + + {item.label} + + + {isSelected && ( + + )} + + )} + + ); + })} + + + + + )} + + ); +} diff --git a/src/components/ui/menus/ManageDAO/ManageDAOMenu.tsx b/src/components/ui/menus/ManageDAO/ManageDAOMenu.tsx index 69c9fd4ecc..72a807406d 100644 --- a/src/components/ui/menus/ManageDAO/ManageDAOMenu.tsx +++ b/src/components/ui/menus/ManageDAO/ManageDAOMenu.tsx @@ -15,7 +15,7 @@ import useClawBack from '../../../../hooks/DAO/useClawBack'; import useBlockTimestamp from '../../../../hooks/utils/useBlockTimestamp'; import { useCanUserCreateProposal } from '../../../../hooks/utils/useCanUserSubmitProposal'; import { useFractal } from '../../../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore'; import { FractalModuleType, FreezeVotingType, GovernanceType } from '../../../../types'; import { ModalType } from '../../modals/ModalProvider'; @@ -42,7 +42,7 @@ export function ManageDAOMenu() { }, }); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const handleNavigateToSettings = useCallback(() => { if (safeAddress) { diff --git a/src/components/ui/menus/SafesMenu/SafeMenuItem.tsx b/src/components/ui/menus/SafesMenu/SafeMenuItem.tsx index 47b39b4f95..16e43f0c53 100644 --- a/src/components/ui/menus/SafesMenu/SafeMenuItem.tsx +++ b/src/components/ui/menus/SafesMenu/SafeMenuItem.tsx @@ -2,11 +2,9 @@ import { Button, Flex, Image, MenuItem, Spacer, Text } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { Address } from 'viem'; -import { useSwitchChain } from 'wagmi'; import { DAO_ROUTES } from '../../../../constants/routes'; import useAvatar from '../../../../hooks/utils/useAvatar'; -import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; -import { getChainIdFromPrefix, getNetworkIcon } from '../../../../utils/url'; +import { getNetworkIcon } from '../../../../utils/url'; import Avatar from '../../page/Header/Avatar'; export interface SafeMenuItemProps { @@ -20,26 +18,13 @@ export interface SafeMenuItemProps { export function SafeMenuItem({ address, network, name }: SafeMenuItemProps) { const navigate = useNavigate(); - const { addressPrefix } = useNetworkConfig(); - const { switchChain } = useSwitchChain({ - mutation: { - onSuccess: () => { - navigate(DAO_ROUTES.dao.relative(network, address)); - }, - }, - }); - // if by chance the safe name is an ENS name, let's attempt to get the avatar for that const avatarURL = useAvatar(name); const { t } = useTranslation('dashboard'); const onClickNav = () => { - if (addressPrefix !== network) { - switchChain({ chainId: getChainIdFromPrefix(network) }); - } else { - navigate(DAO_ROUTES.dao.relative(network, address)); - } + navigate(DAO_ROUTES.dao.relative(network, address)); }; return ( diff --git a/src/components/ui/modals/AddStrategyPermissionModal.tsx b/src/components/ui/modals/AddStrategyPermissionModal.tsx index 281239bd71..9ca1380559 100644 --- a/src/components/ui/modals/AddStrategyPermissionModal.tsx +++ b/src/components/ui/modals/AddStrategyPermissionModal.tsx @@ -4,14 +4,14 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { zeroAddress } from 'viem'; import { DAO_ROUTES } from '../../../constants/routes'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { Card } from '../cards/Card'; export default function AddStrategyPermissionModal({ closeModal }: { closeModal: () => void }) { const { t } = useTranslation(['settings', 'common']); const navigate = useNavigate(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { safe } = useDaoInfoStore(); if (!safe) { diff --git a/src/components/ui/modals/ConfirmDeleteStrategyModal.tsx b/src/components/ui/modals/ConfirmDeleteStrategyModal.tsx index 2d30ed4726..d5434c3973 100644 --- a/src/components/ui/modals/ConfirmDeleteStrategyModal.tsx +++ b/src/components/ui/modals/ConfirmDeleteStrategyModal.tsx @@ -6,7 +6,7 @@ import { toast } from 'sonner'; import { DAO_ROUTES } from '../../../constants/routes'; import useVotingStrategiesAddresses from '../../../hooks/utils/useVotingStrategiesAddresses'; import { useFractal } from '../../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useProposalActionsStore } from '../../../store/actions/useProposalActionsStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { AzoriusGovernance, CreateProposalTransaction, ProposalActionType } from '../../../types'; @@ -16,7 +16,7 @@ import { SafePermissionsStrategyAction } from '../../SafeSettings/SafePermission export function ConfirmDeleteStrategyModal({ onClose }: { onClose: () => void }) { const navigate = useNavigate(); const { t } = useTranslation('settings'); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { governance, governanceContracts } = useFractal(); const { safe } = useDaoInfoStore(); const { addAction } = useProposalActionsStore(); diff --git a/src/components/ui/modals/ConfirmModifyGovernanceModal.tsx b/src/components/ui/modals/ConfirmModifyGovernanceModal.tsx index b5a8acffb3..70346a01f8 100644 --- a/src/components/ui/modals/ConfirmModifyGovernanceModal.tsx +++ b/src/components/ui/modals/ConfirmModifyGovernanceModal.tsx @@ -2,14 +2,14 @@ import { Box, Button, Text } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { DAO_ROUTES } from '../../../constants/routes'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import Divider from '../utils/Divider'; export function ConfirmModifyGovernanceModal({ close }: { close: () => void }) { const { t } = useTranslation('modals'); const { safe } = useDaoInfoStore(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); if (!safe?.address) { return null; diff --git a/src/components/ui/modals/ForkProposalTemplateModal.tsx b/src/components/ui/modals/ForkProposalTemplateModal.tsx index 2c6d07c8ff..4f344f6149 100644 --- a/src/components/ui/modals/ForkProposalTemplateModal.tsx +++ b/src/components/ui/modals/ForkProposalTemplateModal.tsx @@ -8,7 +8,7 @@ import { DAO_ROUTES } from '../../../constants/routes'; import { useIsSafe } from '../../../hooks/safe/useIsSafe'; import { validateAddress } from '../../../hooks/schemas/common/useValidationAddress'; import { useCanUserCreateProposal } from '../../../hooks/utils/useCanUserSubmitProposal'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { ProposalTemplate } from '../../../types/proposalBuilder'; import { InputComponent } from '../forms/InputComponent'; @@ -32,7 +32,7 @@ export default function ForkProposalTemplateModal({ const { t } = useTranslation('proposalTemplate'); const navigate = useNavigate(); const publicClient = usePublicClient(); - const { chain, addressPrefix } = useNetworkConfig(); + const { chain, addressPrefix } = useNetworkConfigStore(); const { subgraphInfo } = useDaoInfoStore(); const { isSafe, isSafeLoading } = useIsSafe(targetDAOAddress); diff --git a/src/components/ui/modals/PaymentWithdrawModal.tsx b/src/components/ui/modals/PaymentWithdrawModal.tsx index 32bb69e7b4..3cacfa8bb5 100644 --- a/src/components/ui/modals/PaymentWithdrawModal.tsx +++ b/src/components/ui/modals/PaymentWithdrawModal.tsx @@ -10,7 +10,7 @@ import { convertStreamIdToBigInt } from '../../../hooks/streams/useCreateSablier import useAvatar from '../../../hooks/utils/useAvatar'; import { useGetAccountName } from '../../../hooks/utils/useGetAccountName'; import { useTransaction } from '../../../hooks/utils/useTransaction'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { formatCoin } from '../../../utils'; import { getNetworkIcon } from '../../../utils/url'; import Avatar, { AvatarSize } from '../page/Header/Avatar'; @@ -44,7 +44,7 @@ export function PaymentWithdrawModal({ const { displayName: accountDisplayName } = useGetAccountName(withdrawInformation.recipient); const avatarURL = useAvatar(accountDisplayName); const iconSize = useBreakpointValue({ base: 'sm', md: 'icon' }) || 'sm'; - const { chain, addressPrefix } = useNetworkConfig(); + const { chain, addressPrefix } = useNetworkConfigStore(); const handleWithdraw = useCallback(async () => { if ( diff --git a/src/components/ui/modals/ProposalTemplateModal.tsx b/src/components/ui/modals/ProposalTemplateModal.tsx index 0491a72bcd..92663d5b92 100644 --- a/src/components/ui/modals/ProposalTemplateModal.tsx +++ b/src/components/ui/modals/ProposalTemplateModal.tsx @@ -7,7 +7,7 @@ import { logError } from '../../../helpers/errorLogging'; import { usePrepareProposal } from '../../../hooks/DAO/proposal/usePrepareProposal'; import useSubmitProposal from '../../../hooks/DAO/proposal/useSubmitProposal'; import { useCanUserCreateProposal } from '../../../hooks/utils/useCanUserSubmitProposal'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { BigIntValuePair } from '../../../types'; import { ProposalTemplate } from '../../../types/proposalBuilder'; @@ -26,7 +26,7 @@ export default function ProposalTemplateModal({ onClose, }: IProposalTemplateModalProps) { const { safe } = useDaoInfoStore(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const [filledProposalTransactions, setFilledProposalTransactions] = useState(transactions); const [nonce, setNonce] = useState(safe!.nextNonce); diff --git a/src/components/ui/modals/Stake.tsx b/src/components/ui/modals/Stake.tsx index 92d28b464a..12e1530105 100644 --- a/src/components/ui/modals/Stake.tsx +++ b/src/components/ui/modals/Stake.tsx @@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom'; import { DAO_ROUTES } from '../../../constants/routes'; import useLidoStaking from '../../../hooks/stake/lido/useLidoStaking'; import { useFractal } from '../../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { BigIntValuePair, TokenBalance } from '../../../types'; import { BigIntInput } from '../forms/BigIntInput'; @@ -15,7 +15,7 @@ export default function StakeModal({ close }: { close: () => void }) { treasury: { assetsFungible }, } = useFractal(); const { safe } = useDaoInfoStore(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const navigate = useNavigate(); const { t } = useTranslation('stake'); diff --git a/src/components/ui/page/Global/index.tsx b/src/components/ui/page/Global/index.tsx index c95da3f37c..ae4fef745e 100644 --- a/src/components/ui/page/Global/index.tsx +++ b/src/components/ui/page/Global/index.tsx @@ -1,7 +1,6 @@ import * as amplitude from '@amplitude/analytics-browser'; import * as Sentry from '@sentry/react'; import { useEffect, useState } from 'react'; -import { createPublicClient, http, PublicClient } from 'viem'; import { useAccount } from 'wagmi'; import { useAccountFavorites } from '../../../../hooks/DAO/loaders/useFavorites'; import { @@ -14,14 +13,13 @@ import { getSafeName } from '../../../../hooks/utils/useGetSafeName'; import { getNetworkConfig, supportedNetworks, -} from '../../../../providers/NetworkConfig/NetworkConfigProvider'; +} from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { wagmiConfig } from '../../../../providers/NetworkConfig/web3-modal.config'; import { getChainIdFromPrefix } from '../../../../utils/url'; import { Layout } from '../Layout'; const useUserTracking = () => { const { address } = useAccount(); - useEffect(() => { Sentry.setUser(address ? { id: address } : null); if (address) { @@ -37,8 +35,6 @@ const useUpdateFavoritesCache = (onFavoritesUpdated: () => void) => { useEffect(() => { (async () => { - const publicClientsByChain = new Map(); - const favoriteNames = await Promise.all( favoritesList.map(async favorite => { const favoriteChain = wagmiConfig.chains.find( @@ -55,22 +51,9 @@ const useUpdateFavoritesCache = (onFavoritesUpdated: () => void) => { return; } - let favoritePublicClient = publicClientsByChain.get(favoriteChain.id); - - if (!favoritePublicClient) { - favoritePublicClient = createPublicClient({ - chain: favoriteChain, - transport: http(favoriteNetwork.rpcEndpoint), - }); - publicClientsByChain.set(favoriteChain.id, favoritePublicClient); - } - const networkConfig = getNetworkConfig(favoriteChain.id); - return Promise.all([ - favorite, - getSafeName(favoritePublicClient, networkConfig.subgraph, favorite.address), - ]); + return Promise.all([favorite, getSafeName(networkConfig.subgraph, favorite.address)]); }), ); diff --git a/src/components/ui/page/Header/PageHeader.tsx b/src/components/ui/page/Header/PageHeader.tsx index 1386663602..6dfc559ff7 100644 --- a/src/components/ui/page/Header/PageHeader.tsx +++ b/src/components/ui/page/Header/PageHeader.tsx @@ -4,7 +4,7 @@ import { ReactNode, useEffect, useState } from 'react'; import { CONTENT_MAXW } from '../../../../constants/common'; import { DAO_ROUTES } from '../../../../constants/routes'; import { createAccountSubstring } from '../../../../hooks/utils/useGetAccountName'; -import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore'; import AddressCopier from '../../links/AddressCopier'; import Divider from '../../utils/Divider'; @@ -34,7 +34,7 @@ function PageHeader({ }: PageHeaderProps) { const { safe, subgraphInfo } = useDaoInfoStore(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const safeAddress = safe?.address; const [links, setLinks] = useState([...breadcrumbs]); diff --git a/src/components/ui/page/Navigation/NavigationLinks.tsx b/src/components/ui/page/Navigation/NavigationLinks.tsx index 62c67636d7..7855ef2536 100644 --- a/src/components/ui/page/Navigation/NavigationLinks.tsx +++ b/src/components/ui/page/Navigation/NavigationLinks.tsx @@ -2,7 +2,7 @@ import { Box, Flex, Hide } from '@chakra-ui/react'; import { BookOpen, Coins, GitFork, House, Question, UsersThree } from '@phosphor-icons/react'; import { DAO_ROUTES } from '../../../../constants/routes'; import { URL_DOCS, URL_FAQ } from '../../../../constants/url'; -import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore'; import Divider from '../../utils/Divider'; import { NavigationLink } from './NavigationLink'; @@ -44,7 +44,7 @@ function ExternalLinks({ closeDrawer }: { closeDrawer?: () => void }) { function InternalLinks({ closeDrawer }: { closeDrawer?: () => void }) { const { safe } = useDaoInfoStore(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const safeAddress = safe?.address; diff --git a/src/hooks/DAO/loaders/useDecentTreasury.ts b/src/hooks/DAO/loaders/useDecentTreasury.ts index 2c59c919ec..5b743079ee 100644 --- a/src/hooks/DAO/loaders/useDecentTreasury.ts +++ b/src/hooks/DAO/loaders/useDecentTreasury.ts @@ -12,7 +12,7 @@ import { useFractal } from '../../../providers/App/AppProvider'; import useBalancesAPI from '../../../providers/App/hooks/useBalancesAPI'; import { useSafeAPI } from '../../../providers/App/hooks/useSafeAPI'; import { TreasuryAction } from '../../../providers/App/treasury/action'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { TokenEventType, @@ -33,7 +33,7 @@ export const useDecentTreasury = () => { const safeAPI = useSafeAPI(); const { getTokenBalances, getNFTBalances, getDeFiBalances } = useBalancesAPI(); - const { chain, nativeTokenIcon } = useNetworkConfig(); + const { chain, nativeTokenIcon } = useNetworkConfigStore(); const safeAddress = safe?.address; const publicClient = usePublicClient(); diff --git a/src/hooks/DAO/loaders/useFavorites.ts b/src/hooks/DAO/loaders/useFavorites.ts index ac4a9a3740..2c07abcf16 100644 --- a/src/hooks/DAO/loaders/useFavorites.ts +++ b/src/hooks/DAO/loaders/useFavorites.ts @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useBetween } from 'use-between'; import { Address } from 'viem'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { CacheKeys, CacheExpiry, FavoritesCacheValue } from '../../utils/cache/cacheDefaults'; import { getValue, setValue } from '../../utils/cache/useLocalStorage'; @@ -17,7 +17,7 @@ const useSharedAccountFavorites = () => { export const useAccountFavorites = () => { const { favoritesList, setFavoritesList } = useBetween(useSharedAccountFavorites); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const toggleFavorite = (address: Address, name: string) => { const favorites = getValue({ cacheName: CacheKeys.FAVORITES }) || []; diff --git a/src/hooks/DAO/loaders/useFractalGovernance.ts b/src/hooks/DAO/loaders/useFractalGovernance.ts index 75bb5e8fd9..3187a6800e 100644 --- a/src/hooks/DAO/loaders/useFractalGovernance.ts +++ b/src/hooks/DAO/loaders/useFractalGovernance.ts @@ -4,7 +4,7 @@ import { DAOQueryDocument } from '../../../../.graphclient'; import { useFractal } from '../../../providers/App/AppProvider'; import { FractalGovernanceAction } from '../../../providers/App/governance/action'; import useIPFSClient from '../../../providers/App/hooks/useIPFSClient'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { GovernanceType, ProposalTemplate } from '../../../types'; import { useERC20LinearStrategy } from './governance/useERC20LinearStrategy'; @@ -38,7 +38,7 @@ export const useFractalGovernance = () => { const ONE_MINUTE = 60 * 1000; - const { subgraph } = useNetworkConfig(); + const { subgraph } = useNetworkConfigStore(); useQuery(DAOQueryDocument, { variables: { safeAddress }, diff --git a/src/hooks/DAO/loaders/useFractalGuardContracts.ts b/src/hooks/DAO/loaders/useFractalGuardContracts.ts index 9600a335ac..61e14095ce 100644 --- a/src/hooks/DAO/loaders/useFractalGuardContracts.ts +++ b/src/hooks/DAO/loaders/useFractalGuardContracts.ts @@ -4,7 +4,7 @@ import { getContract, zeroAddress } from 'viem'; import { usePublicClient } from 'wagmi'; import { useFractal } from '../../../providers/App/AppProvider'; import { GuardContractAction } from '../../../providers/App/guardContracts/action'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { FreezeGuardType, FreezeVotingType } from '../../../types'; import { useAddressContractType } from '../../utils/useAddressContractType'; @@ -19,7 +19,7 @@ export const useFractalGuardContracts = ({ loadOnMount = true }: { loadOnMount?: const safeAddress = safe?.address; - const { chain } = useNetworkConfig(); + const { chain } = useNetworkConfigStore(); const { getAddressContractType } = useAddressContractType(); diff --git a/src/hooks/DAO/loaders/useFractalNode.ts b/src/hooks/DAO/loaders/useFractalNode.ts index 007db1291e..863057f958 100644 --- a/src/hooks/DAO/loaders/useFractalNode.ts +++ b/src/hooks/DAO/loaders/useFractalNode.ts @@ -4,23 +4,25 @@ import { Address, getAddress, isAddress } from 'viem'; import { DAOQueryDocument } from '../../../../.graphclient'; import { useFractal } from '../../../providers/App/AppProvider'; import { useSafeAPI } from '../../../providers/App/hooks/useSafeAPI'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { useDecentModules } from './useDecentModules'; export const useFractalNode = ({ addressPrefix, safeAddress, + wrongNetwork, }: { addressPrefix?: string; safeAddress?: Address; + wrongNetwork?: boolean; }) => { const safeApi = useSafeAPI(); const lookupModules = useDecentModules(); // tracks the current valid Safe address and chain id; helps prevent unnecessary calls const currentValidSafe = useRef(); const [errorLoading, setErrorLoading] = useState(false); - const { subgraph } = useNetworkConfig(); + const { subgraph } = useNetworkConfigStore(); const [getDAOInfo] = useLazyQuery(DAOQueryDocument, { context: { subgraphSpace: subgraph.space, @@ -85,11 +87,11 @@ export const useFractalNode = ({ ]); useEffect(() => { - if (`${addressPrefix}${safeAddress}` !== currentValidSafe.current) { + if (`${addressPrefix}${safeAddress}` !== currentValidSafe.current && !wrongNetwork) { reset({ error: false }); setDAO(); } - }, [addressPrefix, safeAddress, setDAO, reset]); + }, [addressPrefix, safeAddress, setDAO, reset, wrongNetwork]); return { errorLoading }; }; diff --git a/src/hooks/DAO/loaders/useHatsTree.ts b/src/hooks/DAO/loaders/useHatsTree.ts index 17727b780e..5d7488054e 100644 --- a/src/hooks/DAO/loaders/useHatsTree.ts +++ b/src/hooks/DAO/loaders/useHatsTree.ts @@ -9,7 +9,7 @@ import { StreamsQueryDocument } from '../../../../.graphclient'; import { SablierV2LockupLinearAbi } from '../../../assets/abi/SablierV2LockupLinear'; import { useFractal } from '../../../providers/App/AppProvider'; import useIPFSClient from '../../../providers/App/hooks/useIPFSClient'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { DecentHatsError } from '../../../store/roles/rolesStoreUtils'; import { useRolesStore } from '../../../store/roles/useRolesStore'; import { SablierPayment } from '../../../types/roles'; @@ -45,7 +45,7 @@ const useHatsTree = () => { hatsAccount1ofNMasterCopy: hatsAccountImplementation, hatsElectionsEligibilityMasterCopy: hatsElectionsImplementation, }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const publicClient = usePublicClient(); const apolloClient = useApolloClient(); diff --git a/src/hooks/DAO/proposal/useCreateProposalTemplate.ts b/src/hooks/DAO/proposal/useCreateProposalTemplate.ts index b24f73f650..c5cc8063f6 100644 --- a/src/hooks/DAO/proposal/useCreateProposalTemplate.ts +++ b/src/hooks/DAO/proposal/useCreateProposalTemplate.ts @@ -6,7 +6,7 @@ import { normalize } from 'viem/ens'; import { usePublicClient } from 'wagmi'; import { useFractal } from '../../../providers/App/AppProvider'; import useIPFSClient from '../../../providers/App/hooks/useIPFSClient'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { ProposalExecuteData } from '../../../types'; import { CreateProposalForm } from '../../../types/proposalBuilder'; import { validateENSName } from '../../../utils/url'; @@ -28,7 +28,7 @@ export default function useCreateProposalTemplate() { const { contracts: { keyValuePairs }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const { t } = useTranslation('proposalMetadata'); diff --git a/src/hooks/DAO/proposal/useRemoveProposalTemplate.ts b/src/hooks/DAO/proposal/useRemoveProposalTemplate.ts index b5652ecee4..5b1e12e8c9 100644 --- a/src/hooks/DAO/proposal/useRemoveProposalTemplate.ts +++ b/src/hooks/DAO/proposal/useRemoveProposalTemplate.ts @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { encodeFunctionData } from 'viem'; import { useFractal } from '../../../providers/App/AppProvider'; import useIPFSClient from '../../../providers/App/hooks/useIPFSClient'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { ProposalExecuteData } from '../../../types'; export default function useRemoveProposalTemplate() { @@ -15,7 +15,7 @@ export default function useRemoveProposalTemplate() { const { contracts: { keyValuePairs }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const { t } = useTranslation('proposalMetadata'); diff --git a/src/hooks/DAO/proposal/useSubmitProposal.ts b/src/hooks/DAO/proposal/useSubmitProposal.ts index 05efdc6f7e..28b10d6e45 100644 --- a/src/hooks/DAO/proposal/useSubmitProposal.ts +++ b/src/hooks/DAO/proposal/useSubmitProposal.ts @@ -21,7 +21,7 @@ import { useFractal } from '../../../providers/App/AppProvider'; import { FractalGovernanceAction } from '../../../providers/App/governance/action'; import useIPFSClient from '../../../providers/App/hooks/useIPFSClient'; import { useSafeAPI } from '../../../providers/App/hooks/useSafeAPI'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { CreateProposalMetadata, MetaTransaction, ProposalExecuteData } from '../../../types'; import { buildSafeApiUrl, getAzoriusModuleFromModules } from '../../../utils'; @@ -102,7 +102,7 @@ export default function useSubmitProposal() { safeBaseURL, addressPrefix, contracts: { multiSendCallOnly }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const ipfsClient = useIPFSClient(); const pendingProposalAdd = useCallback( diff --git a/src/hooks/DAO/useBuildDAOTx.ts b/src/hooks/DAO/useBuildDAOTx.ts index 3075365a12..0ee80c19cd 100644 --- a/src/hooks/DAO/useBuildDAOTx.ts +++ b/src/hooks/DAO/useBuildDAOTx.ts @@ -3,7 +3,7 @@ import { Address } from 'viem'; import { useAccount, usePublicClient } from 'wagmi'; import { TxBuilderFactory } from '../../models/TxBuilderFactory'; import { useFractal } from '../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { AzoriusERC20DAO, AzoriusERC721DAO, @@ -35,7 +35,7 @@ const useBuildDAOTx = () => { freezeVotingErc721MasterCopy, freezeVotingMultisigMasterCopy, }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const { governance, diff --git a/src/hooks/DAO/useCreateSubDAOProposal.ts b/src/hooks/DAO/useCreateSubDAOProposal.ts index 0551db3c50..39942b3a14 100644 --- a/src/hooks/DAO/useCreateSubDAOProposal.ts +++ b/src/hooks/DAO/useCreateSubDAOProposal.ts @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { encodeFunctionData, isHex } from 'viem'; import MultiSendCallOnlyAbi from '../../assets/abi/MultiSendCallOnly'; import { useFractal } from '../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../store/daoInfo/useDaoInfoStore'; import { AzoriusERC20DAO, @@ -28,7 +28,7 @@ export const useCreateSubDAOProposal = () => { const { safe } = useDaoInfoStore(); const { contracts: { multiSendCallOnly, keyValuePairs }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const azoriusGovernance = governance as AzoriusGovernance; const safeAddress = safe?.address; diff --git a/src/hooks/DAO/useDeployAzorius.ts b/src/hooks/DAO/useDeployAzorius.ts index de40bd3978..65a96caa9f 100644 --- a/src/hooks/DAO/useDeployAzorius.ts +++ b/src/hooks/DAO/useDeployAzorius.ts @@ -11,7 +11,7 @@ import { SENTINEL_ADDRESS } from '../../constants/common'; import { DAO_ROUTES } from '../../constants/routes'; import { TxBuilderFactory } from '../../models/TxBuilderFactory'; import { useSafeAPI } from '../../providers/App/hooks/useSafeAPI'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../store/daoInfo/useDaoInfoStore'; import { AzoriusERC20DAO, @@ -51,7 +51,7 @@ const useDeployAzorius = () => { freezeVotingMultisigMasterCopy, }, addressPrefix, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const { safe, subgraphInfo } = useDaoInfoStore(); const { t } = useTranslation(['transaction', 'proposalMetadata']); diff --git a/src/hooks/DAO/useDeployDAO.ts b/src/hooks/DAO/useDeployDAO.ts index 62c50138a1..9cbfed3c52 100644 --- a/src/hooks/DAO/useDeployDAO.ts +++ b/src/hooks/DAO/useDeployDAO.ts @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { Address, getContract, isHex } from 'viem'; import { useWalletClient } from 'wagmi'; import MultiSendCallOnlyAbi from '../../assets/abi/MultiSendCallOnly'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { SafeMultisigDAO, AzoriusERC20DAO, AzoriusERC721DAO } from '../../types'; import { useTransaction } from '../utils/useTransaction'; import useBuildDAOTx from './useBuildDAOTx'; @@ -17,7 +17,7 @@ const useDeployDAO = () => { const { addressPrefix, contracts: { multiSendCallOnly }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const { data: walletClient } = useWalletClient(); diff --git a/src/hooks/DAO/useKeyValuePairs.ts b/src/hooks/DAO/useKeyValuePairs.ts index 6cb4fbdfe8..ad8b80d86d 100644 --- a/src/hooks/DAO/useKeyValuePairs.ts +++ b/src/hooks/DAO/useKeyValuePairs.ts @@ -4,7 +4,7 @@ import { useEffect } from 'react'; import { Address, GetContractEventsReturnType, getContract } from 'viem'; import { usePublicClient } from 'wagmi'; import { logError } from '../../helpers/errorLogging'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../store/daoInfo/useDaoInfoStore'; import { useRolesStore } from '../../store/roles/useRolesStore'; @@ -96,7 +96,7 @@ const useKeyValuePairs = () => { const { chain, contracts: { keyValuePairs, sablierV2LockupLinear }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const { setHatKeyValuePairData, resetHatsStore } = useRolesStore(); const safeAddress = node.safe?.address; diff --git a/src/hooks/DAO/useParseSafeAddress.ts b/src/hooks/DAO/useParseSafeAddress.ts index fe5713527d..19f0b76899 100644 --- a/src/hooks/DAO/useParseSafeAddress.ts +++ b/src/hooks/DAO/useParseSafeAddress.ts @@ -1,7 +1,7 @@ import { useSearchParams } from 'react-router-dom'; import { isAddress } from 'viem'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; import { validPrefixes } from '../../providers/NetworkConfig/networks'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; export const useParseSafeAddress = () => { const [searchParams] = useSearchParams(); @@ -10,7 +10,7 @@ export const useParseSafeAddress = () => { const queryAddressPrefix = queryPrefixAndAddress?.[0]; const queryDaoAddress = queryPrefixAndAddress?.[1]; - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); if ( queryAddressPrefix === undefined || diff --git a/src/hooks/DAO/useSearchDao.ts b/src/hooks/DAO/useSearchDao.ts index 59f193f586..69c2331194 100644 --- a/src/hooks/DAO/useSearchDao.ts +++ b/src/hooks/DAO/useSearchDao.ts @@ -1,38 +1,75 @@ -import { useEffect, useState } from 'react'; +import SafeApiKit from '@safe-global/api-kit'; +import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; -import { useIsSafe } from '../safe/useIsSafe'; -import useAddress from '../utils/useAddress'; +import { Address } from 'viem'; +import { supportedNetworks } from '../../providers/NetworkConfig/useNetworkConfigStore'; +import { useResolveENSName } from '../utils/useResolveENSName'; +type ResolvedAddressWithPrefix = { + address: Address; + chainId: number; +}; export const useSearchDao = () => { + const { t } = useTranslation('dashboard'); + const { resolveENSName, isLoading: isAddressLoading } = useResolveENSName(); const [searchString, setSearchString] = useState(''); const [errorMessage, setErrorMessage] = useState(); - const { address, isValid, isLoading: isAddressLoading } = useAddress(searchString); - const { isSafe, isSafeLoading } = useIsSafe(address); - const { t } = useTranslation('dashboard'); - const { chain } = useNetworkConfig(); + const [isSafeLookupLoading, setIsSafeLookupLoading] = useState(false); + const [resolvedAddressesWithPrefix, setSafeResolvedAddressesWithPrefix] = useState< + ResolvedAddressWithPrefix[] + >([]); + + const findSafes = useCallback( + async (resolvedAddressesWithChainId: { address: Address; chainId: number }[]) => { + setIsSafeLookupLoading(true); + for await (const resolved of resolvedAddressesWithChainId) { + const safeAPI = new SafeApiKit({ chainId: BigInt(resolved.chainId) }); + safeAPI.getSafeCreationInfo(resolved.address); + try { + await safeAPI.getSafeCreationInfo(resolved.address); - const isLoading = isAddressLoading === true || isSafeLoading === true; + setSafeResolvedAddressesWithPrefix(prevState => [...prevState, resolved]); + } catch (e) { + // Safe not found + continue; + } + } + setIsSafeLookupLoading(false); + }, + [], + ); + + const resolveInput = useCallback( + async (input: string) => { + const { resolvedAddress, isValid } = await resolveENSName(input); + if (isValid) { + await findSafes( + supportedNetworks.map(network => ({ + address: resolvedAddress, + chainId: network.chain.id, + })), + ); + } else { + setErrorMessage('Invalid search'); + } + }, + [findSafes, resolveENSName], + ); useEffect(() => { setErrorMessage(undefined); - - if (searchString === '' || isLoading || isSafe || isValid === undefined) { + setSafeResolvedAddressesWithPrefix([]); + if (searchString === '') { return; } - - if (isValid === true) { - setErrorMessage(t('errorFailedSearch', { chain: chain.name })); - } else { - setErrorMessage(t('errorInvalidSearch')); - } - }, [chain.name, isLoading, isSafe, isValid, searchString, t]); + resolveInput(searchString).catch(() => setErrorMessage(t('errorInvalidSearch'))); + }, [resolveInput, searchString, t]); return { + resolvedAddressesWithPrefix, errorMessage, - isLoading, - address, + isLoading: isAddressLoading || isSafeLookupLoading, setSearchString, searchString, }; diff --git a/src/hooks/schemas/DAOCreate/useDAOCreateTests.ts b/src/hooks/schemas/DAOCreate/useDAOCreateTests.ts index 7b86d4075b..34be96500c 100644 --- a/src/hooks/schemas/DAOCreate/useDAOCreateTests.ts +++ b/src/hooks/schemas/DAOCreate/useDAOCreateTests.ts @@ -4,7 +4,7 @@ import { isAddress, erc20Abi, getContract } from 'viem'; import { usePublicClient } from 'wagmi'; import { AnyObject } from 'yup'; import { logError } from '../../../helpers/errorLogging'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { AddressValidationMap, CreatorFormState, TokenAllocation } from '../../../types'; import { validateENSName } from '../../../utils/url'; import { validateAddress } from '../common/useValidationAddress'; @@ -22,7 +22,7 @@ export function useDAOCreateTests() { const addressValidationMap = useRef(new Map()); const { t } = useTranslation(['daoCreate', 'common']); const publicClient = usePublicClient(); - const { chain } = useNetworkConfig(); + const { chain } = useNetworkConfigStore(); const minValueValidation = useMemo( () => (minValue: number) => { diff --git a/src/hooks/schemas/common/useValidationAddress.tsx b/src/hooks/schemas/common/useValidationAddress.tsx index d3e0e69038..7c9718e46b 100644 --- a/src/hooks/schemas/common/useValidationAddress.tsx +++ b/src/hooks/schemas/common/useValidationAddress.tsx @@ -4,7 +4,7 @@ import { Address, PublicClient, getAddress, isAddress } from 'viem'; import { normalize } from 'viem/ens'; import { usePublicClient } from 'wagmi'; import { AnyObject } from 'yup'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { AddressValidationMap, ERC721TokenConfig } from '../../../types'; import { validateENSName } from '../../../utils/url'; @@ -73,7 +73,7 @@ export const useValidationAddress = () => { const { t } = useTranslation(['daoCreate', 'common', 'modals']); const { safe } = useDaoInfoStore(); - const { chain } = useNetworkConfig(); + const { chain } = useNetworkConfigStore(); const publicClient = usePublicClient(); diff --git a/src/hooks/stake/lido/useLidoStaking.ts b/src/hooks/stake/lido/useLidoStaking.ts index 48f051cfff..fa289acc26 100644 --- a/src/hooks/stake/lido/useLidoStaking.ts +++ b/src/hooks/stake/lido/useLidoStaking.ts @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { encodeFunctionData } from 'viem'; import LidoStEthAbi from '../../../assets/abi/LidoStEthAbi'; import LidoWithdrawalQueueAbi from '../../../assets/abi/LidoWithdrawalQueueAbi'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { ProposalExecuteData } from '../../../types'; import useSubmitProposal from '../../DAO/proposal/useSubmitProposal'; @@ -12,7 +12,7 @@ export default function useLidoStaking() { const { safe } = useDaoInfoStore(); const { staking: { lido }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const { submitProposal } = useSubmitProposal(); const { t } = useTranslation('proposal'); diff --git a/src/hooks/streams/useCreateSablierStream.ts b/src/hooks/streams/useCreateSablierStream.ts index 91e6a89bce..6e36ccce0d 100644 --- a/src/hooks/streams/useCreateSablierStream.ts +++ b/src/hooks/streams/useCreateSablierStream.ts @@ -4,7 +4,7 @@ import { useCallback } from 'react'; import { Address, Hex, encodeFunctionData, erc20Abi, getAddress, zeroAddress } from 'viem'; import GnosisSafeL2 from '../../assets/abi/GnosisSafeL2'; import SablierV2BatchAbi from '../../assets/abi/SablierV2Batch'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../store/daoInfo/useDaoInfoStore'; import { PreparedNewStreamData } from '../../types/roles'; import { SENTINEL_MODULE } from '../../utils/address'; @@ -19,7 +19,7 @@ export function convertStreamIdToBigInt(streamId: string) { export default function useCreateSablierStream() { const { contracts: { sablierV2LockupLinear, sablierV2Batch, decentSablierStreamManagementModule }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const { safe } = useDaoInfoStore(); const safeAddress = safe?.address; diff --git a/src/hooks/utils/cache/useLocalDB.ts b/src/hooks/utils/cache/useLocalDB.ts index 1fb3a1ad91..f8b96f5871 100644 --- a/src/hooks/utils/cache/useLocalDB.ts +++ b/src/hooks/utils/cache/useLocalDB.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import { logError } from '../../../helpers/errorLogging'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { CacheExpiry, CACHE_DEFAULTS, IStorageValue, keyInternal } from './cacheDefaults'; /** @@ -131,7 +131,7 @@ export const getIndexedDBValue = async ( }; export const useIndexedDB = (objectStoreName: string) => { - const { chain } = useNetworkConfig(); + const { chain } = useNetworkConfigStore(); const set = useCallback( async (key: string, value: any, expirationMinutes: number = CacheExpiry.ONE_WEEK) => { diff --git a/src/hooks/utils/useAutomaticSwitchChain.ts b/src/hooks/utils/useAutomaticSwitchChain.ts index 570c3b5095..7fc2f36a48 100644 --- a/src/hooks/utils/useAutomaticSwitchChain.ts +++ b/src/hooks/utils/useAutomaticSwitchChain.ts @@ -1,35 +1,45 @@ import { useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { toast } from 'sonner'; -import { useSwitchChain } from 'wagmi'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useSwitchChain, useWalletClient } from 'wagmi'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { getChainIdFromPrefix } from '../../utils/url'; export const useAutomaticSwitchChain = ({ - addressPrefix, + urlAddressPrefix, }: { - addressPrefix: string | undefined; + urlAddressPrefix: string | undefined; }) => { - const { t } = useTranslation(['common']); - const networkConfig = useNetworkConfig(); + const { setCurrentConfig, getConfigByChainId, addressPrefix } = useNetworkConfigStore(); + const { isFetchedAfterMount } = useWalletClient(); const { switchChain } = useSwitchChain({ mutation: { - onSuccess: () => { - window.location.reload(); - }, - onError: error => { - if (error.name !== 'UserRejectedRequestError') { - toast.warning(t('automaticChainSwitchingErrorMessage')); + onError: () => { + if (addressPrefix !== urlAddressPrefix && urlAddressPrefix !== undefined) { + const chainId = getChainIdFromPrefix(urlAddressPrefix); + switchChain({ chainId }); } }, }, }); useEffect(() => { - if (addressPrefix === undefined || networkConfig.addressPrefix === addressPrefix) { + if (urlAddressPrefix === undefined || addressPrefix === urlAddressPrefix) { return; } - - switchChain({ chainId: getChainIdFromPrefix(addressPrefix) }); - }, [switchChain, addressPrefix, networkConfig.addressPrefix]); + const chainId = getChainIdFromPrefix(urlAddressPrefix); + if ( + addressPrefix !== urlAddressPrefix && + urlAddressPrefix !== undefined && + isFetchedAfterMount + ) { + switchChain({ chainId }); + } + setTimeout(() => setCurrentConfig(getConfigByChainId(chainId)), 300); + }, [ + addressPrefix, + setCurrentConfig, + getConfigByChainId, + urlAddressPrefix, + switchChain, + isFetchedAfterMount, + ]); }; diff --git a/src/hooks/utils/useAvatar.ts b/src/hooks/utils/useAvatar.ts index 8712b60dff..f491a71a1e 100644 --- a/src/hooks/utils/useAvatar.ts +++ b/src/hooks/utils/useAvatar.ts @@ -1,8 +1,8 @@ import { useEnsAvatar } from 'wagmi'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; const useAvatar = (name: string) => { - const { chain } = useNetworkConfig(); + const { chain } = useNetworkConfigStore(); const { data: avatarURL } = useEnsAvatar({ name, chainId: chain.id, diff --git a/src/hooks/utils/useCreateRoles.ts b/src/hooks/utils/useCreateRoles.ts index be978509bf..6490f83495 100644 --- a/src/hooks/utils/useCreateRoles.ts +++ b/src/hooks/utils/useCreateRoles.ts @@ -35,7 +35,7 @@ import { getRandomBytes } from '../../helpers'; import { generateContractByteCodeLinear } from '../../models/helpers/utils'; import { useFractal } from '../../providers/App/AppProvider'; import useIPFSClient from '../../providers/App/hooks/useIPFSClient'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../store/daoInfo/useDaoInfoStore'; import { useRolesStore } from '../../store/roles/useRolesStore'; import { @@ -110,7 +110,7 @@ export default function useCreateRoles() { decentAutonomousAdminV1MasterCopy, hatsElectionsEligibilityMasterCopy, }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const { t } = useTranslation(['roles', 'navigation', 'modals', 'common']); diff --git a/src/hooks/utils/useGetSafeName.ts b/src/hooks/utils/useGetSafeName.ts index e7598543ca..356c0e7201 100644 --- a/src/hooks/utils/useGetSafeName.ts +++ b/src/hooks/utils/useGetSafeName.ts @@ -1,25 +1,27 @@ import { useCallback } from 'react'; -import { Address, ChainDoesNotSupportContract, PublicClient } from 'viem'; -import { usePublicClient } from 'wagmi'; +import { Address, http, createPublicClient } from 'viem'; import { DAOQueryDocument } from '../../../.graphclient'; import graphQLClient from '../../graphql'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { + supportedNetworks, + useNetworkConfigStore, +} from '../../providers/NetworkConfig/useNetworkConfigStore'; import { createAccountSubstring } from './useGetAccountName'; export const getSafeName = async ( - publicClient: PublicClient, subgraph: { space: number; slug: string; version: string }, address: Address, ) => { - const ensName = await publicClient.getEnsName({ address }).catch((error: Error) => { - if (error.name === ChainDoesNotSupportContract.name) { - // Sliently fail, this is fine. - // https://github.com/wevm/viem/discussions/781 - } else { - throw error; - } - }); + const mainnet = supportedNetworks.find(network => network.chain.id === 1); + if (!mainnet) { + throw new Error('Mainnet not found'); + } + const mainnetPublicClient = createPublicClient({ + chain: mainnet.chain, + transport: http(mainnet.rpcEndpoint), + }); + const ensName = await mainnetPublicClient.getEnsName({ address }); if (ensName) { return ensName; } @@ -44,18 +46,14 @@ export const getSafeName = async ( }; export const useGetSafeName = (chainId?: number) => { - const publicClient = usePublicClient({ chainId }); - const { subgraph } = useNetworkConfig(chainId); + const { getConfigByChainId } = useNetworkConfigStore(); return { getSafeName: useCallback( (address: Address) => { - if (!publicClient) { - throw new Error('Public client not available'); - } - return getSafeName(publicClient, subgraph, address); + return getSafeName(getConfigByChainId(chainId).subgraph, address); }, - [publicClient, subgraph], + [chainId, getConfigByChainId], ), }; }; diff --git a/src/hooks/utils/useResolveENSName.ts b/src/hooks/utils/useResolveENSName.ts new file mode 100644 index 0000000000..7ba84734b9 --- /dev/null +++ b/src/hooks/utils/useResolveENSName.ts @@ -0,0 +1,60 @@ +import { useState, useCallback } from 'react'; +import { Address, createPublicClient, http, isAddress, getAddress, zeroAddress } from 'viem'; +import { normalize } from 'viem/ens'; +import { supportedNetworks } from '../../providers/NetworkConfig/useNetworkConfigStore'; + +type ResolveENSNameReturnType = { + resolvedAddress: Address; + isValid: boolean; +}; +export const useResolveENSName = () => { + const [isLoading, setIsLoading] = useState(false); + + const resolveENSName = useCallback(async (input: string): Promise => { + setIsLoading(true); + + const returnedResult: ResolveENSNameReturnType = { + resolvedAddress: zeroAddress, + isValid: false, + }; + + if (input === '') { + throw new Error('ENS name is empty'); + } + + if (isAddress(input)) { + // @dev if its a valid address, its valid on all networks + returnedResult.isValid = true; + returnedResult.resolvedAddress = getAddress(input); + setIsLoading(false); + return returnedResult; + } + + // @dev if its not an address, try to resolve as possible ENS name on all networks + let normalizedName: string; + try { + normalizedName = normalize(input); + } catch { + setIsLoading(false); + return returnedResult; + } + const mainnet = supportedNetworks.find(network => network.chain.id === 1); + if (!mainnet) { + throw new Error('Mainnet not found'); + } + + const mainnetPublicClient = createPublicClient({ + chain: mainnet.chain, + transport: http(mainnet.rpcEndpoint), + }); + const resolvedAddress = await mainnetPublicClient.getEnsAddress({ name: normalizedName }); + if (resolvedAddress) { + returnedResult.resolvedAddress = resolvedAddress; + returnedResult.isValid = true; + } + + setIsLoading(false); + return returnedResult; + }, []); + return { resolveENSName, isLoading }; +}; diff --git a/src/hooks/utils/useSafeDecoder.tsx b/src/hooks/utils/useSafeDecoder.tsx index 49c3d0aa80..af2b379ef2 100644 --- a/src/hooks/utils/useSafeDecoder.tsx +++ b/src/hooks/utils/useSafeDecoder.tsx @@ -1,7 +1,7 @@ import axios from 'axios'; import { useCallback } from 'react'; import { Address, encodePacked, keccak256 } from 'viem'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { DecodedTransaction, DecodedTxParam } from '../../types'; import { buildSafeApiUrl, parseMultiSendTransactions } from '../../utils'; import { CacheKeys } from './cache/cacheDefaults'; @@ -10,7 +10,7 @@ import { DBObjectKeys, useIndexedDB } from './cache/useLocalDB'; * Handles decoding and caching transactions via the Safe API. */ export const useSafeDecoder = () => { - const { safeBaseURL } = useNetworkConfig(); + const { safeBaseURL } = useNetworkConfigStore(); const [setValue, getValue] = useIndexedDB(DBObjectKeys.DECODED_TRANSACTIONS); const decode = useCallback( async (value: string, to: Address, data?: string): Promise => { diff --git a/src/i18n/locales/en/daoCreate.json b/src/i18n/locales/en/daoCreate.json index 935a811d55..491a8553c0 100644 --- a/src/i18n/locales/en/daoCreate.json +++ b/src/i18n/locales/en/daoCreate.json @@ -101,6 +101,8 @@ "tooltipNftVoting": "NFT Voting allows a group of ERC-721 (NFT) holders to propose and vote on transactions. Multiple NFT addresses can be used. <1>Learn more", "errorUnsupportedCreateOption": "Previously selected Governance option is not supported on this network.", "attachFractalModuleLabel": "Enable Clawback", + "networks": "Networks", + "networkDescription": "What network would you like to deploy this DAO on?", "attachFractalModuleDescription": "This setting controls whether Parent DAO will be able to execute arbitrary transactions on Child DAO bypassing voting process on Child DAO.", "fractalModuleAttachedDescription": "This setting can not be modified as Fractal Module already attached to the DAO." } diff --git a/src/i18n/locales/en/dashboard.json b/src/i18n/locales/en/dashboard.json index 0c2d403a4f..567b53e56c 100644 --- a/src/i18n/locales/en/dashboard.json +++ b/src/i18n/locales/en/dashboard.json @@ -2,7 +2,7 @@ "emptyFavorites": "You haven't added any DAOs yet.", "loadingFavorite": "Loading DAO Name", "errorInvalidSearch": "Oops! This Ethereum address is invalid.", - "errorFailedSearch": "Sorry, this address is not a DAO on {{chain}}.", + "errorFailedSearch": "Sorry, this address is not a DAO on any supported chain.", "searchDAOPlaceholder": "Enter Address or ENS Name", "titleGovernance": "Governance", "titleType": "Type", diff --git a/src/i18n/locales/en/menu.json b/src/i18n/locales/en/menu.json index 2844121ad1..253f61f7eb 100644 --- a/src/i18n/locales/en/menu.json +++ b/src/i18n/locales/en/menu.json @@ -2,7 +2,6 @@ "connect": "Connect", "connectWallet": "Connect Wallet", "disconnect": "Disconnect", - "network": "Network", "wallet": "Wallet", "titleManageProposalTemplate": "Manage Template", "optionCreateSubDAO": "Create SubDAO", diff --git a/src/main.tsx b/src/main.tsx index f5f6a1eeff..10e805db45 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,12 +7,12 @@ import './assets/css/sentry.css'; import './assets/css/Toast.css'; import './insights'; import { runMigrations } from './hooks/utils/cache/runMigrations'; -import { useNetworkConfig } from './providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from './providers/NetworkConfig/useNetworkConfigStore'; import Providers from './providers/Providers'; import { router } from './router'; function DecentRouterProvider() { - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const urlParams = new URLSearchParams(window.location.search); const addressWithPrefix = urlParams.get('dao'); diff --git a/src/pages/LoadingProblem.tsx b/src/pages/LoadingProblem.tsx index 5d4875c344..1f45e6b783 100644 --- a/src/pages/LoadingProblem.tsx +++ b/src/pages/LoadingProblem.tsx @@ -5,10 +5,10 @@ import { StetoscopeIllustrationDesktop, } from '../components/ui/icons/Icons'; import { CONTENT_MAXW } from '../constants/common'; -import { useNetworkConfig } from '../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../providers/NetworkConfig/useNetworkConfigStore'; -function LoadingProblem({ type }: { type: 'invalidSafe' | 'wrongNetwork' | 'badQueryParam' }) { - const { chain } = useNetworkConfig(); +function LoadingProblem({ type }: { type: 'invalidSafe' | 'badQueryParam' }) { + const { chain } = useNetworkConfigStore(); const { t } = useTranslation('common'); return ( diff --git a/src/pages/dao/SafeController.tsx b/src/pages/dao/SafeController.tsx index 2f3c92f688..52dd46844a 100644 --- a/src/pages/dao/SafeController.tsx +++ b/src/pages/dao/SafeController.tsx @@ -20,17 +20,18 @@ import LoadingProblem from '../LoadingProblem'; export function SafeController() { const { invalidQuery, wrongNetwork, addressPrefix, safeAddress } = useParseSafeAddress(); + useAutomaticSwitchChain({ urlAddressPrefix: addressPrefix }); useUpdateSafeData(safeAddress); usePageTitle(); useTemporaryProposals(); - useAutomaticSwitchChain({ addressPrefix }); const { subgraphInfo } = useDaoInfoStore(); const { errorLoading } = useFractalNode({ addressPrefix, safeAddress, + wrongNetwork, }); useGovernanceContracts(); @@ -48,8 +49,6 @@ export function SafeController() { // the order of the if blocks of these next three error states matters if (invalidQuery) { return ; - } else if (wrongNetwork) { - return ; } else if (errorLoading) { return ; } diff --git a/src/pages/dao/edit/governance/SafeEditGovernancePage.tsx b/src/pages/dao/edit/governance/SafeEditGovernancePage.tsx index f6052c765c..4b86b1c9a9 100644 --- a/src/pages/dao/edit/governance/SafeEditGovernancePage.tsx +++ b/src/pages/dao/edit/governance/SafeEditGovernancePage.tsx @@ -13,7 +13,7 @@ import { DAO_ROUTES } from '../../../../constants/routes'; import useDeployAzorius from '../../../../hooks/DAO/useDeployAzorius'; import { analyticsEvents } from '../../../../insights/analyticsEvents'; import { useFractal } from '../../../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore'; import { AzoriusERC20DAO, @@ -33,7 +33,7 @@ export function SafeEditGovernancePage() { } = useFractal(); const user = useAccount(); const { safe, subgraphInfo } = useDaoInfoStore(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { t } = useTranslation(['daoEdit', 'common', 'breadcrumbs']); const navigate = useNavigate(); const isMultisig = type === GovernanceType.MULTISIG; diff --git a/src/pages/dao/hierarchy/SafeHierarchyPage.tsx b/src/pages/dao/hierarchy/SafeHierarchyPage.tsx index ef1dcf7a8c..0cee833161 100644 --- a/src/pages/dao/hierarchy/SafeHierarchyPage.tsx +++ b/src/pages/dao/hierarchy/SafeHierarchyPage.tsx @@ -10,7 +10,7 @@ import { useHeaderHeight } from '../../../constants/common'; import { DAO_ROUTES } from '../../../constants/routes'; import { useCanUserCreateProposal } from '../../../hooks/utils/useCanUserSubmitProposal'; import { analyticsEvents } from '../../../insights/analyticsEvents'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; export function SafeHierarchyPage() { @@ -24,7 +24,7 @@ export function SafeHierarchyPage() { const HEADER_HEIGHT = useHeaderHeight(); const navigate = useNavigate(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { canUserCreateProposal } = useCanUserCreateProposal(); diff --git a/src/pages/dao/proposal-templates/SafeProposalTemplatesPage.tsx b/src/pages/dao/proposal-templates/SafeProposalTemplatesPage.tsx index 8349f7d7dd..2369541965 100644 --- a/src/pages/dao/proposal-templates/SafeProposalTemplatesPage.tsx +++ b/src/pages/dao/proposal-templates/SafeProposalTemplatesPage.tsx @@ -12,7 +12,7 @@ import { DAO_ROUTES } from '../../../constants/routes'; import { useCanUserCreateProposal } from '../../../hooks/utils/useCanUserSubmitProposal'; import { analyticsEvents } from '../../../insights/analyticsEvents'; import { useFractal } from '../../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; export function SafeProposalTemplatesPage() { @@ -26,7 +26,7 @@ export function SafeProposalTemplatesPage() { } = useFractal(); const { safe } = useDaoInfoStore(); const { canUserCreateProposal } = useCanUserCreateProposal(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const safeAddress = safe?.address; diff --git a/src/pages/dao/proposals/[proposalId]/index.tsx b/src/pages/dao/proposals/[proposalId]/index.tsx index 0dae6bebdc..b60e166931 100644 --- a/src/pages/dao/proposals/[proposalId]/index.tsx +++ b/src/pages/dao/proposals/[proposalId]/index.tsx @@ -14,7 +14,7 @@ import useSnapshotProposal from '../../../../hooks/DAO/loaders/snapshot/useSnaps import { useGetMetadata } from '../../../../hooks/DAO/proposal/useGetMetadata'; import { analyticsEvents } from '../../../../insights/analyticsEvents'; import { useFractal } from '../../../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore'; import { AzoriusProposal, SnapshotProposal } from '../../../../types'; @@ -29,7 +29,7 @@ export function SafeProposalDetailsPage() { governance: { proposals, loadingProposals, allProposalsLoaded, isAzorius }, } = useFractal(); const { safe } = useDaoInfoStore(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { proposalId } = useParams(); // Either the proposals have not even started loading yet, or not all proposals have been loaded, so this could be one of them diff --git a/src/pages/dao/proposals/new/sablier/SafeSablierProposalCreatePage.tsx b/src/pages/dao/proposals/new/sablier/SafeSablierProposalCreatePage.tsx index 704e107e1b..b986be6259 100644 --- a/src/pages/dao/proposals/new/sablier/SafeSablierProposalCreatePage.tsx +++ b/src/pages/dao/proposals/new/sablier/SafeSablierProposalCreatePage.tsx @@ -71,7 +71,7 @@ import useSubmitProposal from '../../../../../hooks/DAO/proposal/useSubmitPropos import { useCanUserCreateProposal } from '../../../../../hooks/utils/useCanUserSubmitProposal'; import { analyticsEvents } from '../../../../../insights/analyticsEvents'; import { useFractal } from '../../../../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../../store/daoInfo/useDaoInfoStore'; import { BigIntValuePair, CreateProposalSteps } from '../../../../../types'; import { scrollToBottom } from '../../../../../utils/ui'; @@ -763,7 +763,7 @@ export function SafeSablierProposalCreatePage() { const { addressPrefix, contracts: { sablierV2Batch, sablierV2LockupTranched }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const navigate = useNavigate(); const { t } = useTranslation(['proposalTemplate', 'proposal']); const [title, setTitle] = useState(''); diff --git a/src/pages/dao/roles/SafeRolesPage.tsx b/src/pages/dao/roles/SafeRolesPage.tsx index 8669e12282..9e59436852 100644 --- a/src/pages/dao/roles/SafeRolesPage.tsx +++ b/src/pages/dao/roles/SafeRolesPage.tsx @@ -13,7 +13,7 @@ import PageHeader from '../../../components/ui/page/Header/PageHeader'; import { DAO_ROUTES } from '../../../constants/routes'; import { useCanUserCreateProposal } from '../../../hooks/utils/useCanUserSubmitProposal'; import { analyticsEvents } from '../../../insights/analyticsEvents'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { useRolesStore } from '../../../store/roles/useRolesStore'; @@ -23,7 +23,7 @@ export function SafeRolesPage() { }, []); const { hatsTree } = useRolesStore(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { t } = useTranslation(['roles']); const { safe } = useDaoInfoStore(); const navigate = useNavigate(); diff --git a/src/pages/dao/roles/edit/SafeRolesEditPage.tsx b/src/pages/dao/roles/edit/SafeRolesEditPage.tsx index 31329f4989..8c6cd6b6d7 100644 --- a/src/pages/dao/roles/edit/SafeRolesEditPage.tsx +++ b/src/pages/dao/roles/edit/SafeRolesEditPage.tsx @@ -18,7 +18,7 @@ import { DAO_ROUTES } from '../../../../constants/routes'; import { getRandomBytes } from '../../../../helpers'; import { useNavigationBlocker } from '../../../../hooks/utils/useNavigationBlocker'; import { analyticsEvents } from '../../../../insights/analyticsEvents'; -import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore'; import { useRolesStore } from '../../../../store/roles/useRolesStore'; import { RoleFormValues } from '../../../../types/roles'; @@ -30,7 +30,7 @@ export function SafeRolesEditPage() { const { t } = useTranslation(['roles', 'navigation', 'modals', 'common']); const { safe } = useDaoInfoStore(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { values, setFieldValue } = useFormikContext(); diff --git a/src/pages/dao/roles/edit/details/SafeRoleEditDetailsPage.tsx b/src/pages/dao/roles/edit/details/SafeRoleEditDetailsPage.tsx index f909a30260..e4991c5edc 100644 --- a/src/pages/dao/roles/edit/details/SafeRoleEditDetailsPage.tsx +++ b/src/pages/dao/roles/edit/details/SafeRoleEditDetailsPage.tsx @@ -9,7 +9,7 @@ import { RoleFormTabs } from '../../../../../components/Roles/forms/RoleFormTabs import PageHeader from '../../../../../components/ui/page/Header/PageHeader'; import { DAO_ROUTES } from '../../../../../constants/routes'; import { useNavigationBlocker } from '../../../../../hooks/utils/useNavigationBlocker'; -import { useNetworkConfig } from '../../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../../store/daoInfo/useDaoInfoStore'; import { EditBadgeStatus, @@ -21,7 +21,7 @@ import { export function SafeRoleEditDetailsPage() { const { t } = useTranslation(['roles']); const { safe } = useDaoInfoStore(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const navigate = useNavigate(); const { values, setFieldValue, touched, setTouched } = useFormikContext(); const [searchParams] = useSearchParams(); diff --git a/src/pages/dao/roles/edit/summary/SafeRolesEditProposalSummaryPage.tsx b/src/pages/dao/roles/edit/summary/SafeRolesEditProposalSummaryPage.tsx index f3da8732d3..f215c4462e 100644 --- a/src/pages/dao/roles/edit/summary/SafeRolesEditProposalSummaryPage.tsx +++ b/src/pages/dao/roles/edit/summary/SafeRolesEditProposalSummaryPage.tsx @@ -7,7 +7,7 @@ import { RoleFormCreateProposal } from '../../../../../components/Roles/forms/Ro import PageHeader from '../../../../../components/ui/page/Header/PageHeader'; import { CONTENT_MAXW } from '../../../../../constants/common'; import { DAO_ROUTES } from '../../../../../constants/routes'; -import { useNetworkConfig } from '../../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../../store/daoInfo/useDaoInfoStore'; import { RoleFormValues } from '../../../../../types/roles'; @@ -15,7 +15,7 @@ export function SafeRolesEditProposalSummaryPage() { const navigate = useNavigate(); const { safe } = useDaoInfoStore(); const { t } = useTranslation(['roles', 'breadcrumbs']); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { values } = useFormikContext(); const safeAddress = safe?.address; diff --git a/src/pages/dao/settings/general/SafeGeneralSettingsPage.tsx b/src/pages/dao/settings/general/SafeGeneralSettingsPage.tsx index 8311e60aed..43938c0fe2 100644 --- a/src/pages/dao/settings/general/SafeGeneralSettingsPage.tsx +++ b/src/pages/dao/settings/general/SafeGeneralSettingsPage.tsx @@ -13,7 +13,7 @@ import { DAO_ROUTES } from '../../../../constants/routes'; import useSubmitProposal from '../../../../hooks/DAO/proposal/useSubmitProposal'; import { useCanUserCreateProposal } from '../../../../hooks/utils/useCanUserSubmitProposal'; import { createAccountSubstring } from '../../../../hooks/utils/useGetAccountName'; -import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore'; import { ProposalExecuteData } from '../../../../types'; import { validateENSName } from '../../../../utils/url'; @@ -31,7 +31,7 @@ export function SafeGeneralSettingsPage() { const { addressPrefix, contracts: { keyValuePairs }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const safeAddress = safe?.address; diff --git a/src/pages/dao/settings/governance/SafeGovernanceSettingsPage.tsx b/src/pages/dao/settings/governance/SafeGovernanceSettingsPage.tsx index 8e4bf85947..97a483aa16 100644 --- a/src/pages/dao/settings/governance/SafeGovernanceSettingsPage.tsx +++ b/src/pages/dao/settings/governance/SafeGovernanceSettingsPage.tsx @@ -9,13 +9,13 @@ import { SignersContainer } from '../../../../components/SafeSettings/Signers/Si import NestedPageHeader from '../../../../components/ui/page/Header/NestedPageHeader'; import { DAO_ROUTES } from '../../../../constants/routes'; import { useFractal } from '../../../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore'; import { GovernanceType } from '../../../../types'; export function SafeGovernanceSettingsPage() { const { t } = useTranslation('settings'); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { governance: { type }, } = useFractal(); diff --git a/src/pages/dao/settings/modules-and-guard/SafeModulesSettingsPage.tsx b/src/pages/dao/settings/modules-and-guard/SafeModulesSettingsPage.tsx index e4da7a80e0..f5e2af63a7 100644 --- a/src/pages/dao/settings/modules-and-guard/SafeModulesSettingsPage.tsx +++ b/src/pages/dao/settings/modules-and-guard/SafeModulesSettingsPage.tsx @@ -9,13 +9,13 @@ import Divider from '../../../../components/ui/utils/Divider'; import { DAO_ROUTES } from '../../../../constants/routes'; import { createAccountSubstring } from '../../../../hooks/utils/useGetAccountName'; import { useFractal } from '../../../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore'; import { FractalModuleType } from '../../../../types'; export function SafeModulesSettingsPage() { const { t } = useTranslation('settings'); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { guardContracts: { freezeGuardContractAddress, freezeVotingContractAddress }, } = useFractal(); diff --git a/src/pages/dao/settings/permissions/SafePermissionsCreateProposal.tsx b/src/pages/dao/settings/permissions/SafePermissionsCreateProposal.tsx index 96476a6d80..e541baf0c8 100644 --- a/src/pages/dao/settings/permissions/SafePermissionsCreateProposal.tsx +++ b/src/pages/dao/settings/permissions/SafePermissionsCreateProposal.tsx @@ -27,7 +27,7 @@ import { DAO_ROUTES } from '../../../../constants/routes'; import { getRandomBytes } from '../../../../helpers'; import { generateContractByteCodeLinear } from '../../../../models/helpers/utils'; import { useFractal } from '../../../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useProposalActionsStore } from '../../../../store/actions/useProposalActionsStore'; import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore'; import { @@ -47,7 +47,7 @@ export function SafePermissionsCreateProposal() { linearVotingErc721MasterCopy, zodiacModuleProxyFactory, }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const [searchParams] = useSearchParams(); const votingStrategyAddress = searchParams.get('votingStrategy'); const navigate = useNavigate(); diff --git a/src/pages/dao/settings/permissions/SafePermissionsSettingsPage.tsx b/src/pages/dao/settings/permissions/SafePermissionsSettingsPage.tsx index 0a0e828649..5edbb03270 100644 --- a/src/pages/dao/settings/permissions/SafePermissionsSettingsPage.tsx +++ b/src/pages/dao/settings/permissions/SafePermissionsSettingsPage.tsx @@ -16,14 +16,14 @@ import { NEUTRAL_2_84 } from '../../../../constants/common'; import { DAO_ROUTES } from '../../../../constants/routes'; import { useCanUserCreateProposal } from '../../../../hooks/utils/useCanUserSubmitProposal'; import { useFractal } from '../../../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore'; import { AzoriusGovernance } from '../../../../types'; export function SafePermissionsSettingsPage() { const { t } = useTranslation(['settings', 'common']); const navigate = useNavigate(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const { governance, governanceContracts: { isLoaded, linearVotingErc20Address }, diff --git a/src/pages/dao/treasury/SafeTreasuryPage.tsx b/src/pages/dao/treasury/SafeTreasuryPage.tsx index 5a97332519..bbad65aeb7 100644 --- a/src/pages/dao/treasury/SafeTreasuryPage.tsx +++ b/src/pages/dao/treasury/SafeTreasuryPage.tsx @@ -17,7 +17,7 @@ import { DAO_ROUTES } from '../../../constants/routes'; import { useCanUserCreateProposal } from '../../../hooks/utils/useCanUserSubmitProposal'; import { analyticsEvents } from '../../../insights/analyticsEvents'; import { useFractal } from '../../../providers/App/AppProvider'; -import { useNetworkConfig } from '../../../providers/NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useProposalActionsStore } from '../../../store/actions/useProposalActionsStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { ProposalActionType } from '../../../types'; @@ -41,7 +41,7 @@ export function SafeTreasuryPage() { const { isOpen, onOpen, onClose } = useDisclosure(); const { addAction } = useProposalActionsStore(); const navigate = useNavigate(); - const { addressPrefix } = useNetworkConfig(); + const { addressPrefix } = useNetworkConfigStore(); const hasAnyBalanceOfAnyFungibleTokens = assetsFungible.reduce((p, c) => p + BigInt(c.balance), 0n) > 0n; diff --git a/src/pages/home/SafeDisplayRow.tsx b/src/pages/home/SafeDisplayRow.tsx index acf4516e4b..b3e1289a05 100644 --- a/src/pages/home/SafeDisplayRow.tsx +++ b/src/pages/home/SafeDisplayRow.tsx @@ -3,13 +3,11 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { Address } from 'viem'; -import { useSwitchChain } from 'wagmi'; import Avatar from '../../components/ui/page/Header/Avatar'; import { DAO_ROUTES } from '../../constants/routes'; import useAvatar from '../../hooks/utils/useAvatar'; import { createAccountSubstring } from '../../hooks/utils/useGetAccountName'; import { useGetSafeName } from '../../hooks/utils/useGetSafeName'; -import { useNetworkConfig } from '../../providers/NetworkConfig/NetworkConfigProvider'; import { getChainIdFromPrefix, getChainName, getNetworkIcon } from '../../utils/url'; interface SafeDisplayRowProps { @@ -27,17 +25,8 @@ export function SafeDisplayRow({ showAddress, name, }: SafeDisplayRowProps) { - const { addressPrefix } = useNetworkConfig(); const navigate = useNavigate(); - const { switchChain } = useSwitchChain({ - mutation: { - onSuccess: () => { - navigate(DAO_ROUTES.dao.relative(network, address)); - }, - }, - }); - const { getSafeName } = useGetSafeName(getChainIdFromPrefix(network)); const [safeName, setSafeName] = useState(name); @@ -54,11 +43,7 @@ export function SafeDisplayRow({ const onClickNav = () => { if (onClick) onClick(); - if (addressPrefix !== network) { - switchChain({ chainId: getChainIdFromPrefix(network) }); - } else { - navigate(DAO_ROUTES.dao.relative(network, address)); - } + navigate(DAO_ROUTES.dao.relative(network, address)); }; const nameColor = showAddress ? 'neutral-7' : 'white-0'; diff --git a/src/providers/App/hooks/useBalancesAPI.ts b/src/providers/App/hooks/useBalancesAPI.ts index e347874403..1b5220cdbe 100644 --- a/src/providers/App/hooks/useBalancesAPI.ts +++ b/src/providers/App/hooks/useBalancesAPI.ts @@ -2,13 +2,13 @@ import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Address } from 'viem'; import { DefiBalance, NFTBalance, TokenBalance } from '../../../types'; -import { useNetworkConfig } from '../../NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../NetworkConfig/useNetworkConfigStore'; export default function useBalancesAPI() { const { chain, moralis: { deFiSupported }, - } = useNetworkConfig(); + } = useNetworkConfigStore(); const { t } = useTranslation('treasury'); const getTokenBalances = useCallback( diff --git a/src/providers/App/hooks/useSafeAPI.ts b/src/providers/App/hooks/useSafeAPI.ts index 014e08dcaa..3559387c39 100644 --- a/src/providers/App/hooks/useSafeAPI.ts +++ b/src/providers/App/hooks/useSafeAPI.ts @@ -15,7 +15,7 @@ import { setIndexedDBValue, } from '../../../hooks/utils/cache/useLocalDB'; import { SafeWithNextNonce } from '../../../types'; -import { useNetworkConfig } from '../../NetworkConfig/NetworkConfigProvider'; +import { useNetworkConfigStore } from '../../NetworkConfig/useNetworkConfigStore'; class EnhancedSafeApiKit extends SafeApiKit { readonly CHAINID: number; @@ -103,7 +103,7 @@ class EnhancedSafeApiKit extends SafeApiKit { } export function useSafeAPI() { - const { chain } = useNetworkConfig(); + const { chain } = useNetworkConfigStore(); const safeAPI = useMemo(() => { return new EnhancedSafeApiKit({ chainId: BigInt(chain.id) }); diff --git a/src/providers/NetworkConfig/NetworkConfigProvider.tsx b/src/providers/NetworkConfig/NetworkConfigProvider.tsx deleted file mode 100644 index 73d0de8817..0000000000 --- a/src/providers/NetworkConfig/NetworkConfigProvider.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { createContext, ReactNode, useContext, useEffect, useState } from 'react'; -import { useChainId } from 'wagmi'; -import { NetworkConfig } from '../../types/network'; - -import { networks } from './networks'; - -type NetworkConfigContextType = { - currentConfig: NetworkConfig; - getConfigByChainId: (chainId: number) => NetworkConfig; -}; - -export const NetworkConfigContext = createContext( - {} as NetworkConfigContextType, -); - -export const useNetworkConfig = (chainId?: number) => { - const context = useContext(NetworkConfigContext); - if (chainId) { - return context.getConfigByChainId(chainId); - } - return context.currentConfig; -}; - -export const supportedNetworks = Object.values(networks).sort((a, b) => a.order - b.order); -export const moralisSupportedChainIds = supportedNetworks - .filter(network => network.moralis.chainSupported) - .map(network => network.chain.id); - -export const getNetworkConfig = (chainId: number) => { - const foundChain = supportedNetworks.find(network => network.chain.id === chainId); - if (foundChain) { - return foundChain; - } else { - throw new Error(`Can't get network config for chain ${chainId}`); - } -}; - -export function NetworkConfigProvider({ children }: { children: ReactNode }) { - const chainId = useChainId(); - const [currentConfig, setCurrentConfig] = useState(getNetworkConfig(chainId)); - - useEffect(() => { - setCurrentConfig(getNetworkConfig(chainId)); - }, [chainId]); - - const contextValue: NetworkConfigContextType = { - currentConfig, - getConfigByChainId: getNetworkConfig, - }; - - return ( - {children} - ); -} diff --git a/src/providers/NetworkConfig/useNetworkConfigStore.ts b/src/providers/NetworkConfig/useNetworkConfigStore.ts new file mode 100644 index 0000000000..46d4cb2249 --- /dev/null +++ b/src/providers/NetworkConfig/useNetworkConfigStore.ts @@ -0,0 +1,31 @@ +import { create } from 'zustand'; +import { NetworkConfig } from '../../types/network'; +import { networks } from './networks'; +import { mainnetConfig } from './networks/mainnet'; + +interface NetworkConfigStore extends NetworkConfig { + getConfigByChainId: (chainId?: number) => NetworkConfig; + setCurrentConfig: (config: NetworkConfig) => void; +} + +export const supportedNetworks = Object.values(networks).sort((a, b) => a.order - b.order); + +export const moralisSupportedChainIds = supportedNetworks + .filter(network => network.moralis.chainSupported) + .map(network => network.chain.id); + +export const getNetworkConfig = (chainId?: number): NetworkConfig => { + const foundChain = supportedNetworks.find(network => network.chain.id === chainId); + if (foundChain) { + return foundChain; + } else { + throw new Error(`Can't get network config for chain ${chainId}`); + } +}; + +// Create the Zustand store +export const useNetworkConfigStore = create(set => ({ + ...mainnetConfig, + getConfigByChainId: getNetworkConfig, + setCurrentConfig: (config: NetworkConfig) => set({ ...config }), +})); diff --git a/src/providers/NetworkConfig/web3-modal.config.ts b/src/providers/NetworkConfig/web3-modal.config.ts index bc155fff64..2d9e4e2d61 100644 --- a/src/providers/NetworkConfig/web3-modal.config.ts +++ b/src/providers/NetworkConfig/web3-modal.config.ts @@ -5,14 +5,14 @@ import { HttpTransport } from 'viem'; import { http } from 'wagmi'; import { Chain } from 'wagmi/chains'; import { NetworkConfig } from '../../types/network'; -import { supportedNetworks } from './NetworkConfigProvider'; +import { supportedNetworks } from './useNetworkConfigStore'; const supportedWagmiChains = supportedNetworks.map(network => network.chain); export const walletConnectProjectId = import.meta.env.VITE_APP_WALLET_CONNECT_PROJECT_ID; export const queryClient = new QueryClient(); -const wagmiMetadata = { +const metadata = { name: import.meta.env.VITE_APP_NAME, description: 'Are you outgrowing your Multisig? Decent extends Safe treasuries into on-chain hierarchies of permissions, token flows, and governance.', @@ -20,7 +20,10 @@ const wagmiMetadata = { icons: [`${import.meta.env.VITE_APP_SITE_URL}/favicon-96x96.png`], }; -const transportsReducer = (accumulator: Record, network: NetworkConfig) => { +export const transportsReducer = ( + accumulator: Record, + network: NetworkConfig, +) => { accumulator[network.chain.id] = http(network.rpcEndpoint); return accumulator; }; @@ -28,7 +31,7 @@ const transportsReducer = (accumulator: Record, network: export const wagmiConfig = defaultWagmiConfig({ chains: supportedWagmiChains as [Chain, ...Chain[]], projectId: walletConnectProjectId, - metadata: wagmiMetadata, + metadata, transports: supportedNetworks.reduce(transportsReducer, {}), batch: { multicall: true, @@ -36,5 +39,5 @@ export const wagmiConfig = defaultWagmiConfig({ }); if (walletConnectProjectId) { - createWeb3Modal({ wagmiConfig, projectId: walletConnectProjectId }); + createWeb3Modal({ wagmiConfig, projectId: walletConnectProjectId, metadata: metadata }); } diff --git a/src/providers/Providers.tsx b/src/providers/Providers.tsx index ff12e02c54..dfb14b094d 100644 --- a/src/providers/Providers.tsx +++ b/src/providers/Providers.tsx @@ -9,7 +9,6 @@ import { ErrorBoundary } from '../components/ui/utils/ErrorBoundary'; import { TopErrorFallback } from '../components/ui/utils/TopErrorFallback'; import graphQLClient from '../graphql'; import { AppProvider } from './App/AppProvider'; -import { NetworkConfigProvider } from './NetworkConfig/NetworkConfigProvider'; import { queryClient, wagmiConfig } from './NetworkConfig/web3-modal.config'; export default function Providers({ children }: { children: ReactNode }) { @@ -25,19 +24,17 @@ export default function Providers({ children }: { children: ReactNode }) { - - - - {children} - - + + + {children} + diff --git a/src/utils/url.ts b/src/utils/url.ts index ab42df238e..9edc366177 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -1,5 +1,5 @@ import { normalize } from 'viem/ens'; -import { supportedNetworks } from '../providers/NetworkConfig/NetworkConfigProvider'; +import { supportedNetworks } from '../providers/NetworkConfig/useNetworkConfigStore'; export const isValidUrl = (urlString: string) => { try { const url = new URL(urlString);