Skip to content

Commit

Permalink
Merge pull request #2630 from decentdao/issue/2565-cross-chain-dao-se…
Browse files Browse the repository at this point in the history
…arch

`[Issue #2565]` Cross-chain Safe search
  • Loading branch information
Da-Colon authored Dec 16, 2024
2 parents 5b8556b + 992c141 commit 89b2989
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 35 deletions.
14 changes: 10 additions & 4 deletions src/components/ui/menus/DAOSearch/SearchDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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 { getNetworkConfig } from '../../../../providers/NetworkConfig/useNetworkConfigStore';
import { useDaoInfoStore } from '../../../../store/daoInfo/useDaoInfoStore';
import { ErrorBoundary } from '../../utils/ErrorBoundary';
import { MySafesErrorFallback } from '../../utils/MySafesErrorFallback';
Expand All @@ -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 } = useNetworkConfigStore();

const isCurrentSafe = useMemo(
() => !!node && !!node?.safe?.address && node.safe.address === address,
Expand Down Expand Up @@ -85,7 +91,7 @@ export function SearchDisplay({ loading, errorMessage, address, onClickView }: I
<SafeDisplayRow
name={undefined}
address={address}
network={addressPrefix}
network={getNetworkConfig(chainId).addressPrefix}
onClick={() => {
onClickView();
}}
Expand Down
23 changes: 13 additions & 10 deletions src/components/ui/menus/DAOSearch/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function DAOSearch() {
const { t } = useTranslation(['dashboard']);
const [localInput, setLocalInput] = useState<string>('');
const [typing, setTyping] = useState<boolean>(false);
const { errorMessage, isLoading, address, setSearchString } = useSearchDao();
const { errorMessage, isLoading, setSearchString, resolvedAddressesWithPrefix } = useSearchDao();

const { isOpen, onOpen, onClose } = useDisclosure();
const ref = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -149,12 +148,16 @@ export function DAOSearch() {
w="full"
position="absolute"
>
<SearchDisplay
loading={isLoading}
errorMessage={errorMessage}
address={address}
onClickView={resetSearch}
/>
{resolvedAddressesWithPrefix.map(resolved => (
<SearchDisplay
key={resolved.address}
loading={isLoading}
errorMessage={errorMessage}
address={resolved.address}
chainId={resolved.chainId}
onClickView={resetSearch}
/>
))}
</Box>
</Popover>
</Box>
Expand Down
71 changes: 51 additions & 20 deletions src/hooks/DAO/useSearchDao.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,69 @@
import { useEffect, useState } 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 { t } = useTranslation('dashboard');
const { resolveAddressMultiChain, isLoading: isAddressLoading } = useResolveAddressMultiChain();
const [searchString, setSearchString] = useState<string>('');
const [errorMessage, setErrorMessage] = useState<string>();

const { address, isValid, isLoading: isAddressLoading } = useAddress(searchString);
const { isSafe, isSafeLoading } = useIsSafe(address);
const { t } = useTranslation('dashboard');
const { chain } = useNetworkConfigStore();
const [isSafeLookupLoading, setIsSafeLookupLoading] = useState<boolean>(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 { resolved, isValid } = await resolveAddressMultiChain(input);
if (isValid) {
await findSafes(resolved);
} else {
setErrorMessage('Invalid search');
}
},
[findSafes, resolveAddressMultiChain],
);

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,
};
Expand Down
72 changes: 72 additions & 0 deletions src/hooks/utils/useResolveAddressMultiChain.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>(false);

const resolveAddressMultiChain = useCallback(
async (input: string): Promise<ResolveAddressReturnType> => {
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 };
};
2 changes: 1 addition & 1 deletion src/i18n/locales/en/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 89b2989

Please sign in to comment.