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

fix(website): CAN-502 - safes from custom chains #1463

Merged
merged 11 commits into from
Oct 15, 2024
76 changes: 0 additions & 76 deletions packages/website/src/constants/deployChains.ts

This file was deleted.

61 changes: 50 additions & 11 deletions packages/website/src/features/Deploy/SafeAddressInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { includes } from '@/helpers/array';
import { State, useStore } from '@/helpers/store';
import {
isValidSafe,
isValidSafeFromSafeString,
isValidSafeString,
parseSafe,
SafeString,
Expand All @@ -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,
Expand All @@ -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';

Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -106,6 +116,9 @@ export function SafeAddressInput() {
);

function handleSafeDelete(safeString: SafeString) {
if (!isValidSafeString(safeString)) {
return;
}
deleteSafe(parseSafe(safeString));
}

Expand Down Expand Up @@ -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."
);
Expand Down Expand Up @@ -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) => (
<CustomMenuList {...props} inputErrorText={inputErrorText} />
),
}}
/>
<FormErrorMessage>{inputErrorText}</FormErrorMessage>
</FormControl>

{currentSafe && pendingServiceTransactions.count > 0 && (
<Tooltip
label={`There ${
Expand Down Expand Up @@ -319,11 +346,23 @@ function DeletableOption({
);
}

function CustomMenuList({ children, ...props }: any) {
function CustomMenuList({
children,
inputErrorText,
...props
}: MenuListProps<SafeOption> & {
inputErrorText: string;
}) {
return (
<chakraComponents.MenuList {...props}>
<Text color="gray.400" fontSize="xs" my={2} ml={4}>
To add a Safe, enter it in the format chainId:safeAddress
<Text
color={inputErrorText ? 'red.400' : 'gray.400'}
fontSize="xs"
my={2}
ml={4}
>
{inputErrorText ||
'To add a Safe, enter it in the format chainId:safeAddress'}
</Text>
{children}
</chakraComponents.MenuList>
Expand Down
3 changes: 0 additions & 3 deletions packages/website/src/helpers/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -20,8 +19,6 @@ export type IdentifiableTxn = {
pkgUrl: string;
};

export type ChainId = (typeof chains)[number]['id'];

export type SafeDefinition = {
chainId: number;
address: Address;
Expand Down
40 changes: 20 additions & 20 deletions packages/website/src/hooks/safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
}
Expand All @@ -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') {
Expand All @@ -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(),
});
}
Expand Down
Loading
Loading