diff --git a/packages/website/src/constants/deployChains.ts b/packages/website/src/constants/deployChains.ts deleted file mode 100644 index 02f124f4e..000000000 --- a/packages/website/src/constants/deployChains.ts +++ /dev/null @@ -1,76 +0,0 @@ -// Service urls taken from https://docs.safe.global/learn/safe-core/safe-core-api/available-services -// shortNames taken from https://github.com/ethereum-lists/chains/blob/master/_data/chains -export const chains = [ - { - id: 1, - name: 'Ethereum Mainnet', - shortName: 'eth', - serviceUrl: 'https://safe-transaction-mainnet.safe.global', - }, - { - id: 10, - name: 'Optimism', - shortName: 'oeth', - serviceUrl: 'https://safe-transaction-optimism.safe.global', - }, - { - id: 56, - name: 'Binance Smart Chain', - shortName: 'bnb', - serviceUrl: 'https://safe-transaction-bsc.safe.global', - }, - { - id: 100, - name: 'Gnosis Chain', - shortName: 'gno', - serviceUrl: 'https://safe-transaction-gnosis-chain.safe.global', - }, - { - id: 137, - name: 'Polygon', - shortName: 'matic', - serviceUrl: 'https://safe-transaction-polygon.safe.global', - }, - { - id: 420, - name: 'Optimism Goerli Testnet', - shortName: 'ogor', - serviceUrl: '', - }, - { - id: 8453, - name: 'Base', - shortName: 'base', - serviceUrl: 'https://safe-transaction-base.safe.global', - }, - { - id: 42161, - name: 'Arbitrum', - shortName: 'arb1', - serviceUrl: 'https://safe-transaction-arbitrum.safe.global', - }, - { - id: 43114, - name: 'Avalanche', - shortName: 'avax', - serviceUrl: 'https://safe-transaction-avalanche.safe.global', - }, - { - id: 11155111, - name: 'Sepolia', - shortName: 'sepolia', - serviceUrl: 'https://safe-transaction-sepolia.safe.global', - }, - { - id: 1313161554, - name: 'Aurora', - shortName: 'aurora', - serviceUrl: 'https://safe-transaction-aurora.safe.global', - }, - { - id: 1729, - name: 'Reya Network', - shortName: 'reyaNetwork', - serviceUrl: 'https://transaction.safe.reya.network', - }, -] as const; diff --git a/packages/website/src/features/Deploy/SafeAddressInput.tsx b/packages/website/src/features/Deploy/SafeAddressInput.tsx index 8d67509e5..10716256a 100644 --- a/packages/website/src/features/Deploy/SafeAddressInput.tsx +++ b/packages/website/src/features/Deploy/SafeAddressInput.tsx @@ -4,6 +4,7 @@ import { includes } from '@/helpers/array'; import { State, useStore } from '@/helpers/store'; import { isValidSafe, + isValidSafeFromSafeString, isValidSafeString, parseSafe, SafeString, @@ -12,7 +13,14 @@ import { useWalletPublicSafes, } from '@/hooks/safe'; import { CloseIcon, WarningIcon } from '@chakra-ui/icons'; -import { FormControl, IconButton, Text, Flex, Tooltip } from '@chakra-ui/react'; +import { + FormControl, + IconButton, + Text, + Flex, + Tooltip, + FormErrorMessage, +} from '@chakra-ui/react'; import { chakraComponents, ChakraStylesConfig, @@ -21,10 +29,11 @@ import { OptionProps, SingleValue, SingleValueProps, + MenuListProps, } from 'chakra-react-select'; import deepEqual from 'fast-deep-equal'; import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useSwitchChain } from 'wagmi'; import omit from 'lodash/omit'; @@ -44,6 +53,7 @@ export function SafeAddressInput() { const currentSafe = useStore((s) => s.currentSafe); const safeAddresses = useStore((s) => s.safeAddresses); const setCurrentSafe = useStore((s) => s.setCurrentSafe); + const [inputErrorText, setInputErrorText] = useState(''); // This state prevents the initialization useEffect (which sets the selected safe from the url or the currentSafe) // from running when clearing the input @@ -84,10 +94,10 @@ export function SafeAddressInput() { return; } - const parsedSafeInput = parseSafe(safeString); - if (!parsedSafeInput) { + if (!isValidSafeString(safeString)) { return; } + const parsedSafeInput = parseSafe(safeString); if (!isValidSafe(parsedSafeInput, chains)) { return; @@ -106,6 +116,9 @@ export function SafeAddressInput() { ); function handleSafeDelete(safeString: SafeString) { + if (!isValidSafeString(safeString)) { + return; + } deleteSafe(parseSafe(safeString)); } @@ -166,7 +179,7 @@ export function SafeAddressInput() { if (address && chainId) { const safeFromUrl = parseSafe(`${chainId}:${address}`); - if (!safeFromUrl || !isValidSafe(safeFromUrl, chains)) { + if (!isValidSafe(safeFromUrl, chains)) { throw new Error( "We couldn't find a safe for the specified chain. If it is a custom chain, please ensure that a custom provider is properly configured in the settings page." ); @@ -229,17 +242,31 @@ export function SafeAddressInput() { onCreateOption={handleNewOrSelectedSafe} // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore-next-line + //@ts-ignore-next-line onDeleteOption={(selected: SafeOption) => - handleSafeDelete(selected?.value || null) + handleSafeDelete(selected.value) } - isValidNewOption={isValidSafeString} + isValidNewOption={(safeString) => { + setInputErrorText(''); + const res = isValidSafeFromSafeString(safeString, chains); + if (!res && safeString !== '') { + setInputErrorText( + 'Invalid Safe Address. If you are using a custom chain, configure a custom PRC in the settings page.' + ); + } + return res; + }} components={{ Option: DeletableOption, SingleValue: SelectedOption, - MenuList: CustomMenuList, + MenuList: (props) => ( + + ), }} /> + {inputErrorText} + {currentSafe && pendingServiceTransactions.count > 0 && ( & { + inputErrorText: string; +}) { return ( - - To add a Safe, enter it in the format chainId:safeAddress + + {inputErrorText || + 'To add a Safe, enter it in the format chainId:safeAddress'} {children} diff --git a/packages/website/src/helpers/store.ts b/packages/website/src/helpers/store.ts index f26f56e07..878bafb27 100644 --- a/packages/website/src/helpers/store.ts +++ b/packages/website/src/helpers/store.ts @@ -4,7 +4,6 @@ import uniqWith from 'lodash/uniqWith'; import { Address, TransactionRequestBase, AbiFunction } from 'viem'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; -import { chains } from '@/constants/deployChains'; import { BuildState } from '@/hooks/cannon'; import { includes } from '@/helpers/array'; import { externalLinks } from '@/constants/externalLinks'; @@ -20,8 +19,6 @@ export type IdentifiableTxn = { pkgUrl: string; }; -export type ChainId = (typeof chains)[number]['id']; - export type SafeDefinition = { chainId: number; address: Address; diff --git a/packages/website/src/hooks/safe.ts b/packages/website/src/hooks/safe.ts index 18d2b5618..806bdf0cc 100644 --- a/packages/website/src/hooks/safe.ts +++ b/packages/website/src/hooks/safe.ts @@ -16,32 +16,36 @@ import { import { useAccount, useReadContracts } from 'wagmi'; import SafeABI from '@/abi/Safe.json'; import SafeABI_v1_4_1 from '@/abi/Safe-v1.4.1.json'; -import { chains } from '@/constants/deployChains'; import * as onchainStore from '@/helpers/onchain-store'; -import { ChainId, SafeDefinition, useStore } from '@/helpers/store'; +import { SafeDefinition, useStore } from '@/helpers/store'; import { SafeTransaction } from '@/types/SafeTransaction'; -import { useCannonChains } from '@/providers/CannonProvidersProvider'; +import { chainMetadata, useCannonChains } from '@/providers/CannonProvidersProvider'; -export type SafeString = `${ChainId}:${Address}`; +export type SafeString = `${number}:${Address}`; export function safeToString(safe: SafeDefinition): SafeString { - return `${safe.chainId as ChainId}:${safe.address}`; + return `${safe.chainId}:${safe.address}`; } -const addressStringRegex = /^[1-9][0-9]*:0x[a-fA-F0-9]{40}$/; - export function isValidSafeString(safeString: string): boolean { if (typeof safeString !== 'string') return false; - if (!addressStringRegex.test(safeString)) return false; const chainId = Number.parseInt(safeString.split(':')[0]); - return chains.some((chain) => chain.id === chainId); + if (isNaN(chainId)) return false; + const address = safeString.split(':')[1]; + return isAddress(address || ''); +} + +export function isValidSafeFromSafeString(safeString: string, chains: Chain[]): boolean { + if (!isValidSafeString(safeString)) return false; + const chainId = Number.parseInt(safeString.split(':')[0]); + const existChain = chains.some((chain) => chain.id === chainId); + return existChain; } -export function parseSafe(safeString: string): SafeDefinition | null { - if (!isValidSafeString(safeString)) return null; +export function parseSafe(safeString: string): SafeDefinition { const [chainId, address] = safeString.split(':'); return { - chainId: Number.parseInt(chainId) as ChainId, + chainId: Number.parseInt(chainId), address: getAddress(address), }; } @@ -55,12 +59,9 @@ export function isValidSafe(safe: SafeDefinition, supportedChains: Chain[]): boo ); } -function _getShortName(safe: SafeDefinition) { - return chains.find((chain) => chain.id === safe.chainId)?.shortName; -} - function _getSafeShortNameAddress(safe: SafeDefinition) { - return `${_getShortName(safe)}:${getAddress(safe.address)}`; + const metadata = chainMetadata[safe.chainId]; + return `${metadata.shortName || safe.chainId}:${getAddress(safe.address)}`; } export function getSafeUrl(safe: SafeDefinition, pathname = '/home') { @@ -71,12 +72,11 @@ export function getSafeUrl(safe: SafeDefinition, pathname = '/home') { function _createSafeApiKit(chainId: number) { if (!chainId) return null; - const chain = chains.find((chain) => chain.id === chainId); - + const chain = chainMetadata[chainId]; if (!chain?.serviceUrl) return null; return new SafeApiKit({ - chainId: BigInt(chain.id), + chainId: BigInt(chainId), txServiceUrl: new URL('/api', chain.serviceUrl).toString(), }); } diff --git a/packages/website/src/providers/CannonProvidersProvider.tsx b/packages/website/src/providers/CannonProvidersProvider.tsx index 4d1955405..913f2b4c9 100644 --- a/packages/website/src/providers/CannonProvidersProvider.tsx +++ b/packages/website/src/providers/CannonProvidersProvider.tsx @@ -28,7 +28,10 @@ import { useQuery } from '@tanstack/react-query'; type CustomProviders = | { chains: Chain[]; - chainMetadata: Record; + chainMetadata: Record< + number, + { color?: string; shortName?: string; serviceUrl?: string } + >; transports: Record; getChainById: (chainId: number) => Chain | undefined; getExplorerUrl: (chainId: number, hash: Hash) => string; @@ -42,33 +45,51 @@ const cannonNetwork = { name: 'Cannon', } as Chain; -const chainMetadata = { +// Service urls taken from https://docs.safe.global/learn/safe-core/safe-core-api/available-services +// shortNames taken from https://github.com/ethereum-lists/chains/blob/master/_data/chains +// export const chains = [ + +export const chainMetadata = { [chains.arbitrum.id]: { color: '#96bedc', + shortName: 'arb1', + serviceUrl: 'https://safe-transaction-arbitrum.safe.global', }, [chains.avalanche.id]: { color: '#e84141', + shortName: 'avax', + serviceUrl: 'https://safe-transaction-avalanche.safe.global', }, [chains.base.id]: { color: '#0052ff', + shortName: 'base', + serviceUrl: 'https://safe-transaction-base.safe.global', }, [chains.bsc.id]: { color: '#ebac0e', + shortName: 'bnb', + serviceUrl: 'https://safe-transaction-bsc.safe.global', }, [chains.cronos.id]: { color: '#002D74', }, [chains.mainnet.id]: { color: '#37367b', + shortName: 'eth', + serviceUrl: 'https://safe-transaction-mainnet.safe.global', }, [chains.hardhat.id]: { color: '#f9f7ec', }, [chains.optimism.id]: { color: '#ff5a57', + shortName: 'oeth', + serviceUrl: 'https://safe-transaction-optimism.safe.global', }, [chains.polygon.id]: { color: '#9f71ec', + shortName: 'matic', + serviceUrl: 'https://safe-transaction-polygon.safe.global', }, [chains.zora.id]: { color: '#000000', @@ -78,9 +99,21 @@ const chainMetadata = { }, [chains.gnosis.id]: { color: '#3e6957', + shortName: 'gno', + serviceUrl: 'https://safe-transaction-gnosis-chain.safe.global', + }, + [chains.sepolia.id]: { + shortName: 'sepolia', + serviceUrl: 'https://safe-transaction-sepolia.safe.global', }, [chains.reyaNetwork.id]: { color: '#04f06a', + shortName: 'reyaNetwork', + serviceUrl: 'https://transaction.safe.reya.network', + }, + [chains.aurora.id]: { + shortName: 'aurora', + serviceUrl: 'https://safe-transaction-aurora.safe.global', }, [chains.snax.id]: { color: '#00D1FF', @@ -208,6 +241,7 @@ export const CannonProvidersProvider: React.FC = ({ const chainsUrls = Object.values(verifiedProviders || {}).map( (v) => v.rpcUrl ); + const [_allChains, _allTransports] = useMemo( () => [ _getAllChains(verifiedProviders),