Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/get is safe #2747

Merged
merged 8 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions src/hooks/DAO/useSearchDao.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Address } from 'viem';
import { getSafeAPI } from '../../providers/App/hooks/useSafeAPI';
import {
supportedEnsNetworks,
supportedNetworks,
useNetworkConfigStore,
} from '../../providers/NetworkConfig/useNetworkConfigStore';
import { getIsSafe } from '../safe/useIsSafe';
import { useResolveENSName } from '../utils/useResolveENSName';

type ResolvedAddressWithChainId = {
Expand All @@ -29,22 +29,27 @@ export const useSearchDao = () => {
const findSafes = useCallback(
async (resolvedAddressesWithChainId: { address: Address; chainId: number }[]) => {
/*
This function only checks if the address is on any of the EVM networks.
The same safe could of on multiple networks

Changes requested inside getSafeCreationInfo
This function only checks if the address is a Safe on any of the EVM networks.
The same Safe could of on multiple networks
*/
for await (const resolved of resolvedAddressesWithChainId) {
try {
const safeAPI = getSafeAPI(getConfigByChainId(resolved.chainId));
await safeAPI.getSafeCreationInfo(resolved.address);

setSafeResolvedAddressesWithPrefix(prevState => [...prevState, resolved]);
} catch (e) {
// Safe not found
continue;
}
}
const realSafes = (
await Promise.all(
resolvedAddressesWithChainId.map(async resolved => {
const networkConfig = getConfigByChainId(resolved.chainId);
const isSafe = await getIsSafe(resolved.address, networkConfig);
if (isSafe) {
return resolved;
} else {
return null;
}
}),
)
).filter(safe => safe !== null);

// We're left with a list of chains and addresses
// (all the same address) that have a Safe at that address.
setSafeResolvedAddressesWithPrefix(realSafes);
},
[getConfigByChainId],
);
Expand Down
109 changes: 90 additions & 19 deletions src/hooks/safe/useIsSafe.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,110 @@
import {
getSafeL2SingletonDeployment,
getSafeSingletonDeployment,
} from '@safe-global/safe-deployments';
import { useEffect, useState } from 'react';
import { isAddress } from 'viem';
import { useSafeAPI } from '../../providers/App/hooks/useSafeAPI';

/**
* A hook which determines whether the provided Ethereum address is a Safe
* smart contract address on the currently connected chain (chainId).
*
* The state can be either true/false or undefined, if a network call is currently
* being performed to determine that status.
*
* @param address the address to check
* @returns isSafe: whether the address is a Safe,
* isSafeLoading: true/false whether the isSafe status is still being determined
*/
import { Address, createPublicClient, http, isAddress, toHex } from 'viem';
import { getSafeContractDeploymentAddress } from '../../providers/NetworkConfig/networks/utils';
import { useNetworkConfigStore } from '../../providers/NetworkConfig/useNetworkConfigStore';
import { NetworkConfig } from '../../types/network';

const safeVersions = ['1.3.0', '1.4.1'];

const safe130bytecode =
'0x608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea2646970667358221220d1429297349653a4918076d650332de1a1068c5f3e07c5c82360c277770b955264736f6c63430007060033';
const safe141bytecode =
'0x608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea264697066735822122003d1488ee65e08fa41e58e888a9865554c535f2c77126a82cb4c0f917f31441364736f6c63430007060033';

function getSafeSingleton(chainId: number, safeVersion: string): string | undefined {
try {
const singleton = getSafeContractDeploymentAddress(
getSafeSingletonDeployment,
safeVersion,
chainId.toString(),
);
return singleton.toLowerCase().replace('0x', '');
} catch (err) {
return undefined;
}
}

function getSafeL2Singleton(chainId: number, safeVersion: string): string | undefined {
try {
const singleton = getSafeContractDeploymentAddress(
getSafeL2SingletonDeployment,
safeVersion,
chainId.toString(),
);
return singleton.toLowerCase().replace('0x', '');
} catch (err) {
return undefined;
}
}

function getSafeSingletons(chainId: number): string[] {
const safeSingletons = safeVersions
.map(version => getSafeSingleton(chainId, version))
.filter(singleton => singleton != undefined);
const safeL2Singletons = safeVersions
.map(version => getSafeL2Singleton(chainId, version))
.filter(singleton => singleton != undefined);
return [safeSingletons, safeL2Singletons].flat();
}

export async function getIsSafe(address: Address, networkConfig: NetworkConfig): Promise<boolean> {
try {
const publicClient = createPublicClient({
chain: networkConfig.chain,
transport: http(networkConfig.rpcEndpoint),
});

const bytecode = (await publicClient.getBytecode({ address: address }))?.toLowerCase();
if (bytecode != safe130bytecode && bytecode != safe141bytecode) {
return false;
}

const store = await publicClient.getStorageAt({
address: address,
slot: toHex(0),
});

if (store === undefined) {
return false;
}

// We have the bytecode, let's just find if any of the single addresses are there
const safeSingletons = getSafeSingletons(networkConfig.chain.id);
return safeSingletons.some(singleton => store === `0x000000000000000000000000${singleton}`);
} catch (error) {
console.log(error);
return false;
}
}

export const useIsSafe = (address: string | undefined) => {
const [isSafeLoading, setSafeLoading] = useState<boolean>(false);
const [isSafe, setIsSafe] = useState<boolean | undefined>();
const safeAPI = useSafeAPI();

// currently connected chain
const { chain, getConfigByChainId } = useNetworkConfigStore();
const networkConfig = getConfigByChainId(chain.id);

useEffect(() => {
setSafeLoading(true);
setIsSafe(undefined);

if (!address || !isAddress(address) || !safeAPI) {
if (!address || !isAddress(address)) {
setIsSafe(false);
setSafeLoading(false);
return;
}

safeAPI
.getSafeCreationInfo(address)
getIsSafe(address, networkConfig)
.then(setIsSafe)
.then(() => setIsSafe(true))
.catch(() => setIsSafe(false))
.finally(() => setSafeLoading(false));
}, [address, safeAPI]);
}, [address, networkConfig]);

return { isSafe, isSafeLoading };
};
41 changes: 0 additions & 41 deletions src/providers/App/hooks/useSafeAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import SafeApiKit, {
AllTransactionsListResponse,
AllTransactionsOptions,
ProposeTransactionProps,
SafeCreationInfoResponse,
SafeInfoResponse,
SafeMultisigTransactionListResponse,
SignatureResponse,
Expand Down Expand Up @@ -38,7 +37,6 @@ class EnhancedSafeApiKit extends SafeApiKit {
//
// - overridden functions
// - getSafeInfo ✅
// - getSafeCreationInfo 🟨 ENG-291
// - getAllTransactions 🟨 ENG-292
// - getNextNonce 🟨 ENG-293
// - getToken ✅
Expand Down Expand Up @@ -134,45 +132,6 @@ class EnhancedSafeApiKit extends SafeApiKit {
throw new Error('Failed to getSafeInfo()');
}

override async getSafeCreationInfo(safeAddress: Address): Promise<SafeCreationInfoResponse> {
try {
return await super.getSafeCreationInfo(safeAddress);
} catch (error) {
console.error('Error fetching getSafeCreationInfo from safeAPI:', error);
}

try {
type SafeClientCreationInfoResponse = {
readonly created: string;
readonly creator: string;
readonly transactionHash: string;
readonly factoryAddress: string;

readonly masterCopy: string;
readonly setupData: string;
};

const response: SafeClientCreationInfoResponse = await this._safeClientGet(
safeAddress,
'/transactions/creation',
);

return { ...response, singleton: response.masterCopy };
} catch (error) {
console.error('Error fetching getSafeCreationInfo from safe-client:', error);
}

try {
// TODO ENG-291
// add another layer of onchain fallback here
// use subgraph to get this data
} catch (error) {
console.error('Error fetching getSafeCreationInfo from subgraph:', error);
}

throw new Error('Failed to getSafeCreationInfo()');
}

private async _safeClientGet(safeAddress: Address, path: string): Promise<any> {
const value = await axios.get(`${this.safeClientUrlPrefix}${safeAddress}${path}`, {
headers: {
Expand Down