refactor to allow switching markets

This commit is contained in:
Matthias Kretschmann 2024-04-01 13:42:08 +01:00
parent 1496238b4a
commit c75354a54d
Signed by: m
GPG Key ID: 606EEEF3C479A91F
37 changed files with 328 additions and 239 deletions

View File

@ -1,9 +1,9 @@
import styles from './page.module.css' import styles from './page.module.css'
import { Swap, Buy } from '@/components/Strategies' import { Swap, Buy } from '@/features/strategies'
import { Content } from '@/components/Content' import { Content } from '@/components/Content'
import { CalculationBase } from '@/components/CalculationBaseOutput'
import { Header } from '@/components/Header' import { Header } from '@/components/Header'
import { Footer } from '@/components/Footer' import { Footer } from '@/components/Footer'
import { MarketData } from '@/features/prices'
export default function Home() { export default function Home() {
return ( return (
@ -16,7 +16,7 @@ export default function Home() {
<Swap /> <Swap />
<Buy /> <Buy />
</div> </div>
<CalculationBase /> <MarketData />
</section> </section>
<Content /> <Content />

View File

@ -1 +0,0 @@
export * from './CalculationBase'

View File

@ -1,26 +0,0 @@
import { InputAmount } from '@/components/FormAmount/Inputs/InputAmount'
import { InputToken } from './Inputs/InputToken'
import styles from './FormAmount.module.css'
import { Dispatch, SetStateAction } from 'react'
import { TokenSymbol } from '@/types'
export function FormAmount({
amount,
setAmount,
token,
setToken,
isFiat
}: {
amount: number
setAmount: Dispatch<SetStateAction<number>>
token: TokenSymbol | string
setToken?: Dispatch<SetStateAction<TokenSymbol>>
isFiat?: boolean
}) {
return (
<form className={styles.form}>
<InputAmount amount={amount} setAmount={setAmount} />
<InputToken token={token} setToken={setToken} isFiat={isFiat} />
</form>
)
}

View File

@ -1,47 +0,0 @@
'use client'
import { Dispatch, SetStateAction } from 'react'
import styles from './InputToken.module.css'
import { CaretDownIcon } from '@radix-ui/react-icons'
import { TokenSymbol } from '@/types'
import { useSWRConfig } from 'swr'
export function InputToken({
token,
setToken,
isFiat
}: {
token: TokenSymbol | string
isFiat?: boolean
setToken?: Dispatch<SetStateAction<TokenSymbol>>
}) {
const { mutate } = useSWRConfig()
return (
<span className={styles.selectWrapper}>
<select
className={styles.select}
onChange={(e) => {
if (!setToken) return
setToken(e.target.value as TokenSymbol)
mutate('/api/quote')
}}
value={token}
disabled={!setToken}
style={setToken ? { paddingRight: '1.25rem' } : {}}
>
{isFiat ? (
<option value="USD">USD</option>
) : (
<>
<option value="OCEAN">OCEAN</option>
<option value="FET">FET</option>
<option value="AGIX">AGIX</option>
</>
)}
</select>
{setToken ? <CaretDownIcon className={styles.icon} /> : null}
</span>
)
}

View File

@ -1 +0,0 @@
export * from './ResultRow'

View File

@ -1,3 +1,7 @@
.selectWrapper {
position: relative;
}
.select { .select {
display: inline-block; display: inline-block;
all: unset; all: unset;
@ -23,10 +27,6 @@
} }
} }
.selectWrapper {
position: relative;
}
.icon { .icon {
position: absolute; position: absolute;
right: 0.1rem; right: 0.1rem;

View File

@ -0,0 +1,22 @@
import { SelectHTMLAttributes } from 'react'
import styles from './Select.module.css'
import { CaretDownIcon } from '@radix-ui/react-icons'
type Props = SelectHTMLAttributes<HTMLSelectElement> & {
options: { value: string; label: string }[]
}
export function Select({ options, ...rest }: Props) {
return (
<span className={styles.selectWrapper}>
<select className={styles.select} {...rest}>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
{options.length > 1 ? <CaretDownIcon className={styles.icon} /> : null}
</span>
)
}

View File

@ -0,0 +1 @@
export * from './Select'

View File

@ -1,66 +0,0 @@
import { Result } from '@/components/ResultRow'
import { ratioOceanToAsi, ratioAgixToAsi, ratioFetToAsi } from '@/constants'
import { usePrices } from '@/hooks'
import { getTokenBySymbol } from '@/lib/utils'
import { TokenSymbol } from '@/types'
import { useQuote } from '@/hooks'
export function SwapResults({
tokenSymbol,
amount
}: {
tokenSymbol: TokenSymbol
amount: number
}) {
const {
prices,
isValidating: isValidatingPrices,
isLoading: isLoadingPrices
} = usePrices()
const {
amountToOcean,
amountToAgix,
amountToFet,
isValidatingToAgix,
isLoadingToAgix,
isValidatingToFet,
isLoadingToFet,
isValidatingToOcean,
isLoadingToOcean
} = useQuote(tokenSymbol, amount)
return (
<>
<Result
token={getTokenBySymbol('OCEAN')}
amount={amountToOcean}
amountAsi={amountToOcean * ratioOceanToAsi}
amountFiat={amountToOcean * ratioOceanToAsi * prices.asi}
amountOriginalFiat={amountToOcean * prices.ocean}
isValidating={isValidatingToOcean || isValidatingPrices}
isLoading={isLoadingToOcean || isLoadingPrices}
/>
<Result
token={getTokenBySymbol('AGIX')}
amount={amountToAgix}
amountAsi={amountToAgix * ratioAgixToAsi}
amountFiat={amountToAgix * ratioAgixToAsi * prices.asi}
amountOriginalFiat={amountToAgix * prices.agix}
isValidating={isValidatingToAgix || isValidatingPrices}
isLoading={isLoadingToAgix || isLoadingPrices}
/>
<Result
token={getTokenBySymbol('FET')}
amount={amountToFet}
amountAsi={amountToFet * ratioFetToAsi}
amountFiat={amountToFet * prices.asi}
amountOriginalFiat={amountToFet * prices.asi}
isValidating={isValidatingToFet || isValidatingPrices}
isLoading={isLoadingToFet || isLoadingPrices}
/>
</>
)
}

View File

@ -1,2 +0,0 @@
export * from './Buy'
export * from './Swap/Swap'

View File

@ -1,4 +1,4 @@
.calculationBase { .marketData {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(235px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(235px, 1fr));
justify-content: center; justify-content: center;
@ -9,14 +9,14 @@
margin-top: 1.5rem; margin-top: 1.5rem;
} }
.calculationBase li { .marketData li {
border-bottom: 1px solid rgba(var(--foreground-rgb), 0.2); border-bottom: 1px solid rgba(var(--foreground-rgb), 0.2);
border-right: 1px solid rgba(var(--foreground-rgb), 0.2); border-right: 1px solid rgba(var(--foreground-rgb), 0.2);
padding: 1rem; padding: 1rem;
font-size: 0.9rem; font-size: 0.9rem;
} }
.calculationBase p { .marketData p {
display: flex; display: flex;
align-items: center; align-items: center;
} }

View File

@ -1,11 +1,11 @@
'use client' 'use client'
import { ratioOceanToAsi, ratioAgixToAsi, ratioFetToAsi } from '@/constants' import { ratioOceanToAsi, ratioAgixToAsi, ratioFetToAsi } from '@/constants'
import styles from './CalculationBase.module.css' import styles from './MarketData.module.css'
import { usePrices } from '@/hooks' import { usePrices } from '@/features/prices/hooks'
import { Label } from '@/components/Label' import { Label } from '@/components/Label'
export function CalculationBase() { export function MarketData() {
const { prices, isValidating, isLoading } = usePrices() const { prices, isValidating, isLoading } = usePrices()
const feedbackClasses = isLoading const feedbackClasses = isLoading
@ -15,7 +15,7 @@ export function CalculationBase() {
: '' : ''
return ( return (
<ul className={styles.calculationBase}> <ul className={styles.marketData}>
<li> <li>
<p>1 ASI</p> <p>1 ASI</p>
<p> <p>

View File

@ -0,0 +1 @@
export * from './MarketData'

View File

@ -1,2 +1 @@
export * from './use-prices' export * from './use-prices'
export * from './use-quote'

View File

@ -4,6 +4,13 @@ import useSWR from 'swr'
const tokenAddresses = tokens.map((token) => token.address).toString() const tokenAddresses = tokens.map((token) => token.address).toString()
export type Prices = {
ocean: number
fet: number
agix: number
asi: number
}
export function usePrices(): { export function usePrices(): {
prices: { ocean: number; fet: number; agix: number; asi: number } prices: { ocean: number; fet: number; agix: number; asi: number }
isValidating: boolean isValidating: boolean

View File

@ -0,0 +1,2 @@
export * from './components/MarketData'
export * from './hooks'

View File

@ -1,13 +1,12 @@
'use client' 'use client'
import { ratioOceanToAsi, ratioAgixToAsi, ratioFetToAsi } from '@/constants'
import { Result } from '@/components/ResultRow'
import { useState } from 'react' import { useState } from 'react'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import stylesShared from './styles.module.css' import { ratioOceanToAsi, ratioAgixToAsi, ratioFetToAsi } from '@/constants'
import { usePrices } from '@/hooks' import { usePrices } from '@/features/prices'
import { FormAmount } from '@/components/FormAmount'
import { getTokenBySymbol } from '@/lib/utils' import { getTokenBySymbol } from '@/lib/utils'
import { FormAmount, Result } from '@/features/strategies/components'
import stylesShared from '@/features/strategies/styles/shared.module.css'
export function Buy() { export function Buy() {
const { prices, isValidating, isLoading } = usePrices() const { prices, isValidating, isLoading } = usePrices()

View File

@ -0,0 +1,46 @@
import { InputAmount } from './Inputs/InputAmount'
import styles from './FormAmount.module.css'
import { Dispatch, SetStateAction } from 'react'
import { TokenSymbol } from '@/types'
import { Select } from '@/components/Select'
export function FormAmount({
amount,
setAmount,
token,
setToken,
isFiat
}: {
amount: number
setAmount: Dispatch<SetStateAction<number>>
token: TokenSymbol | string
setToken?: Dispatch<SetStateAction<TokenSymbol>>
isFiat?: boolean
}) {
function handleTokenChange(e: React.ChangeEvent<HTMLSelectElement>) {
if (!setToken) return
setToken(e.target.value as TokenSymbol)
}
const options = isFiat
? [{ value: 'USD', label: 'USD' }]
: [
{ value: 'OCEAN', label: 'OCEAN' },
{ value: 'FET', label: 'FET' },
{ value: 'AGIX', label: 'AGIX' }
]
return (
<form className={styles.form}>
<InputAmount amount={amount} setAmount={setAmount} />
<Select
options={options}
value={token}
onChange={handleTokenChange}
disabled={!setToken}
style={setToken ? { paddingRight: '1.25rem' } : {}}
/>
</form>
)
}

View File

@ -1,6 +1,4 @@
'use client' import { Dispatch, SetStateAction } from 'react'
import { Dispatch, SetStateAction, useRef, useState } from 'react'
import styles from './InputAmount.module.css' import styles from './InputAmount.module.css'
export function InputAmount({ export function InputAmount({

View File

@ -0,0 +1,8 @@
.form {
display: inline-flex;
border: 1px solid rgba(var(--foreground-rgb), 0.15);
border-radius: var(--border-radius);
overflow: hidden;
margin: -0.15em 0.25rem 0 0.25rem;
font-size: 0.9em;
}

View File

@ -0,0 +1,27 @@
import styles from './FormMarket.module.css'
import { Dispatch, SetStateAction } from 'react'
import { Select } from '@/components/Select'
import { type Market } from '@/features/strategies'
export function FormMarket({
market,
setMarket
}: {
market: Market
setMarket: Dispatch<SetStateAction<Market>>
}) {
const options = [
{ value: 'market', label: 'All Markets' },
{ value: 'uniswap-v3', label: 'Uniswap v3' }
]
return (
<form className={styles.form}>
<Select
options={options}
value={market}
onChange={(e) => setMarket(e.target.value as Market)}
style={{ paddingRight: '1.25rem' }}
/>
</form>
)
}

View File

@ -0,0 +1 @@
export * from './FormMarket'

View File

@ -1,7 +1,7 @@
import styles from './ResultRow.module.css' import styles from './Result.module.css'
import { formatNumber } from '@/lib/utils' import { formatNumber } from '@/lib/utils'
import { ArrowRightIcon } from '@radix-ui/react-icons' import { ArrowRightIcon } from '@radix-ui/react-icons'
import { TokenLogo } from '../TokenLogo/TokenLogo' import { TokenLogo } from '../../../../components/TokenLogo/TokenLogo'
import { Token } from '@/types' import { Token } from '@/types'
type Props = { type Props = {

View File

@ -0,0 +1 @@
export * from './Result'

View File

@ -0,0 +1,81 @@
import { ratioOceanToAsi, ratioAgixToAsi, ratioFetToAsi } from '@/constants'
import { usePrices, type Prices } from '@/features/prices'
import { getTokenBySymbol } from '@/lib/utils'
import { TokenSymbol } from '@/types'
import { useQuote, type Market } from '@/features/strategies'
import { Result } from '../Result'
export function SwapResults({
tokenSymbol,
amount,
market
}: {
tokenSymbol: TokenSymbol
amount: number
market: Market
}) {
const isUniswap = market === 'uniswap-v3'
const {
prices,
isValidating: isValidatingPrices,
isLoading: isLoadingPrices
} = usePrices()
const {
amountToOcean: amountToOceanUniswap,
amountToAgix: amountToAgixUniswap,
amountToFet: amountToFetUniswap,
isValidatingToAgix,
isLoadingToAgix,
isValidatingToFet,
isLoadingToFet,
isValidatingToOcean,
isLoadingToOcean
} = useQuote(tokenSymbol, amount, isUniswap)
const amountInUsd = amount * prices[tokenSymbol.toLowerCase() as keyof Prices]
const amountToOcean = amountInUsd / prices.ocean
const amountToAgix = amountInUsd / prices.agix
const amountToFet = amountInUsd / prices.fet
return (
<>
<Result
token={getTokenBySymbol('OCEAN')}
amount={amountToOceanUniswap || amountToOcean}
amountAsi={(amountToOceanUniswap || amountToOcean) * ratioOceanToAsi}
amountFiat={
(amountToOceanUniswap || amountToOcean) * ratioOceanToAsi * prices.asi
}
amountOriginalFiat={
(amountToOceanUniswap || amountToOcean) * prices.ocean
}
isValidating={isValidatingToOcean || isValidatingPrices}
isLoading={isLoadingToOcean || isLoadingPrices}
/>
<Result
token={getTokenBySymbol('AGIX')}
amount={amountToAgixUniswap || amountToAgix}
amountAsi={(amountToAgixUniswap || amountToAgix) * ratioAgixToAsi}
amountFiat={
(amountToAgixUniswap || amountToAgix) * ratioAgixToAsi * prices.asi
}
amountOriginalFiat={(amountToAgixUniswap || amountToAgix) * prices.agix}
isValidating={isValidatingToAgix || isValidatingPrices}
isLoading={isLoadingToAgix || isLoadingPrices}
/>
<Result
token={getTokenBySymbol('FET')}
amount={amountToFetUniswap || amountToFet}
amountAsi={(amountToFetUniswap || amountToFet) * ratioFetToAsi}
amountFiat={(amountToFetUniswap || amountToFet) * prices.asi}
amountOriginalFiat={(amountToFetUniswap || amountToFet) * prices.asi}
isValidating={isValidatingToFet || isValidatingPrices}
isLoading={isLoadingToFet || isLoadingPrices}
/>
</>
)
}

View File

@ -1,16 +1,18 @@
'use client' 'use client'
import stylesShared from '../styles.module.css'
import { useState } from 'react' import { useState } from 'react'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { FormAmount } from '@/components/FormAmount'
import { SwapResults } from './Results' import { SwapResults } from './Results'
import { TokenSymbol } from '@/types' import { TokenSymbol } from '@/types'
import { FormAmount, FormMarket } from '@/features/strategies/components'
import stylesShared from '@/features/strategies/styles/shared.module.css'
import { type Market } from '@/features/strategies'
export function Swap() { export function Swap() {
const [amount, setAmount] = useState(100) const [amount, setAmount] = useState(100)
const [debouncedAmount] = useDebounce(amount, 500) const [debouncedAmount] = useDebounce(amount, 500)
const [tokenSymbol, setTokenSymbol] = useState<TokenSymbol>('OCEAN') const [tokenSymbol, setTokenSymbol] = useState<TokenSymbol>('OCEAN')
const [market, setMarket] = useState<Market>('all')
return ( return (
<div className={stylesShared.results}> <div className={stylesShared.results}>
@ -22,10 +24,15 @@ export function Swap() {
setAmount={setAmount} setAmount={setAmount}
setToken={setTokenSymbol} setToken={setTokenSymbol}
/>{' '} />{' '}
on Uniswap right now gets you: on <FormMarket market={market} setMarket={setMarket} /> right now gets
you:
</h3> </h3>
<SwapResults tokenSymbol={tokenSymbol} amount={debouncedAmount} /> <SwapResults
tokenSymbol={tokenSymbol}
amount={debouncedAmount}
market={market}
/>
</div> </div>
) )
} }

View File

@ -0,0 +1,3 @@
export * from './FormAmount'
export * from './FormMarket'
export * from './Result'

View File

@ -0,0 +1 @@
export * from './use-quote'

View File

@ -0,0 +1,89 @@
import { TokenSymbol } from '@/types'
import { getTokenAddressBySymbol, fetcher } from '@/lib/utils'
import useSWR from 'swr'
const options = {
keepPreviousData: true // so loading UI can kick in properly
}
export function useQuote(
tokenSymbol: TokenSymbol,
amount: number,
shouldFetch: boolean
) {
// -> AGIX
const {
data: dataSwapToAgix,
isValidating: isValidatingToAgix,
isLoading: isLoadingToAgix
} = useSWR(
shouldFetch
? `/api/quote/?tokenIn=${getTokenAddressBySymbol(
tokenSymbol
)}&tokenOut=${getTokenAddressBySymbol('AGIX')}&amountIn=${amount}`
: null,
fetcher,
options
)
// -> FET
const {
data: dataSwapToFet,
isValidating: isValidatingToFet,
isLoading: isLoadingToFet
} = useSWR(
shouldFetch
? `/api/quote/?tokenIn=${getTokenAddressBySymbol(
tokenSymbol
)}&tokenOut=${getTokenAddressBySymbol('FET')}&amountIn=${amount}`
: null,
fetcher,
options
)
// -> OCEAN
const {
data: dataSwapToOcean,
isValidating: isValidatingToOcean,
isLoading: isLoadingToOcean
} = useSWR(
shouldFetch
? `/api/quote/?tokenIn=${getTokenAddressBySymbol(
tokenSymbol
)}&tokenOut=${getTokenAddressBySymbol('OCEAN')}&amountIn=${amount}`
: null,
fetcher,
options
)
const amountToOcean =
dataSwapToOcean?.amountOut / Number(`1e${dataSwapToOcean?.decimals}`)
const amountToAgix =
dataSwapToAgix?.amountOut / Number(`1e${dataSwapToAgix?.decimals}`)
const amountToFet =
dataSwapToFet?.amountOut / Number(`1e${dataSwapToFet?.decimals}`)
return shouldFetch
? {
amountToOcean,
amountToAgix,
amountToFet,
isValidatingToAgix,
isLoadingToAgix,
isValidatingToFet,
isLoadingToFet,
isValidatingToOcean,
isLoadingToOcean
}
: {
amountToOcean: undefined,
amountToAgix: undefined,
amountToFet: undefined,
isValidatingToAgix: false,
isLoadingToAgix: false,
isValidatingToFet: false,
isLoadingToFet: false,
isValidatingToOcean: false,
isLoadingToOcean: false
}
}

View File

@ -0,0 +1,4 @@
export * from './components/Buy'
export * from './components/Swap/Swap'
export * from './hooks'
export * from './types'

View File

@ -9,4 +9,5 @@
font-size: 1.2rem; font-size: 1.2rem;
color: rgb(var(--foreground-rgb-highlight)); color: rgb(var(--foreground-rgb-highlight));
min-height: 58px; min-height: 58px;
line-height: 1.5;
} }

View File

@ -0,0 +1 @@
export type Market = 'all' | 'uniswap-v3'

View File

@ -1,67 +0,0 @@
import { TokenSymbol } from '@/types'
import { getTokenAddressBySymbol, fetcher } from '@/lib/utils'
import useSWR from 'swr'
const options = {
keepPreviousData: true // so loading UI can kick in properly
}
export function useQuote(tokenSymbol: TokenSymbol, amount: number) {
// -> AGIX
const {
data: dataSwapToAgix,
isValidating: isValidatingToAgix,
isLoading: isLoadingToAgix
} = useSWR(
`/api/quote/?tokenIn=${getTokenAddressBySymbol(
tokenSymbol
)}&tokenOut=${getTokenAddressBySymbol('AGIX')}&amountIn=${amount}`,
fetcher,
options
)
// -> FET
const {
data: dataSwapToFet,
isValidating: isValidatingToFet,
isLoading: isLoadingToFet
} = useSWR(
`/api/quote/?tokenIn=${getTokenAddressBySymbol(
tokenSymbol
)}&tokenOut=${getTokenAddressBySymbol('FET')}&amountIn=${amount}`,
fetcher,
options
)
// -> OCEAN
const {
data: dataSwapToOcean,
isValidating: isValidatingToOcean,
isLoading: isLoadingToOcean
} = useSWR(
`/api/quote/?tokenIn=${getTokenAddressBySymbol(
tokenSymbol
)}&tokenOut=${getTokenAddressBySymbol('OCEAN')}&amountIn=${amount}`,
fetcher,
options
)
const amountToOcean =
dataSwapToOcean?.amountOut / Number(`1e${dataSwapToOcean?.decimals}`)
const amountToAgix =
dataSwapToAgix?.amountOut / Number(`1e${dataSwapToAgix?.decimals}`)
const amountToFet =
dataSwapToFet?.amountOut / Number(`1e${dataSwapToFet?.decimals}`)
return {
amountToOcean,
amountToAgix,
amountToFet,
isValidatingToAgix,
isLoadingToAgix,
isValidatingToFet,
isLoadingToFet,
isValidatingToOcean,
isLoadingToOcean
}
}