From af274d33faf77bbdaa48da4173bb73fb103fd230 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Thu, 30 May 2024 07:53:08 -0600 Subject: [PATCH 1/2] Refactor DelegationButton --- .../src/components/ConnectWalletModal.tsx | 62 +---------- .../src/components/DelegationButton.tsx | 102 ++++++++++++++++++ packages/client/src/contexts/Web3Provider.tsx | 5 +- packages/client/src/lib/web3/constants.ts | 27 +++++ packages/client/src/lib/web3/helpers.ts | 27 +++++ packages/client/src/lib/web3/index.ts | 2 + packages/client/src/utils/theme.ts | 10 +- 7 files changed, 172 insertions(+), 63 deletions(-) create mode 100644 packages/client/src/components/DelegationButton.tsx create mode 100644 packages/client/src/lib/web3/helpers.ts create mode 100644 packages/client/src/lib/web3/index.ts diff --git a/packages/client/src/components/ConnectWalletModal.tsx b/packages/client/src/components/ConnectWalletModal.tsx index 41142e1fc..93a15ddba 100644 --- a/packages/client/src/components/ConnectWalletModal.tsx +++ b/packages/client/src/components/ConnectWalletModal.tsx @@ -9,14 +9,12 @@ import { Text, VStack, } from '@chakra-ui/react'; -import { useEffect, useMemo } from 'react'; -import type { Account, Chain, Hex, Transport, WalletClient } from 'viem'; -import { useAccount, useSwitchChain, useWalletClient } from 'wagmi'; +import { useMemo } from 'react'; +import { useAccount, useWalletClient } from 'wagmi'; import { useMUD } from '../contexts/MUDContext'; -import { useDelegation } from '../hooks/useDelegation'; -import { type Burner, createBurner } from '../lib/mud/createBurner'; import { ConnectWalletButton } from './ConnectWalletButton'; +import { DelegationButton } from './DelegationButton'; export const ConnectWalletModal = ({ isOpen, @@ -36,6 +34,7 @@ export const ConnectWalletModal = ({ @@ -68,6 +67,7 @@ export const ConnectWalletModal = ({ @@ -102,55 +102,3 @@ export const ConnectWalletModal = ({ ); }; - -export type SetBurnerProps = { setBurner: (burner: Burner) => () => void }; - -const DelegationButton = ({ - externalWalletClient, - setBurner, -}: SetBurnerProps & { - externalWalletClient: WalletClient; -}) => { - const { chains, switchChain } = useSwitchChain(); - const { chainId } = useAccount(); - const { status, setupDelegation } = useDelegation(externalWalletClient); - - const wrongNetwork = useMemo(() => { - if (!chainId) return true; - const chainIds = chains.map(chain => chain.id); - return !chainIds.includes(chainId); - }, [chainId, chains]); - - if (wrongNetwork) { - return ( - - ); - } - - if (status === 'delegated') { - return ( - - ); - } - - return ; -}; - -const SetBurner = ({ - externalWalletAccountAddress, - setBurner, -}: SetBurnerProps & { externalWalletAccountAddress: Hex }) => { - const { network } = useMUD(); - - useEffect( - () => setBurner(createBurner(network, externalWalletAccountAddress)), - [externalWalletAccountAddress, network, setBurner], - ); - - return null; -}; diff --git a/packages/client/src/components/DelegationButton.tsx b/packages/client/src/components/DelegationButton.tsx new file mode 100644 index 000000000..93ec4a4e1 --- /dev/null +++ b/packages/client/src/components/DelegationButton.tsx @@ -0,0 +1,102 @@ +import { Button, useToast } from '@chakra-ui/react'; +import { useCallback, useEffect, useState } from 'react'; +import type { Account, Chain, Hex, Transport, WalletClient } from 'viem'; +import { useAccount, useSwitchChain } from 'wagmi'; + +import { useMUD } from '../contexts/MUDContext'; +import { useDelegation } from '../hooks/useDelegation'; +import { type Burner, createBurner } from '../lib/mud/createBurner'; +import { getChainNameFromId, isSupportedChain } from '../lib/web3'; + +export type SetBurnerProps = { setBurner: (burner: Burner) => () => void }; + +export const DelegationButton = ({ + externalWalletClient, + onClose, + setBurner, +}: SetBurnerProps & { + externalWalletClient: WalletClient; + onClose?: () => void; +}): JSX.Element => { + const { chains, switchChain } = useSwitchChain(); + const { chainId } = useAccount(); + const { status, setupDelegation } = useDelegation(externalWalletClient); + const toast = useToast(); + + const [isDelegating, setIsDelegating] = useState(false); + + const onSetupDelegation = useCallback(async () => { + try { + if (!setupDelegation) { + throw new Error('Delegation setup function not available'); + } + + setIsDelegating(true); + await setupDelegation(); + + toast({ + title: 'Delegation successful', + status: 'success', + duration: 5000, + isClosable: true, + }); + + if (onClose) { + onClose(); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + + toast({ + title: 'Delegation failed', + status: 'error', + duration: 5000, + isClosable: true, + }); + } finally { + setIsDelegating(false); + } + }, [onClose, setupDelegation, toast]); + + if (!isSupportedChain(chainId)) { + return ( + + ); + } + + if (status === 'delegated') { + return ( + + ); + } + + return ( + + ); +}; + +const SetBurner = ({ + externalWalletAccountAddress, + setBurner, +}: SetBurnerProps & { externalWalletAccountAddress: Hex }) => { + const { network } = useMUD(); + + useEffect( + () => setBurner(createBurner(network, externalWalletAccountAddress)), + [externalWalletAccountAddress, network, setBurner], + ); + + return null; +}; diff --git a/packages/client/src/contexts/Web3Provider.tsx b/packages/client/src/contexts/Web3Provider.tsx index eac04977d..60e4a4757 100644 --- a/packages/client/src/contexts/Web3Provider.tsx +++ b/packages/client/src/contexts/Web3Provider.tsx @@ -9,10 +9,7 @@ import { import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { createConfig, http, WagmiProvider } from 'wagmi'; -import { - SUPPORTED_CHAINS, - WALLET_CONNECT_PROJECT_ID, -} from '../lib/web3/constants'; +import { SUPPORTED_CHAINS, WALLET_CONNECT_PROJECT_ID } from '../lib/web3'; const { wallets } = getDefaultWallets(); diff --git a/packages/client/src/lib/web3/constants.ts b/packages/client/src/lib/web3/constants.ts index b0acfb2b1..c77b8b05c 100644 --- a/packages/client/src/lib/web3/constants.ts +++ b/packages/client/src/lib/web3/constants.ts @@ -3,6 +3,16 @@ import { anvil, baseSepolia, Chain } from 'wagmi/chains'; export const WALLET_CONNECT_PROJECT_ID = import.meta.env .VITE_WALLET_CONNECT_PROJECT_ID; +export const CHAIN_NAME_TO_ID: { [key: string]: number } = { + Anvil: anvil.id, + 'Base Sepolia': baseSepolia.id, +}; + +export const CHAIN_ID_TO_LABEL: { [key: number]: string } = { + [anvil.id]: 'Anvil', + [baseSepolia.id]: 'Base Sepolia', +}; + const getSupportedChains = () => { if (import.meta.env.DEV) { return [anvil] as const; @@ -18,6 +28,23 @@ const validateConfig = () => { if (!WALLET_CONNECT_PROJECT_ID) { throw new Error('VITE_WALLET_CONNECT_PROJECT_ID is not set'); } + + SUPPORTED_CHAINS.forEach(chain => { + if (!CHAIN_ID_TO_LABEL[chain.id]) { + throw new Error(`CHAIN_ID_TO_LABEL[${chain.id}] is not set`); + } + + if ( + !CHAIN_NAME_TO_ID[CHAIN_ID_TO_LABEL[chain.id]] || + CHAIN_NAME_TO_ID[CHAIN_ID_TO_LABEL[chain.id]] !== chain.id + ) { + throw new Error( + `CHAIN_NAME_TO_ID[${ + CHAIN_ID_TO_LABEL[chain.id] + }] is not set or does not match ${chain.id}`, + ); + } + }); }; validateConfig(); diff --git a/packages/client/src/lib/web3/helpers.ts b/packages/client/src/lib/web3/helpers.ts new file mode 100644 index 000000000..88beed007 --- /dev/null +++ b/packages/client/src/lib/web3/helpers.ts @@ -0,0 +1,27 @@ +import { + CHAIN_ID_TO_LABEL, + CHAIN_NAME_TO_ID, + SUPPORTED_CHAINS, +} from './constants'; + +export const isSupportedChain = ( + chainId: number | string | bigint | undefined, +): boolean => + chainId !== undefined && + SUPPORTED_CHAINS.find(c => c.id === Number(chainId)) !== undefined; + +export const getChainIdFromName = (chainLabel: string): number | undefined => { + const chainId = CHAIN_NAME_TO_ID[chainLabel]; + if (!chainId || !isSupportedChain(chainId)) { + return undefined; + } + return chainId; +}; + +export const getChainNameFromId = (chainId: number): string | undefined => { + if (!chainId || !isSupportedChain(chainId)) { + return undefined; + } + + return CHAIN_ID_TO_LABEL[chainId]; +}; diff --git a/packages/client/src/lib/web3/index.ts b/packages/client/src/lib/web3/index.ts new file mode 100644 index 000000000..5a9087d1d --- /dev/null +++ b/packages/client/src/lib/web3/index.ts @@ -0,0 +1,2 @@ +export * from './constants'; +export * from './helpers'; diff --git a/packages/client/src/utils/theme.ts b/packages/client/src/utils/theme.ts index c4482680d..41afb941d 100644 --- a/packages/client/src/utils/theme.ts +++ b/packages/client/src/utils/theme.ts @@ -25,11 +25,17 @@ const Button = { color: 'white', px: 10, py: 6, + _active: { + bg: 'rgba(0, 0, 0, 1)', + }, _hover: { bg: 'rgba(0, 0, 0, 0.8)', }, - _active: { - bg: 'rgba(0, 0, 0, 0.7)', + _loading: { + bg: 'rgba(0, 0, 0, 0.8)', + _hover: { + bg: 'rgba(0, 0, 0, 0.8)', + }, }, }, }, From 94cd3647e003492f0e5702d47e2fd32b8b8b1026 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Thu, 30 May 2024 08:03:30 -0600 Subject: [PATCH 2/2] Add better error handling --- .../src/components/DelegationButton.tsx | 24 +++-------- packages/client/src/hooks/useToast.ts | 43 +++++++++++++++++++ packages/client/src/utils/errors.ts | 18 ++++++++ 3 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 packages/client/src/hooks/useToast.ts create mode 100644 packages/client/src/utils/errors.ts diff --git a/packages/client/src/components/DelegationButton.tsx b/packages/client/src/components/DelegationButton.tsx index 93ec4a4e1..e16a263dd 100644 --- a/packages/client/src/components/DelegationButton.tsx +++ b/packages/client/src/components/DelegationButton.tsx @@ -1,10 +1,11 @@ -import { Button, useToast } from '@chakra-ui/react'; +import { Button } from '@chakra-ui/react'; import { useCallback, useEffect, useState } from 'react'; import type { Account, Chain, Hex, Transport, WalletClient } from 'viem'; import { useAccount, useSwitchChain } from 'wagmi'; import { useMUD } from '../contexts/MUDContext'; import { useDelegation } from '../hooks/useDelegation'; +import { useToast } from '../hooks/useToast'; import { type Burner, createBurner } from '../lib/mud/createBurner'; import { getChainNameFromId, isSupportedChain } from '../lib/web3'; @@ -21,7 +22,7 @@ export const DelegationButton = ({ const { chains, switchChain } = useSwitchChain(); const { chainId } = useAccount(); const { status, setupDelegation } = useDelegation(externalWalletClient); - const toast = useToast(); + const { renderError, renderSuccess } = useToast(); const [isDelegating, setIsDelegating] = useState(false); @@ -34,30 +35,17 @@ export const DelegationButton = ({ setIsDelegating(true); await setupDelegation(); - toast({ - title: 'Delegation successful', - status: 'success', - duration: 5000, - isClosable: true, - }); + renderSuccess('Delegation successful'); if (onClose) { onClose(); } } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - - toast({ - title: 'Delegation failed', - status: 'error', - duration: 5000, - isClosable: true, - }); + renderError(error, 'Failed to delegate'); } finally { setIsDelegating(false); } - }, [onClose, setupDelegation, toast]); + }, [onClose, renderError, renderSuccess, setupDelegation]); if (!isSupportedChain(chainId)) { return ( diff --git a/packages/client/src/hooks/useToast.ts b/packages/client/src/hooks/useToast.ts new file mode 100644 index 000000000..bcab045e6 --- /dev/null +++ b/packages/client/src/hooks/useToast.ts @@ -0,0 +1,43 @@ +import { useToast as useChakraToast } from '@chakra-ui/react'; + +import { getErrorMessage, USER_ERRORS } from '../utils/errors'; + +export const useToast = (): { + renderError: (error: unknown, defaultError?: string) => void; + renderWarning: (msg: string) => void; + renderSuccess: (msg: string) => void; +} => { + const toast = useChakraToast(); + + const renderError = (error: unknown, defaultError?: string) => { + const errorMsg = getErrorMessage(error); + + if (USER_ERRORS.includes(errorMsg)) { + return; + } + + toast({ + description: getErrorMessage(error, defaultError), + position: 'top', + status: 'error', + }); + }; + + const renderWarning = (msg: string) => { + toast({ + description: msg, + position: 'top', + status: 'warning', + }); + }; + + const renderSuccess = (msg: string) => { + toast({ + description: msg, + position: 'top', + status: 'success', + }); + }; + + return { renderError, renderWarning, renderSuccess }; +}; diff --git a/packages/client/src/utils/errors.ts b/packages/client/src/utils/errors.ts new file mode 100644 index 000000000..e3f70af26 --- /dev/null +++ b/packages/client/src/utils/errors.ts @@ -0,0 +1,18 @@ +export const USER_ERRORS = ['User denied signature']; + +export const getErrorMessage = ( + error: unknown, + defaultError: string = 'Unknown error', +): string => { + // eslint-disable-next-line no-console + console.error(error); + if (typeof error === 'string') { + return error; + } + + if ((error as Error)?.message?.toLowerCase().includes('user denied')) { + return USER_ERRORS[0]; + } + + return (error as Error)?.message || defaultError; +};