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 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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([]);