Skip to content

Commit

Permalink
feat: add mu03 support (#99)
Browse files Browse the repository at this point in the history
  • Loading branch information
denviljclarke authored Sep 12, 2023
1 parent 008d885 commit 358bcd8
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 36 deletions.
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
.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')
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

1 comment on commit 358bcd8

@vercel
Copy link

@vercel vercel bot commented on 358bcd8 Sep 12, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.