From 56b0eda552b42a39bb1afc1996e2d8837e2e324d Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:36:15 -0500 Subject: [PATCH 01/14] feat: implement dynamic Wagmi configuration and update providers to use it --- src/components/ui/page/Global/index.tsx | 6 ++--- .../NetworkConfig/useDynamicWagmiConfig.ts | 27 +++++++++++++++++++ .../NetworkConfig/web3-modal.config.ts | 27 ++++--------------- src/providers/Providers.tsx | 6 +++-- 4 files changed, 39 insertions(+), 27 deletions(-) create mode 100644 src/providers/NetworkConfig/useDynamicWagmiConfig.ts diff --git a/src/components/ui/page/Global/index.tsx b/src/components/ui/page/Global/index.tsx index 3a2e3df2e..65e226c9a 100644 --- a/src/components/ui/page/Global/index.tsx +++ b/src/components/ui/page/Global/index.tsx @@ -11,17 +11,16 @@ import { } from '../../../../hooks/utils/cache/cacheDefaults'; import { setValue } from '../../../../hooks/utils/cache/useLocalStorage'; import { getSafeName } from '../../../../hooks/utils/useGetSafeName'; +import { useDynamicWagmiConfig } from '../../../../providers/NetworkConfig/useDynamicWagmiConfig'; import { getNetworkConfig, supportedNetworks, } 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) { @@ -34,6 +33,7 @@ const useUserTracking = () => { const useUpdateFavoritesCache = (onFavoritesUpdated: () => void) => { const { favoritesList } = useAccountFavorites(); + const wagmiConfig = useDynamicWagmiConfig(); useEffect(() => { (async () => { @@ -102,7 +102,7 @@ const useUpdateFavoritesCache = (onFavoritesUpdated: () => void) => { onFavoritesUpdated(); } })(); - }, [favoritesList, onFavoritesUpdated]); + }, [favoritesList, onFavoritesUpdated, wagmiConfig.chains]); }; export function Global() { diff --git a/src/providers/NetworkConfig/useDynamicWagmiConfig.ts b/src/providers/NetworkConfig/useDynamicWagmiConfig.ts new file mode 100644 index 000000000..72a73c05f --- /dev/null +++ b/src/providers/NetworkConfig/useDynamicWagmiConfig.ts @@ -0,0 +1,27 @@ +import { createWeb3Modal } from '@web3modal/wagmi/react'; +import { defaultWagmiConfig } from '@web3modal/wagmi/react/config'; +import { supportedNetworks, useNetworkConfigStore } from './useNetworkConfigStore'; +import { transportsReducer, wagmiMetadata, walletConnectProjectId } from './web3-modal.config'; + +// Dynamic Wagmi Config Hook +export const useDynamicWagmiConfig = () => { + const { chain } = useNetworkConfigStore(); + + const wagmiConfig = defaultWagmiConfig({ + chains: [chain], + projectId: walletConnectProjectId, + metadata: wagmiMetadata, + transports: supportedNetworks.reduce(transportsReducer, {}), + batch: { + multicall: true, + }, + }); + + // Initialize Web3Modal only once + if (walletConnectProjectId) { + createWeb3Modal({ wagmiConfig, projectId: walletConnectProjectId }); + } + + // Return the dynamic Wagmi client + return wagmiConfig; +}; diff --git a/src/providers/NetworkConfig/web3-modal.config.ts b/src/providers/NetworkConfig/web3-modal.config.ts index f721c48b1..1fed495d7 100644 --- a/src/providers/NetworkConfig/web3-modal.config.ts +++ b/src/providers/NetworkConfig/web3-modal.config.ts @@ -1,18 +1,12 @@ import { QueryClient } from '@tanstack/react-query'; -import { createWeb3Modal } from '@web3modal/wagmi/react'; -import { defaultWagmiConfig } from '@web3modal/wagmi/react/config'; import { HttpTransport } from 'viem'; import { http } from 'wagmi'; -import { Chain } from 'wagmi/chains'; import { NetworkConfig } from '../../types/network'; -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 = { +export const wagmiMetadata = { 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,21 +14,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; }; - -export const wagmiConfig = defaultWagmiConfig({ - chains: supportedWagmiChains as [Chain, ...Chain[]], - projectId: walletConnectProjectId, - metadata: wagmiMetadata, - transports: supportedNetworks.reduce(transportsReducer, {}), - batch: { - multicall: true, - }, -}); - -if (walletConnectProjectId) { - createWeb3Modal({ wagmiConfig, projectId: walletConnectProjectId }); -} diff --git a/src/providers/Providers.tsx b/src/providers/Providers.tsx index dfb14b094..7568b4d33 100644 --- a/src/providers/Providers.tsx +++ b/src/providers/Providers.tsx @@ -9,9 +9,11 @@ import { ErrorBoundary } from '../components/ui/utils/ErrorBoundary'; import { TopErrorFallback } from '../components/ui/utils/TopErrorFallback'; import graphQLClient from '../graphql'; import { AppProvider } from './App/AppProvider'; -import { queryClient, wagmiConfig } from './NetworkConfig/web3-modal.config'; +import { useDynamicWagmiConfig } from './NetworkConfig/useDynamicWagmiConfig'; +import { queryClient } from './NetworkConfig/web3-modal.config'; export default function Providers({ children }: { children: ReactNode }) { + const currentConfig = useDynamicWagmiConfig(); return ( - + Date: Sat, 7 Dec 2024 17:32:43 -0500 Subject: [PATCH 02/14] refactor: remove unused dynamic Wagmi configuration hook --- src/components/ui/page/Global/index.tsx | 5 ++-- .../NetworkConfig/useDynamicWagmiConfig.ts | 27 ------------------- src/providers/Providers.tsx | 6 ++--- 3 files changed, 4 insertions(+), 34 deletions(-) delete mode 100644 src/providers/NetworkConfig/useDynamicWagmiConfig.ts diff --git a/src/components/ui/page/Global/index.tsx b/src/components/ui/page/Global/index.tsx index 65e226c9a..21e1be827 100644 --- a/src/components/ui/page/Global/index.tsx +++ b/src/components/ui/page/Global/index.tsx @@ -11,11 +11,11 @@ import { } from '../../../../hooks/utils/cache/cacheDefaults'; import { setValue } from '../../../../hooks/utils/cache/useLocalStorage'; import { getSafeName } from '../../../../hooks/utils/useGetSafeName'; -import { useDynamicWagmiConfig } from '../../../../providers/NetworkConfig/useDynamicWagmiConfig'; import { getNetworkConfig, supportedNetworks, } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; +import { wagmiConfig } from '../../../../providers/NetworkConfig/web3-modal.config'; import { getChainIdFromPrefix } from '../../../../utils/url'; import { Layout } from '../Layout'; @@ -33,7 +33,6 @@ const useUserTracking = () => { const useUpdateFavoritesCache = (onFavoritesUpdated: () => void) => { const { favoritesList } = useAccountFavorites(); - const wagmiConfig = useDynamicWagmiConfig(); useEffect(() => { (async () => { @@ -102,7 +101,7 @@ const useUpdateFavoritesCache = (onFavoritesUpdated: () => void) => { onFavoritesUpdated(); } })(); - }, [favoritesList, onFavoritesUpdated, wagmiConfig.chains]); + }, [favoritesList, onFavoritesUpdated]); }; export function Global() { diff --git a/src/providers/NetworkConfig/useDynamicWagmiConfig.ts b/src/providers/NetworkConfig/useDynamicWagmiConfig.ts deleted file mode 100644 index 72a73c05f..000000000 --- a/src/providers/NetworkConfig/useDynamicWagmiConfig.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createWeb3Modal } from '@web3modal/wagmi/react'; -import { defaultWagmiConfig } from '@web3modal/wagmi/react/config'; -import { supportedNetworks, useNetworkConfigStore } from './useNetworkConfigStore'; -import { transportsReducer, wagmiMetadata, walletConnectProjectId } from './web3-modal.config'; - -// Dynamic Wagmi Config Hook -export const useDynamicWagmiConfig = () => { - const { chain } = useNetworkConfigStore(); - - const wagmiConfig = defaultWagmiConfig({ - chains: [chain], - projectId: walletConnectProjectId, - metadata: wagmiMetadata, - transports: supportedNetworks.reduce(transportsReducer, {}), - batch: { - multicall: true, - }, - }); - - // Initialize Web3Modal only once - if (walletConnectProjectId) { - createWeb3Modal({ wagmiConfig, projectId: walletConnectProjectId }); - } - - // Return the dynamic Wagmi client - return wagmiConfig; -}; diff --git a/src/providers/Providers.tsx b/src/providers/Providers.tsx index 7568b4d33..dfb14b094 100644 --- a/src/providers/Providers.tsx +++ b/src/providers/Providers.tsx @@ -9,11 +9,9 @@ import { ErrorBoundary } from '../components/ui/utils/ErrorBoundary'; import { TopErrorFallback } from '../components/ui/utils/TopErrorFallback'; import graphQLClient from '../graphql'; import { AppProvider } from './App/AppProvider'; -import { useDynamicWagmiConfig } from './NetworkConfig/useDynamicWagmiConfig'; -import { queryClient } from './NetworkConfig/web3-modal.config'; +import { queryClient, wagmiConfig } from './NetworkConfig/web3-modal.config'; export default function Providers({ children }: { children: ReactNode }) { - const currentConfig = useDynamicWagmiConfig(); return ( - + Date: Sat, 7 Dec 2024 17:33:31 -0500 Subject: [PATCH 03/14] feat: integrate automatic chain switching and update Wagmi configuration for network support --- src/hooks/utils/useAutomaticSwitchChain.ts | 19 ++++++++++++++-- .../NetworkConfig/web3-modal.config.ts | 22 ++++++++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/hooks/utils/useAutomaticSwitchChain.ts b/src/hooks/utils/useAutomaticSwitchChain.ts index f226cda09..2459cc18e 100644 --- a/src/hooks/utils/useAutomaticSwitchChain.ts +++ b/src/hooks/utils/useAutomaticSwitchChain.ts @@ -1,4 +1,5 @@ import { useEffect } from 'react'; +import { useSwitchChain } from 'wagmi'; import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { getChainIdFromPrefix } from '../../utils/url'; @@ -8,11 +9,25 @@ export const useAutomaticSwitchChain = ({ urlAddressPrefix: string | undefined; }) => { const { setCurrentConfig, getConfigByChainId, addressPrefix } = useNetworkConfigStore(); + const { switchChain } = useSwitchChain({ + mutation: { + onError: () => { + if (addressPrefix !== urlAddressPrefix && urlAddressPrefix !== undefined) { + const chainId = getChainIdFromPrefix(urlAddressPrefix); + switchChain({ chainId }); + } + }, + }, + }); useEffect(() => { if (urlAddressPrefix === undefined || addressPrefix === urlAddressPrefix) { return; } - setCurrentConfig(getConfigByChainId(getChainIdFromPrefix(urlAddressPrefix))); - }, [addressPrefix, setCurrentConfig, getConfigByChainId, urlAddressPrefix]); + const chainId = getChainIdFromPrefix(urlAddressPrefix); + setCurrentConfig(getConfigByChainId(chainId)); + if (addressPrefix !== urlAddressPrefix && urlAddressPrefix !== undefined) { + switchChain({ chainId }); + } + }, [addressPrefix, setCurrentConfig, getConfigByChainId, urlAddressPrefix, switchChain]); }; diff --git a/src/providers/NetworkConfig/web3-modal.config.ts b/src/providers/NetworkConfig/web3-modal.config.ts index 1fed495d7..2d9e4e2d6 100644 --- a/src/providers/NetworkConfig/web3-modal.config.ts +++ b/src/providers/NetworkConfig/web3-modal.config.ts @@ -1,12 +1,18 @@ import { QueryClient } from '@tanstack/react-query'; +import { createWeb3Modal } from '@web3modal/wagmi/react'; +import { defaultWagmiConfig } from '@web3modal/wagmi/react/config'; import { HttpTransport } from 'viem'; import { http } from 'wagmi'; +import { Chain } from 'wagmi/chains'; import { NetworkConfig } from '../../types/network'; +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(); -export 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.', @@ -21,3 +27,17 @@ export const transportsReducer = ( accumulator[network.chain.id] = http(network.rpcEndpoint); return accumulator; }; + +export const wagmiConfig = defaultWagmiConfig({ + chains: supportedWagmiChains as [Chain, ...Chain[]], + projectId: walletConnectProjectId, + metadata, + transports: supportedNetworks.reduce(transportsReducer, {}), + batch: { + multicall: true, + }, +}); + +if (walletConnectProjectId) { + createWeb3Modal({ wagmiConfig, projectId: walletConnectProjectId, metadata: metadata }); +} From fb9c75139e52f5c86836ea969f0a1191ef60fd2c Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:53:06 -0500 Subject: [PATCH 04/14] feat: integrate chain switching functionality in EstablishEssentials component --- .../formComponents/EstablishEssentials.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/components/DaoCreator/formComponents/EstablishEssentials.tsx b/src/components/DaoCreator/formComponents/EstablishEssentials.tsx index 1db69f86e..aa917e26c 100644 --- a/src/components/DaoCreator/formComponents/EstablishEssentials.tsx +++ b/src/components/DaoCreator/formComponents/EstablishEssentials.tsx @@ -3,6 +3,7 @@ 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 { supportedNetworks, @@ -10,7 +11,7 @@ import { } from '../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../store/daoInfo/useDaoInfoStore'; import { GovernanceType, ICreationStepProps, VotingStrategyType } from '../../../types'; -import { getNetworkIcon } from '../../../utils/url'; +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'; @@ -71,7 +72,9 @@ export function EstablishEssentials(props: ICreationStepProps) { setFieldValue('essentials.governance', value); }; - const { createOptions, setCurrentConfig, chain, getConfigByChainId } = useNetworkConfigStore(); + const { createOptions, setCurrentConfig, chain, getConfigByChainId, addressPrefix } = + useNetworkConfigStore(); + const walletChainID = useChainId(); const [snapshotENSInput, setSnapshotENSInput] = useState(''); @@ -94,6 +97,17 @@ export function EstablishEssentials(props: ICreationStepProps) { selected: chain.id === network.chain.id, })); + const { switchChain } = useSwitchChain({ + mutation: { + onError: () => { + if (chain.id !== walletChainID) { + const chainId = getChainIdFromPrefix(addressPrefix); + switchChain({ chainId }); + } + }, + }, + }); + return ( <> Date: Thu, 12 Dec 2024 00:50:03 -0500 Subject: [PATCH 05/14] Delay setting current config after switching chain to ensure proper state update --- src/hooks/utils/useAutomaticSwitchChain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/utils/useAutomaticSwitchChain.ts b/src/hooks/utils/useAutomaticSwitchChain.ts index 2459cc18e..ec7cd394a 100644 --- a/src/hooks/utils/useAutomaticSwitchChain.ts +++ b/src/hooks/utils/useAutomaticSwitchChain.ts @@ -25,9 +25,9 @@ export const useAutomaticSwitchChain = ({ return; } const chainId = getChainIdFromPrefix(urlAddressPrefix); - setCurrentConfig(getConfigByChainId(chainId)); if (addressPrefix !== urlAddressPrefix && urlAddressPrefix !== undefined) { switchChain({ chainId }); } + setTimeout(() => setCurrentConfig(getConfigByChainId(chainId)), 300); }, [addressPrefix, setCurrentConfig, getConfigByChainId, urlAddressPrefix, switchChain]); }; From 113d28f907169d4e4e54cd75bfbec08a919821fe Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Fri, 13 Dec 2024 00:11:17 -0500 Subject: [PATCH 06/14] Update error message for invalid DAO search to reflect support for all chains --- src/i18n/locales/en/dashboard.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/locales/en/dashboard.json b/src/i18n/locales/en/dashboard.json index 0c2d403a4..567b53e56 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", From d2319e573f360248b283f9edc8df393916c3075d Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Fri, 13 Dec 2024 00:11:29 -0500 Subject: [PATCH 07/14] Enhance useIsSafe hook to search across all supported networks and return found network prefixes --- src/hooks/DAO/useSearchDao.ts | 9 ++++---- src/hooks/safe/useIsSafe.ts | 39 ++++++++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/hooks/DAO/useSearchDao.ts b/src/hooks/DAO/useSearchDao.ts index 74a42a57d..c80ba5d12 100644 --- a/src/hooks/DAO/useSearchDao.ts +++ b/src/hooks/DAO/useSearchDao.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; import { useIsSafe } from '../safe/useIsSafe'; @@ -7,9 +7,10 @@ import useAddress from '../utils/useAddress'; export const useSearchDao = () => { const [searchString, setSearchString] = useState(''); const [errorMessage, setErrorMessage] = useState(); + // This hook needs to search all supoorted chains for the address const { address, isValid, isLoading: isAddressLoading } = useAddress(searchString); - const { isSafe, isSafeLoading } = useIsSafe(address); + const { isSafe, isSafeLoading, safeFoundNetworkPrefixes } = useIsSafe(address); const { t } = useTranslation('dashboard'); const { chain } = useNetworkConfigStore(); @@ -21,15 +22,15 @@ export const useSearchDao = () => { if (searchString === '' || isLoading || isSafe || isValid === undefined) { return; } - if (isValid === true) { - setErrorMessage(t('errorFailedSearch', { chain: chain.name })); + setErrorMessage(t('errorFailedSearch')); } else { setErrorMessage(t('errorInvalidSearch')); } }, [chain.name, isLoading, isSafe, isValid, searchString, t]); return { + safeFoundNetworkPrefixes, errorMessage, isLoading, address, diff --git a/src/hooks/safe/useIsSafe.ts b/src/hooks/safe/useIsSafe.ts index a5a3bdf81..c03704bab 100644 --- a/src/hooks/safe/useIsSafe.ts +++ b/src/hooks/safe/useIsSafe.ts @@ -1,6 +1,7 @@ -import { useEffect, useState } from 'react'; +import SafeApiKit from '@safe-global/api-kit'; +import { useCallback, useEffect, useState } from 'react'; import { isAddress } from 'viem'; -import { useSafeAPI } from '../../providers/App/hooks/useSafeAPI'; +import { supportedNetworks } from '../../providers/NetworkConfig/useNetworkConfigStore'; /** * A hook which determines whether the provided Ethereum address is a Safe @@ -15,25 +16,43 @@ import { useSafeAPI } from '../../providers/App/hooks/useSafeAPI'; */ export const useIsSafe = (address: string | undefined) => { const [isSafeLoading, setSafeLoading] = useState(false); - const [isSafe, setIsSafe] = useState(); - const safeAPI = useSafeAPI(); + const [isSafe, setIsSafe] = useState(undefined); + const [safeFoundNetworkPrefixes, setNetworkPrefixes] = useState([]); + + const findSafes = useCallback(async (_address: string) => { + const networkPrefixes = []; // address prefixes + for await (const network of supportedNetworks) { + const safeAPI = new SafeApiKit({ chainId: BigInt(network.chain.id) }); + safeAPI.getSafeCreationInfo(_address); + try { + await safeAPI.getSafeCreationInfo(_address); + networkPrefixes.push(network.addressPrefix); + } catch (e) { + // Safe not found + continue; + } + } + return [networkPrefixes, networkPrefixes.length > 0] as const; // [networks, isSafe] + }, []); useEffect(() => { setSafeLoading(true); setIsSafe(undefined); - if (!address || !isAddress(address) || !safeAPI) { + if (!address || !isAddress(address)) { setIsSafe(false); setSafeLoading(false); return; } - safeAPI - .getSafeCreationInfo(address) - .then(() => setIsSafe(true)) + findSafes(address) + .then(([_safeFoundNetworkPrefixes, _isSafe]) => { + setNetworkPrefixes(_safeFoundNetworkPrefixes); + setIsSafe(_isSafe); + }) .catch(() => setIsSafe(false)) .finally(() => setSafeLoading(false)); - }, [address, safeAPI]); + }, [address, findSafes]); - return { isSafe, isSafeLoading }; + return { isSafe, isSafeLoading, safeFoundNetworkPrefixes }; }; From 2bb83c175d133e66be5e80d160a8cfba8e5f5873 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Fri, 13 Dec 2024 00:11:33 -0500 Subject: [PATCH 08/14] Refactor DAOSearch to display multiple network prefixes in SearchDisplay component --- .../ui/menus/DAOSearch/SearchDisplay.tsx | 13 +++++++--- src/components/ui/menus/DAOSearch/index.tsx | 25 +++++++++++++------ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/components/ui/menus/DAOSearch/SearchDisplay.tsx b/src/components/ui/menus/DAOSearch/SearchDisplay.tsx index aa1a7ac85..3e02346a5 100644 --- a/src/components/ui/menus/DAOSearch/SearchDisplay.tsx +++ b/src/components/ui/menus/DAOSearch/SearchDisplay.tsx @@ -4,7 +4,6 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Address } from 'viem'; import { SafeDisplayRow } from '../../../../pages/home/SafeDisplayRow'; -import { useNetworkConfigStore } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore'; import { ErrorBoundary } from '../../utils/ErrorBoundary'; import { MySafesErrorFallback } from '../../utils/MySafesErrorFallback'; @@ -14,12 +13,18 @@ interface ISearchDisplay { errorMessage: string | undefined; address: Address | undefined; onClickView: Function; + networkPrefix: string; } -export function SearchDisplay({ loading, errorMessage, address, onClickView }: ISearchDisplay) { +export function SearchDisplay({ + loading, + errorMessage, + address, + onClickView, + networkPrefix, +}: ISearchDisplay) { const { t } = useTranslation(['common', 'dashboard']); const node = useDaoInfoStore(); - const { addressPrefix } = useNetworkConfigStore(); const isCurrentSafe = useMemo( () => !!node && !!node?.safe?.address && node.safe.address === address, @@ -85,7 +90,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 a1a2409f0..ab4e77868 100644 --- a/src/components/ui/menus/DAOSearch/index.tsx +++ b/src/components/ui/menus/DAOSearch/index.tsx @@ -8,6 +8,7 @@ import { useDisclosure, useOutsideClick, Portal, + Flex, } from '@chakra-ui/react'; import debounce from 'lodash.debounce'; import { useEffect, useMemo, useRef, useState } from 'react'; @@ -21,7 +22,8 @@ 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, address, setSearchString, safeFoundNetworkPrefixes } = + useSearchDao(); const { isOpen, onOpen, onClose } = useDisclosure(); const ref = useRef(null); @@ -149,12 +151,21 @@ export function DAOSearch() { w="full" position="absolute" > - + {safeFoundNetworkPrefixes.map(networkPrefix => ( + + + + ))} From 8dab85b4a9e11ec28b0979dd14e68a3be895d286 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Fri, 13 Dec 2024 00:27:10 -0500 Subject: [PATCH 09/14] Refactor DAOSearch component to remove extra gap --- src/components/ui/menus/DAOSearch/index.tsx | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/components/ui/menus/DAOSearch/index.tsx b/src/components/ui/menus/DAOSearch/index.tsx index ab4e77868..9c1112391 100644 --- a/src/components/ui/menus/DAOSearch/index.tsx +++ b/src/components/ui/menus/DAOSearch/index.tsx @@ -8,7 +8,6 @@ import { useDisclosure, useOutsideClick, Portal, - Flex, } from '@chakra-ui/react'; import debounce from 'lodash.debounce'; import { useEffect, useMemo, useRef, useState } from 'react'; @@ -152,19 +151,14 @@ export function DAOSearch() { position="absolute" > {safeFoundNetworkPrefixes.map(networkPrefix => ( - - - + loading={isLoading} + errorMessage={errorMessage} + address={address} + networkPrefix={networkPrefix} + onClickView={resetSearch} + /> ))} From ef7a3de210b99ceb74919e59d22e9513fdcf2f76 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:53:14 -0500 Subject: [PATCH 10/14] revert: changes made to hook. --- src/hooks/safe/useIsSafe.ts | 39 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/src/hooks/safe/useIsSafe.ts b/src/hooks/safe/useIsSafe.ts index c03704bab..a5a3bdf81 100644 --- a/src/hooks/safe/useIsSafe.ts +++ b/src/hooks/safe/useIsSafe.ts @@ -1,7 +1,6 @@ -import SafeApiKit from '@safe-global/api-kit'; -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { isAddress } from 'viem'; -import { supportedNetworks } from '../../providers/NetworkConfig/useNetworkConfigStore'; +import { useSafeAPI } from '../../providers/App/hooks/useSafeAPI'; /** * A hook which determines whether the provided Ethereum address is a Safe @@ -16,43 +15,25 @@ import { supportedNetworks } from '../../providers/NetworkConfig/useNetworkConfi */ export const useIsSafe = (address: string | undefined) => { const [isSafeLoading, setSafeLoading] = useState(false); - const [isSafe, setIsSafe] = useState(undefined); - const [safeFoundNetworkPrefixes, setNetworkPrefixes] = useState([]); - - const findSafes = useCallback(async (_address: string) => { - const networkPrefixes = []; // address prefixes - for await (const network of supportedNetworks) { - const safeAPI = new SafeApiKit({ chainId: BigInt(network.chain.id) }); - safeAPI.getSafeCreationInfo(_address); - try { - await safeAPI.getSafeCreationInfo(_address); - networkPrefixes.push(network.addressPrefix); - } catch (e) { - // Safe not found - continue; - } - } - return [networkPrefixes, networkPrefixes.length > 0] as const; // [networks, isSafe] - }, []); + const [isSafe, setIsSafe] = useState(); + const safeAPI = useSafeAPI(); useEffect(() => { setSafeLoading(true); setIsSafe(undefined); - if (!address || !isAddress(address)) { + if (!address || !isAddress(address) || !safeAPI) { setIsSafe(false); setSafeLoading(false); return; } - findSafes(address) - .then(([_safeFoundNetworkPrefixes, _isSafe]) => { - setNetworkPrefixes(_safeFoundNetworkPrefixes); - setIsSafe(_isSafe); - }) + safeAPI + .getSafeCreationInfo(address) + .then(() => setIsSafe(true)) .catch(() => setIsSafe(false)) .finally(() => setSafeLoading(false)); - }, [address, findSafes]); + }, [address, safeAPI]); - return { isSafe, isSafeLoading, safeFoundNetworkPrefixes }; + return { isSafe, isSafeLoading }; }; From 2118a5dde307a9c643384f95d87706bfe4afa251 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:16:30 -0500 Subject: [PATCH 11/14] Add useResolveAddressMultiChain hook for multi-chain address resolution --- .../utils/useResolveAddressMultiChain.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/hooks/utils/useResolveAddressMultiChain.ts diff --git a/src/hooks/utils/useResolveAddressMultiChain.ts b/src/hooks/utils/useResolveAddressMultiChain.ts new file mode 100644 index 000000000..f3926b1e5 --- /dev/null +++ b/src/hooks/utils/useResolveAddressMultiChain.ts @@ -0,0 +1,72 @@ +import { useState, useCallback } from 'react'; +import { Address, createPublicClient, http, isAddress, getAddress } from 'viem'; +import { normalize } from 'viem/ens'; +import { supportedNetworks } from '../../providers/NetworkConfig/useNetworkConfigStore'; + +type ResolveAddressReturnType = { + resolved: { + address: Address; + chainId: number; + }[]; + isValid: boolean; +}; +export const useResolveAddressMultiChain = () => { + const [isLoading, setIsLoading] = useState(false); + + const resolveAddressMultiChain = useCallback( + async (input: string): Promise => { + setIsLoading(true); + + const returnedResult: ResolveAddressReturnType = { + resolved: [], + 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.resolved = supportedNetworks.map(network => ({ + address: getAddress(input), + chainId: network.chain.id, + })); + 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; + } + for (const network of supportedNetworks) { + const client = createPublicClient({ + chain: network.chain, + transport: http(network.rpcEndpoint), + }); + try { + const resolvedAddress = await client.getEnsAddress({ name: normalizedName }); + if (resolvedAddress) { + returnedResult.resolved.push({ + address: resolvedAddress, + chainId: network.chain.id, + }); + returnedResult.isValid = true; + } + } catch { + // do nothing + } + } + setIsLoading(false); + return returnedResult; + }, + [], + ); + return { resolveAddressMultiChain, isLoading }; +}; From 1e8292a5eb7b826e51a446a7eae59da4ba4efa72 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:16:38 -0500 Subject: [PATCH 12/14] Refactor DAOSearch to use resolved addresses with chain IDs and update SearchDisplay component --- .../ui/menus/DAOSearch/SearchDisplay.tsx | 7 +- src/components/ui/menus/DAOSearch/index.tsx | 16 ++-- src/hooks/DAO/useSearchDao.ts | 73 +++++++++++++------ 3 files changed, 63 insertions(+), 33 deletions(-) diff --git a/src/components/ui/menus/DAOSearch/SearchDisplay.tsx b/src/components/ui/menus/DAOSearch/SearchDisplay.tsx index 3e02346a5..55d47c569 100644 --- a/src/components/ui/menus/DAOSearch/SearchDisplay.tsx +++ b/src/components/ui/menus/DAOSearch/SearchDisplay.tsx @@ -4,6 +4,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Address } from 'viem'; import { SafeDisplayRow } from '../../../../pages/home/SafeDisplayRow'; +import { getNetworkConfig } from '../../../../providers/NetworkConfig/useNetworkConfigStore'; import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore'; import { ErrorBoundary } from '../../utils/ErrorBoundary'; import { MySafesErrorFallback } from '../../utils/MySafesErrorFallback'; @@ -13,7 +14,7 @@ interface ISearchDisplay { errorMessage: string | undefined; address: Address | undefined; onClickView: Function; - networkPrefix: string; + chainId: number; } export function SearchDisplay({ @@ -21,7 +22,7 @@ export function SearchDisplay({ errorMessage, address, onClickView, - networkPrefix, + chainId, }: ISearchDisplay) { const { t } = useTranslation(['common', 'dashboard']); const node = useDaoInfoStore(); @@ -90,7 +91,7 @@ export function SearchDisplay({ { onClickView(); }} diff --git a/src/components/ui/menus/DAOSearch/index.tsx b/src/components/ui/menus/DAOSearch/index.tsx index 9c1112391..a3dc58a0f 100644 --- a/src/components/ui/menus/DAOSearch/index.tsx +++ b/src/components/ui/menus/DAOSearch/index.tsx @@ -21,8 +21,7 @@ export function DAOSearch() { const { t } = useTranslation(['dashboard']); const [localInput, setLocalInput] = useState(''); const [typing, setTyping] = useState(false); - const { errorMessage, isLoading, address, setSearchString, safeFoundNetworkPrefixes } = - useSearchDao(); + const { errorMessage, isLoading, setSearchString, resolvedAddressesWithPrefix } = useSearchDao(); const { isOpen, onOpen, onClose } = useDisclosure(); const ref = useRef(null); @@ -65,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) { @@ -150,13 +148,13 @@ export function DAOSearch() { w="full" position="absolute" > - {safeFoundNetworkPrefixes.map(networkPrefix => ( + {resolvedAddressesWithPrefix.map(resolved => ( ))} diff --git a/src/hooks/DAO/useSearchDao.ts b/src/hooks/DAO/useSearchDao.ts index c80ba5d12..2974f98c9 100644 --- a/src/hooks/DAO/useSearchDao.ts +++ b/src/hooks/DAO/useSearchDao.ts @@ -1,39 +1,70 @@ -import { useState, useEffect } from 'react'; +import SafeApiKit from '@safe-global/api-kit'; +import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore'; -import { useIsSafe } from '../safe/useIsSafe'; -import useAddress from '../utils/useAddress'; +import { Address } from 'viem'; +import { useResolveAddressMultiChain } from '../utils/useResolveAddressMultiChain'; +type ResolvedAddressWithPrefix = { + address: Address; + chainId: number; +}; export const useSearchDao = () => { + const { resolveAddressMultiChain, isLoading: isAddressLoading } = useResolveAddressMultiChain(); const [searchString, setSearchString] = useState(''); const [errorMessage, setErrorMessage] = useState(); - // This hook needs to search all supoorted chains for the address - const { address, isValid, isLoading: isAddressLoading } = useAddress(searchString); - const { isSafe, isSafeLoading, safeFoundNetworkPrefixes } = useIsSafe(address); - const { t } = useTranslation('dashboard'); - const { chain } = useNetworkConfigStore(); + 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); + + setSafeResolvedAddressesWithPrefix(prevState => [...prevState, resolved]); + } catch (e) { + // Safe not found + continue; + } + } + setIsSafeLookupLoading(false); + }, + [], + ); - const isLoading = isAddressLoading === true || isSafeLoading === true; + const resolveInput = useCallback( + async (input: string) => { + const { resolved, isValid } = await resolveAddressMultiChain(input); + if (isValid) { + await findSafes(resolved); + } else { + setErrorMessage('Invalid search'); + } + }, + [findSafes, resolveAddressMultiChain], + ); + + const { t } = useTranslation('dashboard'); useEffect(() => { setErrorMessage(undefined); - - if (searchString === '' || isLoading || isSafe || isValid === undefined) { + setSafeResolvedAddressesWithPrefix([]); + if (searchString === '') { return; } - if (isValid === true) { - setErrorMessage(t('errorFailedSearch')); - } else { - setErrorMessage(t('errorInvalidSearch')); - } - }, [chain.name, isLoading, isSafe, isValid, searchString, t]); + resolveInput(searchString).catch(() => setErrorMessage(t('errorInvalidSearch'))); + }, [resolveInput, searchString, t]); return { - safeFoundNetworkPrefixes, + resolvedAddressesWithPrefix, errorMessage, - isLoading, - address, + isLoading: isAddressLoading || isSafeLookupLoading, setSearchString, searchString, }; From a25fd55f8c9c9c588cbbe16b76ca6fcc60b6d2f5 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:24:16 -0500 Subject: [PATCH 13/14] move translation initialization --- src/hooks/DAO/useSearchDao.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/hooks/DAO/useSearchDao.ts b/src/hooks/DAO/useSearchDao.ts index 2974f98c9..5b46fda4c 100644 --- a/src/hooks/DAO/useSearchDao.ts +++ b/src/hooks/DAO/useSearchDao.ts @@ -9,6 +9,7 @@ type ResolvedAddressWithPrefix = { chainId: number; }; export const useSearchDao = () => { + const { t } = useTranslation('dashboard'); const { resolveAddressMultiChain, isLoading: isAddressLoading } = useResolveAddressMultiChain(); const [searchString, setSearchString] = useState(''); const [errorMessage, setErrorMessage] = useState(); @@ -50,8 +51,6 @@ export const useSearchDao = () => { [findSafes, resolveAddressMultiChain], ); - const { t } = useTranslation('dashboard'); - useEffect(() => { setErrorMessage(undefined); setSafeResolvedAddressesWithPrefix([]); From 8dace81020110f03671b84db5136967cc8e0dca6 Mon Sep 17 00:00:00 2001 From: David Colon <38386583+Da-Colon@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:41:11 -0500 Subject: [PATCH 14/14] Remove 'wrongNetwork' type from LoadingProblem component and its usage in SafeController --- src/pages/LoadingProblem.tsx | 2 +- src/pages/dao/SafeController.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/LoadingProblem.tsx b/src/pages/LoadingProblem.tsx index f75597274..1f45e6b78 100644 --- a/src/pages/LoadingProblem.tsx +++ b/src/pages/LoadingProblem.tsx @@ -7,7 +7,7 @@ import { import { CONTENT_MAXW } from '../constants/common'; import { useNetworkConfigStore } from '../providers/NetworkConfig/useNetworkConfigStore'; -function LoadingProblem({ type }: { type: 'invalidSafe' | 'wrongNetwork' | 'badQueryParam' }) { +function LoadingProblem({ type }: { type: 'invalidSafe' | 'badQueryParam' }) { const { chain } = useNetworkConfigStore(); const { t } = useTranslation('common'); diff --git a/src/pages/dao/SafeController.tsx b/src/pages/dao/SafeController.tsx index f1a23f91d..76b142c25 100644 --- a/src/pages/dao/SafeController.tsx +++ b/src/pages/dao/SafeController.tsx @@ -49,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 ; }