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),