diff --git a/src/components/input/TokenSelectField.tsx b/src/components/input/TokenSelectField.tsx index dcaba80..c02f7cd 100644 --- a/src/components/input/TokenSelectField.tsx +++ b/src/components/input/TokenSelectField.tsx @@ -1,15 +1,14 @@ import { useField } from 'formik' -import { useMemo } from 'react' import { ChevronIcon } from 'src/components/Chevron' import { Select } from 'src/components/input/Select' -import { TokenId, getTokenById, getTokenOptionsByChainId } from 'src/config/tokens' +import { TokenId, getTokenById } from 'src/config/tokens' import { TokenIcon } from 'src/images/tokens/TokenIcon' -import { useNetwork } from 'wagmi' type Props = { name: string label: string onChange?: (optionValue: string) => void + tokenOptions: TokenId[] } const DEFAULT_VALUE = { @@ -17,14 +16,9 @@ const DEFAULT_VALUE = { value: '', } -export function TokenSelectField({ name, label, onChange }: Props) { +export function TokenSelectField({ name, label, onChange, tokenOptions }: Props) { const [field, , helpers] = useField(name) - const { chain } = useNetwork() - const tokenOptions = useMemo(() => { - return chain ? getTokenOptionsByChainId(chain.id) : Object.values(TokenId) - }, [chain]) - const handleChange = (optionValue: string) => { helpers.setValue(optionValue || '') if (onChange) onChange(optionValue) diff --git a/src/config/exchanges.ts b/src/config/exchanges.ts index 0e2df4d..0d02c5b 100644 --- a/src/config/exchanges.ts +++ b/src/config/exchanges.ts @@ -40,8 +40,31 @@ export const AlfajoresExchanges: Exchange[] = [ '0x87D61dA3d668797786D73BC674F053f87111570d', ], }, + { + providerAddr: '0x9B64E8EaBD1a035b148cE970d3319c5C3Ad53EC3', + id: '0x3e6d9109df536ba3f4c166e598bdfe132dca06573a54ca40c2b6f23ac6bd6cc6', + assets: [ + '0x10c892A6EC43a53E45D0B916B4b7D383B1b78C0F', + '0x87D61dA3d668797786D73BC674F053f87111570d', + ], + }, + { + providerAddr: '0x9B64E8EaBD1a035b148cE970d3319c5C3Ad53EC3', + id: '0xcfaa6be9334ee54fda94f2cfdf4c8bc376f24ce008ab9559b2a06b9fc388e78c', + assets: [ + '0xE4D517785D091D3c54818832dB6094bcc2744545', + '0x87D61dA3d668797786D73BC674F053f87111570d', + ], + }, + { + providerAddr: '0x9B64E8EaBD1a035b148cE970d3319c5C3Ad53EC3', + id: '0xe807b1ebe8b57ac4e5c1b8d51fcf8e3b21e919fd788bab807886c4f446a74d37', + assets: [ + '0x10c892A6EC43a53E45D0B916B4b7D383B1b78C0F', + '0x6e673502c5b55F3169657C004e5797fFE5be6653', + ], + }, ] - export const BaklavaExchanges: Exchange[] = [ { providerAddr: '0xFF9a3da00F42839CD6D33AD7adf50bCc97B41411', @@ -75,8 +98,31 @@ export const BaklavaExchanges: Exchange[] = [ '0xD4079B322c392D6b196f90AA4c439fC2C16d6770', ], }, + { + providerAddr: '0xFF9a3da00F42839CD6D33AD7adf50bCc97B41411', + id: '0x3e6d9109df536ba3f4c166e598bdfe132dca06573a54ca40c2b6f23ac6bd6cc6', + assets: [ + '0xf9ecE301247aD2CE21894941830A2470f4E774ca', + '0xD4079B322c392D6b196f90AA4c439fC2C16d6770', + ], + }, + { + providerAddr: '0xFF9a3da00F42839CD6D33AD7adf50bCc97B41411', + id: '0xcfaa6be9334ee54fda94f2cfdf4c8bc376f24ce008ab9559b2a06b9fc388e78c', + assets: [ + '0x6a0EEf2bed4C30Dc2CB42fe6c5f01F80f7EF16d1', + '0xD4079B322c392D6b196f90AA4c439fC2C16d6770', + ], + }, + { + providerAddr: '0xFF9a3da00F42839CD6D33AD7adf50bCc97B41411', + id: '0xe807b1ebe8b57ac4e5c1b8d51fcf8e3b21e919fd788bab807886c4f446a74d37', + assets: [ + '0xf9ecE301247aD2CE21894941830A2470f4E774ca', + '0x6f90ac394b1F45290d3023e4Ba0203005cAF2A4B', + ], + }, ] - export const CeloExchanges: Exchange[] = [ { providerAddr: '0x22d9db95E6Ae61c104A7B6F6C78D7993B94ec901', diff --git a/src/config/tokens.ts b/src/config/tokens.ts index 19bf5c3..4494838 100644 --- a/src/config/tokens.ts +++ b/src/config/tokens.ts @@ -1,4 +1,5 @@ import { ChainId } from 'src/config/chains' +import { MentoExchanges } from 'src/config/exchanges' import { Color } from 'src/styles/Color' import { areAddressesEqual } from 'src/utils/addresses' @@ -20,6 +21,7 @@ export enum TokenId { cEUR = 'cEUR', cREAL = 'cREAL', axlUSDC = 'axlUSDC', + axlEUROC = 'axlEUROC', } export const NativeStableTokenIds = [TokenId.cUSD, TokenId.cEUR, TokenId.cREAL] @@ -62,12 +64,21 @@ export const axlUSDC: Token = Object.freeze({ decimals: 6, }) +export const axlEUROC: Token = Object.freeze({ + id: TokenId.axlEUROC, + symbol: TokenId.axlEUROC, + name: 'Axelar EUROC', + color: Color.usdcBlue, // TODO: Change to EUROC + decimals: 6, +}) + export const Tokens: Record = { CELO, cUSD, cEUR, cREAL, axlUSDC, + axlEUROC, } export const TokenAddresses: Record> = Object.freeze({ @@ -77,6 +88,7 @@ export const TokenAddresses: Record> = Object. [TokenId.cEUR]: '0x10c892A6EC43a53E45D0B916B4b7D383B1b78C0F', [TokenId.cREAL]: '0xE4D517785D091D3c54818832dB6094bcc2744545', [TokenId.axlUSDC]: '0x87D61dA3d668797786D73BC674F053f87111570d', + [TokenId.axlEUROC]: '0x6e673502c5b55F3169657C004e5797fFE5be6653', }, [ChainId.Baklava]: { [TokenId.CELO]: '0xdDc9bE57f553fe75752D61606B94CBD7e0264eF8', @@ -84,6 +96,7 @@ export const TokenAddresses: Record> = Object. [TokenId.cEUR]: '0xf9ecE301247aD2CE21894941830A2470f4E774ca', [TokenId.cREAL]: '0x6a0EEf2bed4C30Dc2CB42fe6c5f01F80f7EF16d1', [TokenId.axlUSDC]: '0xD4079B322c392D6b196f90AA4c439fC2C16d6770', + [TokenId.axlEUROC]: '0x6f90ac394b1F45290d3023e4Ba0203005cAF2A4B', }, [ChainId.Celo]: { [TokenId.CELO]: '0x471EcE3750Da237f93B8E339c536989b8978a438', @@ -91,6 +104,7 @@ export const TokenAddresses: Record> = Object. [TokenId.cEUR]: '0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73', [TokenId.cREAL]: '0xe8537a3d056DA446677B9E9d6c5dB704EaAb4787', [TokenId.axlUSDC]: '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + [TokenId.axlEUROC]: '', }, }) @@ -106,9 +120,34 @@ export function isNativeStableToken(tokenId: string) { return NativeStableTokenIds.includes(tokenId as TokenId) } +export function isSwappable(token_1: string, token_2: string, chainId: number) { + const exchanges = MentoExchanges[chainId as ChainId] + + if (!exchanges) return false + + if (token_1 === token_2) return false + + return exchanges.some( + (obj) => + obj.assets.includes(getTokenAddress(token_1 as TokenId, chainId)) && + obj.assets.includes(getTokenAddress(token_2 as TokenId, chainId)) + ) +} + +export function getSwappableTokenOptions(token: string, chainId: ChainId) { + return getTokenOptionsByChainId(chainId) + .filter((tkn) => isSwappable(tkn, token, chainId)) + .filter((tkn) => token !== tkn) +} + export function getTokenOptionsByChainId(chainId: ChainId): TokenId[] { const tokensForChain = TokenAddresses[chainId] - return tokensForChain ? (Object.keys(TokenAddresses[chainId]) as TokenId[]) : [] + + return tokensForChain + ? Object.entries(tokensForChain) + .filter(([, tokenAddress]) => tokenAddress !== '') // Allows incomplete 'TokenAddresses' list i.e When tokens are not on all chains + .map(([tokenId]) => tokenId as TokenId) + : [] } export function getTokenById(id: string): Token | null { diff --git a/src/features/swap/SwapForm.tsx b/src/features/swap/SwapForm.tsx index d2b1c5e..fc79914 100644 --- a/src/features/swap/SwapForm.tsx +++ b/src/features/swap/SwapForm.tsx @@ -1,23 +1,33 @@ import { useConnectModal } from '@rainbow-me/rainbowkit' import { Form, Formik, useFormikContext } from 'formik' -import { ReactNode, SVGProps, useEffect } from 'react' +import { ReactNode, SVGProps, useEffect, useMemo } from 'react' import { toast } from 'react-toastify' import { Spinner } from 'src/components/animation/Spinner' import { Button3D } from 'src/components/buttons/3DButton' import { RadioInput } from 'src/components/input/RadioInput' import { TokenSelectField } from 'src/components/input/TokenSelectField' -import { TokenId, Tokens, isNativeStableToken, isUSDCVariant } from 'src/config/tokens' +import { + TokenId, + Tokens, + getSwappableTokenOptions, + getTokenOptionsByChainId, + isSwappable, +} from 'src/config/tokens' +import { reset as accountReset } from 'src/features/accounts/accountSlice' import { AccountBalances } from 'src/features/accounts/fetchBalances' +import { reset as blockReset } from 'src/features/blocks/blockSlice' +import { resetTokenPrices } from 'src/features/chart/tokenPriceSlice' import { useAppDispatch, useAppSelector } from 'src/features/store/hooks' import { SettingsMenu } from 'src/features/swap/SettingsMenu' -import { setFormValues } from 'src/features/swap/swapSlice' +import { setFormValues, reset as swapReset } from 'src/features/swap/swapSlice' import { SwapDirection, SwapFormValues } from 'src/features/swap/types' import { useFormValidator } from 'src/features/swap/useFormValidator' import { useSwapQuote } from 'src/features/swap/useSwapQuote' import { FloatingBox } from 'src/layout/FloatingBox' import { fromWei, fromWeiRounded, toSignificant } from 'src/utils/amount' +import { logger } from 'src/utils/logger' import { escapeRegExp, inputRegex } from 'src/utils/string' -import { useAccount } from 'wagmi' +import { useAccount, useNetwork, useSwitchNetwork } from 'wagmi' const initialValues: SwapFormValues = { fromTokenId: TokenId.CELO, @@ -79,9 +89,18 @@ function SwapForm() { function SwapFormInputs({ balances }: { balances: AccountBalances }) { const { address, isConnected } = useAccount() + const { chain } = useNetwork() + + const tokensForChain = useMemo(() => { + return chain ? getTokenOptionsByChainId(chain?.id) : Object.values(TokenId) + }, [chain]) const { values, setFieldValue } = useFormikContext() + const swappableTokenOptions = useMemo(() => { + return chain ? getSwappableTokenOptions(values.fromTokenId, chain?.id) : Object.values(TokenId) + }, [chain, values]) + const { amount, direction, fromTokenId, toTokenId } = values const { isLoading, quote, rate } = useSwapQuote(amount, direction, fromTokenId, toTokenId) @@ -90,6 +109,15 @@ function SwapFormInputs({ balances }: { balances: AccountBalances }) { setFieldValue('quote', quote) }, [quote, setFieldValue]) + useEffect(() => { + if (chain && isConnected && !isSwappable(values.fromTokenId, values.toTokenId, chain?.id)) { + setFieldValue( + 'toTokenId', + swappableTokenOptions.length < 1 ? TokenId.cUSD : swappableTokenOptions[0] + ) + } + }, [setFieldValue, chain, values, swappableTokenOptions, isConnected]) + const roundedBalance = fromWeiRounded(balances[fromTokenId], Tokens[fromTokenId].decimals) const isRoundedBalanceGreaterThanZero = Boolean(Number.parseInt(roundedBalance) > 0) const onClickUseMax = () => { @@ -101,28 +129,19 @@ function SwapFormInputs({ balances }: { balances: AccountBalances }) { const onChangeToken = (isFromToken: boolean) => (tokenId: string) => { const targetField = isFromToken ? 'fromTokenId' : 'toTokenId' - const otherField = isFromToken ? 'toTokenId' : 'fromTokenId' - if (isUSDCVariant(tokenId)) { - setFieldValue(targetField, tokenId) - setFieldValue(otherField, TokenId.cUSD) - } else if (isNativeStableToken(tokenId)) { - setFieldValue(targetField, tokenId) - setFieldValue(otherField, TokenId.CELO) - } else { - const currentTargetTokenId = values[targetField] - const stableTokenId = isNativeStableToken(currentTargetTokenId) - ? currentTargetTokenId - : TokenId.cUSD - setFieldValue(targetField, tokenId) - setFieldValue(otherField, stableTokenId) - } + setFieldValue(targetField, tokenId) } return (
- +
{address && isConnected && isRoundedBalanceGreaterThanZero && ( @@ -146,7 +165,12 @@ function SwapFormInputs({ balances }: { balances: AccountBalances }) {
- +
@@ -198,6 +222,7 @@ function AmountField({ return ( () + const isAccountReady = address && isConnected + const isOnCelo = chains.some((chn) => chn.id === chain?.id) + + const switchToNetwork = async () => { + try { + if (!switchNetworkAsync) throw new Error('switchNetworkAsync undefined') + logger.debug('Resetting and switching to Celo') + await switchNetworkAsync(42220) + dispatch(blockReset()) + dispatch(accountReset()) + dispatch(swapReset()) + dispatch(resetTokenPrices()) + } catch (error) { + logger.error('Error updating network', error) + toast.error('Could not switch network, does wallet support switching?') + } + } - const { errors, touched } = useFormikContext() const error = touched.amount && (errors.amount || errors.fromTokenId || errors.toTokenId || errors.slippage) - const text = error ? error : isAccountReady ? 'Continue' : 'Connect Wallet' + let text + + if (error) { + text = error + } else if (!isAccountReady) { + text = 'Connect Wallet' + } else if (!isOnCelo) { + text = 'Switch to Celo Network' + } else { + text = 'Continue' + } + const type = isAccountReady ? 'submit' : 'button' - const onClick = isAccountReady ? undefined : openConnectModal + let onClick + + if (!isAccountReady) { + onClick = openConnectModal + } else if (!isOnCelo) { + onClick = switchToNetwork + } const showLongError = typeof error === 'string' && error?.length > 50