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

feat: add mu03 support #99

Merged
merged 10 commits into from
Sep 12, 2023
12 changes: 3 additions & 9 deletions src/components/input/TokenSelectField.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,24 @@
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 = {
label: 'Select Token',
value: '',
}

export function TokenSelectField({ name, label, onChange }: Props) {
export function TokenSelectField({ name, label, onChange, tokenOptions }: Props) {
const [field, , helpers] = useField<string>(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)
Expand Down
50 changes: 48 additions & 2 deletions src/config/exchanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
41 changes: 40 additions & 1 deletion src/config/tokens.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -20,6 +21,7 @@ export enum TokenId {
cEUR = 'cEUR',
cREAL = 'cREAL',
axlUSDC = 'axlUSDC',
axlEUROC = 'axlEUROC',
}

export const NativeStableTokenIds = [TokenId.cUSD, TokenId.cEUR, TokenId.cREAL]
Expand Down Expand Up @@ -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<TokenId, Token> = {
CELO,
cUSD,
cEUR,
cREAL,
axlUSDC,
axlEUROC,
}

export const TokenAddresses: Record<ChainId, Record<TokenId, Address>> = Object.freeze({
Expand All @@ -77,20 +88,23 @@ export const TokenAddresses: Record<ChainId, Record<TokenId, Address>> = Object.
[TokenId.cEUR]: '0x10c892A6EC43a53E45D0B916B4b7D383B1b78C0F',
[TokenId.cREAL]: '0xE4D517785D091D3c54818832dB6094bcc2744545',
[TokenId.axlUSDC]: '0x87D61dA3d668797786D73BC674F053f87111570d',
[TokenId.axlEUROC]: '0x6e673502c5b55F3169657C004e5797fFE5be6653',
},
[ChainId.Baklava]: {
[TokenId.CELO]: '0xdDc9bE57f553fe75752D61606B94CBD7e0264eF8',
[TokenId.cUSD]: '0x62492A644A588FD904270BeD06ad52B9abfEA1aE',
[TokenId.cEUR]: '0xf9ecE301247aD2CE21894941830A2470f4E774ca',
[TokenId.cREAL]: '0x6a0EEf2bed4C30Dc2CB42fe6c5f01F80f7EF16d1',
[TokenId.axlUSDC]: '0xD4079B322c392D6b196f90AA4c439fC2C16d6770',
[TokenId.axlEUROC]: '0x6f90ac394b1F45290d3023e4Ba0203005cAF2A4B',
},
[ChainId.Celo]: {
[TokenId.CELO]: '0x471EcE3750Da237f93B8E339c536989b8978a438',
[TokenId.cUSD]: '0x765DE816845861e75A25fCA122bb6898B8B1282a',
[TokenId.cEUR]: '0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73',
[TokenId.cREAL]: '0xe8537a3d056DA446677B9E9d6c5dB704EaAb4787',
[TokenId.axlUSDC]: '0xEB466342C4d449BC9f53A865D5Cb90586f405215',
[TokenId.axlEUROC]: '',
},
})

Expand All @@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably not allow incomplete tokenAddresses right

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tokenAddresses (the token list which is used to populate the UI) isn't incomplete. The TokenAddress token to chain map above is incomplete (as there's no EUROC on main net). This is what allows us to move forward and add tokens for a chain without requiring it on all chains

.map(([tokenId]) => tokenId as TokenId)
: []
}

export function getTokenById(id: string): Token | null {
Expand Down
110 changes: 86 additions & 24 deletions src/features/swap/SwapForm.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<SwapFormValues>()

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)
Expand All @@ -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 = () => {
Expand All @@ -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 (
<div className="flex flex-col gap-3">
<TokenSelectFieldWrapper>
<div className="flex items-center ">
<TokenSelectField name="fromTokenId" label="From Token" onChange={onChangeToken(true)} />
<TokenSelectField
name="fromTokenId"
label="From Token"
tokenOptions={tokensForChain}
onChange={onChangeToken(true)}
/>
</div>
<div className="flex flex-col items-end">
{address && isConnected && isRoundedBalanceGreaterThanZero && (
Expand All @@ -146,7 +165,12 @@ function SwapFormInputs({ balances }: { balances: AccountBalances }) {
</div>
<TokenSelectFieldWrapper>
<div className="flex items-center">
<TokenSelectField name="toTokenId" label="To Token" onChange={onChangeToken(false)} />
<TokenSelectField
name="toTokenId"
label="To Token"
tokenOptions={swappableTokenOptions}
onChange={onChangeToken(false)}
/>
</div>
<AmountField quote={quote} isQuoteLoading={isLoading} direction="out" />
</TokenSelectFieldWrapper>
Expand Down Expand Up @@ -198,6 +222,7 @@ function AmountField({

return (
<input
autoComplete="off"
value={isCurrentInput ? values.amount : toSignificant(quote)}
name={`amount-${direction}`}
step="any"
Expand Down Expand Up @@ -244,15 +269,52 @@ function SlippageRow() {

function SubmitButton() {
const { address, isConnected } = useAccount()
const { chain, chains } = useNetwork()
const { switchNetworkAsync } = useSwitchNetwork()
const { openConnectModal } = useConnectModal()
const dispatch = useAppDispatch()
const { errors, touched } = useFormikContext<SwapFormValues>()

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')
nvtaveras marked this conversation as resolved.
Show resolved Hide resolved
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<SwapFormValues>()
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

Expand Down