Skip to content

Commit

Permalink
feat(frontend): add multi network support (sifiorg#233)
Browse files Browse the repository at this point in the history
Co-authored-by: Norbert <[email protected]>
  • Loading branch information
boriskubrik and norbertdragan authored Sep 22, 2023
1 parent 5a9735b commit c080d12
Show file tree
Hide file tree
Showing 17 changed files with 229 additions and 93 deletions.
19 changes: 11 additions & 8 deletions packages/frontend/src/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SDKProvider } from './providers/SDKProvider';
import { SwapFormProvider } from './providers/SwapFormProvider';
import { WagmiProvider } from './providers/WagmiProvider';
import { TokensProvider } from './providers/TokensProvider';
import { SelectedChainProvider } from './providers/SelectedChainProvider';

const QueryProvider = QueryClientProvider;
const queryClient = new QueryClient();
Expand All @@ -19,14 +20,16 @@ export const AppProvider: FC<PropsWithChildren> = ({ children }) => {
<QueryProvider client={queryClient}>
<AnalyticsProvider>
<SDKProvider>
<TokensProvider>
<SwapFormProvider>
<WagmiProvider>
{children}
<CustomToastContainer />
</WagmiProvider>
</SwapFormProvider>
</TokensProvider>
<SelectedChainProvider>
<TokensProvider>
<SwapFormProvider>
<WagmiProvider>
{children}
<CustomToastContainer />
</WagmiProvider>
</SwapFormProvider>
</TokensProvider>
</SelectedChainProvider>
</SDKProvider>
</AnalyticsProvider>
<ReactQueryDevtools initialIsOpen={false} />
Expand Down
1 change: 1 addition & 0 deletions packages/frontend/src/assets/chain-icons/default-chain.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions packages/frontend/src/assets/chain-icons/optimism.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 5 additions & 4 deletions packages/frontend/src/components/CreateSwap/CreateSwap.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { useEffect, useState } from 'react';
import { useWatch, useForm, useFormContext } from 'react-hook-form';
import { useAccount, useWalletClient, usePublicClient } from 'wagmi';
import { mainnet } from 'viem/chains';
import { showToast, ShiftInput } from '@sifi/shared-ui';
import { useTokens } from 'src/hooks/useTokens';
import { useTokenBalance } from 'src/hooks/useTokenBalance';
import { useMutation } from '@tanstack/react-query';
import { useSifi } from 'src/providers/SDKProvider';
import { getEvmTxUrl, getTokenBySymbol, getViemErrorMessage, parseErrorMessage } from 'src/utils';
import { getEvmTxUrl, getTokenBySymbol, getViemErrorMessage } from 'src/utils';
import { SwapFormKey, SwapFormKeyHelper } from 'src/providers/SwapFormProvider';
import { useCullQueries } from 'src/hooks/useCullQueries';
import { useSpendableBalance } from 'src/hooks/useSpendableBalance';
Expand All @@ -19,12 +18,14 @@ import { MulticallToken } from 'src/types';
import { useMultiCallTokenBalance } from 'src/hooks/useMulticallTokenBalance';
import { usePermit2 } from 'src/hooks/usePermit2';
import { parseUnits } from 'viem';
import { useSelectedChain } from 'src/providers/SelectedChainProvider';

const CreateSwap = () => {
useCullQueries('quote');
const { address, isConnected } = useAccount();
const sifi = useSifi();
const publicClient = usePublicClient({ chainId: 1 });
const { selectedChain } = useSelectedChain();
const publicClient = usePublicClient({ chainId: selectedChain.id });
const { data: walletClient } = useWalletClient();
const { handleSubmit } = useForm();
const { tokens } = useTokens();
Expand Down Expand Up @@ -63,7 +64,7 @@ const CreateSwap = () => {

const { tx } = await sifi.getSwap({ fromAddress: address, quote, permit });
const res = await walletClient.sendTransaction({
chain: mainnet,
chain: selectedChain,
data: tx.data as `0x${string}`,
account: tx.from as `0x${string}`,
to: tx.to as `0x${string}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,24 @@ import { useSwitchNetwork } from 'wagmi';
import { useTokens } from 'src/hooks/useTokens';
import { SwapFormKey } from 'src/providers/SwapFormProvider';
import { getTokenBySymbol } from 'src/utils';
import { useAddNetwork } from 'src/hooks/useAddNetwork';
import { Button } from '../Button';
import { useSelectedChain } from 'src/providers/SelectedChainProvider';

const SwitchNetworkButton = () => {
const { switchNetwork, isLoading: isSwitchingNetwork } = useSwitchNetwork();
const { addNetwork } = useAddNetwork();
const { selectedChain } = useSelectedChain();
const { switchNetwork, isLoading: isSwitchingNetwork } = useSwitchNetwork({
onError: async error => {
if (error?.name.includes('ChainNotConfiguredForConnectorError')) {
await addNetwork(selectedChain);

if (!switchNetwork) return;

switchNetwork(selectedChain.id);
}
},
});
const { tokens } = useTokens();
const [fromTokenSymbol] = useWatch({
name: [SwapFormKey.FromToken, SwapFormKey.ToToken, SwapFormKey.FromAmount],
Expand All @@ -20,11 +34,9 @@ const SwitchNetworkButton = () => {
};

return (
<div className="mb-2">
<Button type="button" isLoading={isSwitchingNetwork} onClick={handleSwitchNetwork}>
Switch Network
</Button>
</div>
<Button type="button" isLoading={isSwitchingNetwork} onClick={handleSwitchNetwork}>
Switch Network
</Button>
);
};

Expand Down
120 changes: 62 additions & 58 deletions packages/frontend/src/components/NetworkSelector/NetworkSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,53 @@
import { Fragment, useState } from 'react';
import { Fragment } from 'react';
import { Listbox, Transition } from '@headlessui/react';
import { ReactComponent as DownCaret } from 'src/assets/down-caret.svg';
import EthereumIcon from '../../assets/chain-icons/ethereum.svg';
// import ArbitrumIcon from '../../assets/chain-icons/arbitrum.svg';
// import PolygonIcon from '../../assets/chain-icons/polygon.svg';
import { useSelectedChain } from 'src/providers/SelectedChainProvider';
import { enableMultipleChains } from 'src/utils/featureFlags';
import { SUPPORTED_CHAINS, getChainIcon } from 'src/utils/chains';
import { Chain, useNetwork, useSwitchNetwork } from 'wagmi';
import { useAddNetwork } from 'src/hooks/useAddNetwork';

// Networks are temporarily hadrcoded

enum SUPPORTED_NETWORKS {
ethereum = 'ethereum',
// polygon = 'polygon',
// arbitrum = 'arbitrum',
}
const NetworkSelector: React.FC = () => {
const { selectedChain, setSelectedChain } = useSelectedChain();
const { chain: activeChain } = useNetwork();
const { addNetwork } = useAddNetwork();
const { switchNetwork } = useSwitchNetwork({
onError: async error => {
if (error?.name.includes('ChainNotConfiguredForConnectorError')) {
await addNetwork(selectedChain);

type NetworkType = keyof typeof SUPPORTED_NETWORKS;
if (!switchNetwork) return;

const NETWORK_ICONS: Record<NetworkType, any> = {
ethereum: EthereumIcon,
// arbitrum: ArbitrumIcon,
// polygon: PolygonIcon,
};
switchNetwork(selectedChain.id);
}
},
});

const DEFAULT_NETWORK = SUPPORTED_NETWORKS.ethereum;
const chains = enableMultipleChains
? Object.values(SUPPORTED_CHAINS)
: Object.values(SUPPORTED_CHAINS).filter(chain => chain.id === 1);

const getChainIcon = (network: NetworkType) => {
const iconSrc = NETWORK_ICONS[network];
const handleChange = (chain: Chain) => {
setSelectedChain(chain);

return iconSrc ? <img src={iconSrc} alt={network} className="w-6" /> : null;
};
if (!switchNetwork) return;

const NetworkSelector: React.FC = () => {
const [selectedNetwork, setSelectedNetwork] = useState<NetworkType>(DEFAULT_NETWORK);
switchNetwork(chain.id);
};

return (
<div className="font-text relative inline-block">
<Listbox value={selectedNetwork} onChange={setSelectedNetwork}>
<Listbox value={selectedChain} onChange={handleChange}>
<div className="relative pr-0 sm:pr-4">
<Listbox.Button
role="button"
className="dark:text-flashbang-white text-new-black border-new-black dark:border-darker-gray font-display flex h-12
items-center gap-3 rounded-md border-0 px-4 py-2 text-sm max-[340px]:gap-3 sm:border-2 md:text-base"
aria-busy="true"
>
{selectedNetwork && getChainIcon(selectedNetwork)}
<span className="hidden">{selectedNetwork}</span>
{selectedChain && (
<img src={getChainIcon(selectedChain.id)} alt={selectedChain.name} className="w-6" />
)}
<DownCaret
className={`text-new-black dark:text-flashbang-white w-4
Expand All @@ -65,39 +69,39 @@ const NetworkSelector: React.FC = () => {
w-full min-w-[16rem] origin-top-right flex-col overflow-y-auto rounded-sm shadow-lg drop-shadow-xs-strong outline-none"
aria-busy="true"
>
{SUPPORTED_NETWORKS && (
<div className="font-display bg-flashbang-white flex flex-col gap-y-2 p-6 text-sm dark:bg-darkest-gray mr-3 rounded-sm">
{Object.values(SUPPORTED_NETWORKS).map(network => (
<Listbox.Option
key={network}
className={() =>
`dark:text-flashbang-white text-new-black font-display block cursor-pointer text-left text-base no-underline transition`
}
value={network}
>
{({ selected }) => (
<div
className={`flex rounded-xl place-items-center justify-between p-2 ${
selected
? 'bg-dark-gray bg-opacity-20'
: 'hover:bg-flashbang-white hover:bg-opacity-10 ease-linear transition-all'
} `}
>
<div className="flex">
<div className="mr-3">{getChainIcon(network)}</div>
<span>{network} </span>
<div className="font-display bg-flashbang-white flex flex-col gap-y-2 p-6 text-sm dark:bg-darkest-gray mr-3 rounded-sm">
{Object.values(chains).map(chain => (
<Listbox.Option
key={chain.name}
className={() =>
`dark:text-flashbang-white text-new-black font-display block cursor-pointer text-left text-base no-underline transition`
}
value={chain}
>
{({ selected }) => (
<div
className={`flex rounded-xl place-items-center justify-between p-2 ${
selected
? 'bg-dark-gray bg-opacity-20'
: 'hover:bg-flashbang-white hover:bg-opacity-10 ease-linear transition-all'
} `}
>
<div className="flex">
<div className="mr-3">
<img src={getChainIcon(chain.id)} alt={chain.name} className="w-6" />
</div>
{selected && (
<div>
<div className="w-3 h-3 bg-emerald-green rounded-full relative drop-shadow-xs-strong" />
</div>
)}
<span>{chain.name} </span>
</div>
)}
</Listbox.Option>
))}
</div>
)}
{chain.id === activeChain?.id && (
<div>
<div className="w-2 h-2 bg-emerald-green rounded-full relative drop-shadow-xs-strong" />
</div>
)}
</div>
)}
</Listbox.Option>
))}
</div>
</Listbox.Options>
</Transition>
</div>
Expand Down
35 changes: 35 additions & 0 deletions packages/frontend/src/hooks/useAddNetwork.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useCallback } from 'react';
import { Chain } from 'viem';
import { useWalletClient } from 'wagmi';

const useAddNetwork = () => {
const { data: walletClient } = useWalletClient();
const addNetwork = useCallback(async (chain: Chain) => {
if (walletClient) {
await walletClient.request({
method: 'wallet_addEthereumChain',
params: [
{
chainId: `0x${chain.id.toString(16)}`,
chainName: chain.name,
nativeCurrency: {
name: chain.nativeCurrency.name,
symbol: chain.nativeCurrency.symbol,
decimals: chain.nativeCurrency.decimals,
},
rpcUrls: chain.rpcUrls.default.http,
blockExplorerUrls: chain.blockExplorers?.default.url
? [chain.blockExplorers.default.url]
: [],
},
],
});
} else {
console.error('Ethereum object does not exist on window, or does not have a request method.');
}
}, []);

return { addNetwork };
};

export { useAddNetwork };
8 changes: 5 additions & 3 deletions packages/frontend/src/hooks/useApprove.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { erc20ABI, mainnet, usePublicClient, useWalletClient } from 'wagmi';
import { erc20ABI, usePublicClient, useWalletClient } from 'wagmi';
import { useWatch } from 'react-hook-form';
import { MAX_ALLOWANCE } from 'src/constants';
import { SwapFormKey } from 'src/providers/SwapFormProvider';
import { getTokenBySymbol } from 'src/utils';
import { useQuote } from './useQuote';
import { useTokens } from './useTokens';
import { useSelectedChain } from 'src/providers/SelectedChainProvider';

const useApprove = () => {
const { selectedChain } = useSelectedChain();
const [isApprovalModalOpen, setIsApprovalModalOpen] = useState(false);
const publicClient = usePublicClient();
const publicClient = usePublicClient({ chainId: selectedChain.id });
const { data: walletClient } = useWalletClient();
const [isLoading, setIsLoading] = useState(false);
const { quote } = useQuote();
Expand Down Expand Up @@ -40,7 +42,7 @@ const useApprove = () => {
// TODO: Handle case when account already has allowance but it's not sufficient

const hash = await walletClient.writeContract({
chain: mainnet,
chain: selectedChain,
address: fromToken.address as `0x${string}`,
abi: erc20ABI,
functionName: 'approve',
Expand Down
4 changes: 3 additions & 1 deletion packages/frontend/src/hooks/useMulticallTokenBalance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ import { useEffect, useState } from "react";
import { ERC20_ABI, ETH_CONTRACT_ADDRESS } from "src/constants";
import type { BalanceMap, MulticallToken } from 'src/types';
import { formatTokenAmount } from "src/utils";
import { useSelectedChain } from 'src/providers/SelectedChainProvider';

type UseMultiCallTokenBalance = {
balanceMap: BalanceMap | null;
refetch: () => void;
}

const useMultiCallTokenBalance = (tokens: MulticallToken[]): UseMultiCallTokenBalance => {
const { selectedChain } = useSelectedChain();
const { address } = useAccount();
const [addressBalanceMap, setAddressBalanceMap] = useState<BalanceMap | null>(null)
const publicClient = usePublicClient();
const publicClient = usePublicClient({ chainId: selectedChain.id });

const balanceReadContracts = address?.startsWith('0x') ? tokens.map(token => ({
address: token.address,
Expand Down
12 changes: 7 additions & 5 deletions packages/frontend/src/hooks/useQuote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useQuery } from '@tanstack/react-query';
import { useFormContext, useWatch } from 'react-hook-form';
import { useSifi } from 'src/providers/SDKProvider';
import { SwapFormKey } from 'src/providers/SwapFormProvider';
import { useSelectedChain } from 'src/providers/SelectedChainProvider';
import { formatTokenAmount, getQueryKey, getTokenBySymbol, isValidTokenAmount } from 'src/utils';
import { ETH_CONTRACT_ADDRESS } from 'src/constants';
import { useTokens } from './useTokens';
Expand All @@ -16,19 +17,20 @@ const useQuote = () => {
});
const { setValue } = useFormContext();
const { tokens } = useTokens();
const { selectedChain } = useSelectedChain();
const fromToken = getTokenBySymbol(fromTokenSymbol, tokens);
const toToken = getTokenBySymbol(toTokenSymbol, tokens);
const isFromEthereum = fromToken?.address === ETH_CONTRACT_ADDRESS;
const isSameTokenPair = fromToken?.address === toToken?.address;

const quoteRequest = {
fromToken: fromToken?.address || ETH_CONTRACT_ADDRESS,
toToken: toToken?.address || ETH_CONTRACT_ADDRESS,
fromAmount: parseUnits(
fromAmount?.endsWith('.') ? `${fromAmount}0` : fromAmount || '0',
fromToken?.decimals || 0
)
.toString(),
fromAmount?.endsWith('.') ? `${fromAmount}0` : fromAmount || '0',
fromToken?.decimals || 0
).toString(),
fromChain: selectedChain.id,
toChain: selectedChain.id,
};

const handleSuccesfulQuoteFetch = (quote: Quote): void => {
Expand Down
Loading

0 comments on commit c080d12

Please sign in to comment.