Skip to content
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
62 changes: 5 additions & 57 deletions packages/client/src/components/ConnectWalletModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -36,6 +34,7 @@ export const ConnectWalletModal = ({
<Button onClick={onClose}>Continue</Button>
<DelegationButton
externalWalletClient={externalWalletClient}
onClose={onClose}
setBurner={setBurnerWithCleanup}
/>
</VStack>
Expand Down Expand Up @@ -68,6 +67,7 @@ export const ConnectWalletModal = ({
</VStack>
<DelegationButton
externalWalletClient={externalWalletClient}
onClose={onClose}
setBurner={setBurnerWithCleanup}
/>
</VStack>
Expand Down Expand Up @@ -102,55 +102,3 @@ export const ConnectWalletModal = ({
</Modal>
);
};

export type SetBurnerProps = { setBurner: (burner: Burner) => () => void };

const DelegationButton = ({
externalWalletClient,
setBurner,
}: SetBurnerProps & {
externalWalletClient: WalletClient<Transport, Chain, Account>;
}) => {
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 (
<Button onClick={() => switchChain({ chainId: chains[0].id })}>
Wrong Network
</Button>
);
}

if (status === 'delegated') {
return (
<SetBurner
externalWalletAccountAddress={externalWalletClient.account.address}
setBurner={setBurner}
/>
);
}

return <Button onClick={setupDelegation}>Delegate</Button>;
};

const SetBurner = ({
externalWalletAccountAddress,
setBurner,
}: SetBurnerProps & { externalWalletAccountAddress: Hex }) => {
const { network } = useMUD();

useEffect(
() => setBurner(createBurner(network, externalWalletAccountAddress)),
[externalWalletAccountAddress, network, setBurner],
);

return null;
};
90 changes: 90 additions & 0 deletions packages/client/src/components/DelegationButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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';

export type SetBurnerProps = { setBurner: (burner: Burner) => () => void };

export const DelegationButton = ({
externalWalletClient,
onClose,
setBurner,
}: SetBurnerProps & {
externalWalletClient: WalletClient<Transport, Chain, Account>;
onClose?: () => void;
}): JSX.Element => {
const { chains, switchChain } = useSwitchChain();
const { chainId } = useAccount();
const { status, setupDelegation } = useDelegation(externalWalletClient);
const { renderError, renderSuccess } = 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();

renderSuccess('Delegation successful');

if (onClose) {
onClose();
}
} catch (error) {
renderError(error, 'Failed to delegate');
} finally {
setIsDelegating(false);
}
}, [onClose, renderError, renderSuccess, setupDelegation]);

if (!isSupportedChain(chainId)) {
return (
<Button onClick={() => switchChain({ chainId: chains[0].id })}>
Switch to {getChainNameFromId(chains[0].id)}
</Button>
);
}

if (status === 'delegated') {
return (
<SetBurner
externalWalletAccountAddress={externalWalletClient.account.address}
setBurner={setBurner}
/>
);
}

return (
<Button
isLoading={isDelegating}
loadingText="Delegating..."
onClick={onSetupDelegation}
>
Delegate
</Button>
);
};

const SetBurner = ({
externalWalletAccountAddress,
setBurner,
}: SetBurnerProps & { externalWalletAccountAddress: Hex }) => {
const { network } = useMUD();

useEffect(
() => setBurner(createBurner(network, externalWalletAccountAddress)),
[externalWalletAccountAddress, network, setBurner],
);

return null;
};
5 changes: 1 addition & 4 deletions packages/client/src/contexts/Web3Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
43 changes: 43 additions & 0 deletions packages/client/src/hooks/useToast.ts
Original file line number Diff line number Diff line change
@@ -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 };
};
27 changes: 27 additions & 0 deletions packages/client/src/lib/web3/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
27 changes: 27 additions & 0 deletions packages/client/src/lib/web3/helpers.ts
Original file line number Diff line number Diff line change
@@ -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];
};
2 changes: 2 additions & 0 deletions packages/client/src/lib/web3/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './constants';
export * from './helpers';
18 changes: 18 additions & 0 deletions packages/client/src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -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;
};
10 changes: 8 additions & 2 deletions packages/client/src/utils/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
},
},
},
},
Expand Down