Merge pull request #2 from kremalicious/simplify

Allow market selection
This commit is contained in:
Matthias Kretschmann 2024-04-01 14:29:08 +01:00 committed by GitHub
commit fd7bbe224a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 380 additions and 284 deletions

View File

@ -3,16 +3,14 @@ import { Hanken_Grotesk } from 'next/font/google'
import '@/styles/globals.css'
import '@/styles/loading-ui.css'
import Script from 'next/script'
import { title, description } from '@/constants'
const hankenGrotesk = Hanken_Grotesk({
subsets: ['latin'],
variable: '--font-hanken-grotesk'
})
export const metadata: Metadata = {
title: 'ASI Calculator',
description: 'See how much ASI you get for your OCEAN, AGIX, or FET.'
}
export const metadata: Metadata = { title, description }
export default function RootLayout({
children

View File

@ -1,9 +1,7 @@
import styles from './page.module.css'
import { Swap, Buy } from '@/components/Strategies'
import { Content } from '@/components/Content'
import { CalculationBase } from '@/components/CalculationBaseOutput'
import { Header } from '@/components/Header'
import { Footer } from '@/components/Footer'
import { Swap, Buy } from '@/features/strategies'
import { MarketData } from '@/features/prices'
import { Content, Footer, Header } from '@/components'
export default function Home() {
return (
@ -16,7 +14,7 @@ export default function Home() {
<Swap />
<Buy />
</div>
<CalculationBase />
<MarketData />
</section>
<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,36 +0,0 @@
'use client'
import { Dispatch, SetStateAction, useRef, useState } from 'react'
import styles from './InputAmount.module.css'
export function InputAmount({
amount,
setAmount
}: {
amount: number
setAmount: Dispatch<SetStateAction<number>>
}) {
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const { value } = e.target
if (value === '') {
setAmount(0)
} else {
setAmount(Number(value))
}
}
return (
<input
className={styles.input}
type="text"
inputMode="numeric"
pattern="[0-9]*"
value={amount}
onChange={handleChange}
style={{
width: Math.min(Math.max(amount.toString().length, 2), 50) + 'ch'
}}
/>
)
}

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,11 +1,11 @@
import { metadata } from '@/app/layout'
import { title, description } from '@/constants'
import styles from './Header.module.css'
export function Header() {
return (
<header>
<h1 className={styles.title}>{`${metadata.title}`}</h1>
<p className={styles.description}>{`${metadata.description}`}</p>
<h1 className={styles.title}>{`${title}`}</h1>
<p className={styles.description}>{`${description}`}</p>
</header>
)
}

View File

@ -0,0 +1,8 @@
import { InputHTMLAttributes } from 'react'
import styles from './Input.module.css'
type Props = InputHTMLAttributes<HTMLInputElement>
export function Input(props: Props) {
return <input className={styles.input} {...props} />
}

View File

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

View File

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

View File

@ -1,3 +1,7 @@
.selectWrapper {
position: relative;
}
.select {
display: inline-block;
all: unset;
@ -23,10 +27,6 @@
}
}
.selectWrapper {
position: relative;
}
.icon {
position: absolute;
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'

7
components/index.tsx Normal file
View File

@ -0,0 +1,7 @@
export * from './Content'
export * from './Footer'
export * from './Header'
export * from './Input'
export * from './Label'
export * from './Select'
export * from './TokenLogo'

View File

@ -1,5 +1,9 @@
import { Token } from '@/types'
export const title = 'ASI Calculator'
export const description =
'See how much ASI you get for your OCEAN, AGIX, or FET.'
export const ratioOceanToAsi = 0.433226
export const ratioAgixToAsi = 0.43335
export const ratioFetToAsi = 1

View File

@ -1,4 +1,6 @@
The **→ lines** show what you would get with the displayed token amount at the moment of the ASI swap, along with the converted value based on the current market price of FET. The fiat values are fetched from [Coingecko](https://coingecko.com), and the token swap estimations directly from [Uniswap](https://uniswap.org) v3 swap routes.
The **→ lines** show what you would get with the displayed token amount at the moment of the ASI swap, along with the converted value based on the current market price of FET.
The fiat values are fetched from [Coingecko](https://coingecko.com), and the token swap estimations directly from [Uniswap](https://uniswap.org) v3 swap routes.
All displayed values should be seen as estimates. Except for the [fixed ASI exchange rate](https://blog.oceanprotocol.com/ocean-protocol-is-joining-the-superintelligence-alliance-767c82693f24#3c8e), all other values are constantly changing based on market conditions. There is no guarantee the displayed values reflect the value of your investment once the actual ASI swap mechanism is released. Use at your own risk.

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,18 @@
'use client'
import { tokens } from '@/constants'
import { fetcher, getTokenAddressBySymbol } from '@/lib/utils'
import useSWR from 'swr'
const tokenAddresses = tokens.map((token) => token.address).toString()
export type Prices = {
ocean: number
fet: number
agix: number
asi: number
}
export function usePrices(): {
prices: { ocean: number; fet: number; agix: number; asi: number }
isValidating: boolean

View File

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

View File

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

View File

@ -0,0 +1,64 @@
import styles from './FormAmount.module.css'
import { Dispatch, SetStateAction } from 'react'
import { TokenSymbol } from '@/types'
import { Select, Input } from '@/components'
export function FormAmount({
amount,
setAmount,
token,
setToken,
isFiat
}: {
amount: number
setAmount: Dispatch<SetStateAction<number>>
token: TokenSymbol | string
setToken?: Dispatch<SetStateAction<TokenSymbol>>
isFiat?: boolean
}) {
function handleAmountChange(e: React.ChangeEvent<HTMLInputElement>) {
const { value } = e.target
if (value === '') {
setAmount(0)
} else {
setAmount(Number(value))
}
}
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}>
<Input
type="text"
inputMode="numeric"
pattern="[0-9]*"
value={amount}
onChange={handleAmountChange}
style={{
width: Math.min(Math.max(amount.toString().length, 2), 50) + 'ch'
}}
/>
<Select
options={options}
value={token}
onChange={handleTokenChange}
disabled={!setToken}
style={setToken ? { paddingRight: '1.25rem' } : {}}
/>
</form>
)
}

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'
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 { ArrowRightIcon } from '@radix-ui/react-icons'
import { TokenLogo } from '../TokenLogo/TokenLogo'
import { TokenLogo } from '@/components'
import { Token } from '@/types'
type Props = {

View File

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

View File

@ -0,0 +1,81 @@
import { ratioOceanToAsi, ratioAgixToAsi, ratioFetToAsi } from '@/constants'
import { getTokenBySymbol } from '@/lib/utils'
import { type TokenSymbol } from '@/types'
import { usePrices, type Prices } from '@/features/prices'
import { type Market, useQuote } 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'
import stylesShared from '../styles.module.css'
import { useState } from 'react'
import { useDebounce } from 'use-debounce'
import { FormAmount } from '@/components/FormAmount'
import { SwapResults } from './Results'
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() {
const [amount, setAmount] = useState(100)
const [debouncedAmount] = useDebounce(amount, 500)
const [tokenSymbol, setTokenSymbol] = useState<TokenSymbol>('OCEAN')
const [market, setMarket] = useState<Market>('all')
return (
<div className={stylesShared.results}>
@ -22,10 +24,15 @@ export function Swap() {
setAmount={setAmount}
setToken={setTokenSymbol}
/>{' '}
on Uniswap right now gets you:
on <FormMarket market={market} setMarket={setMarket} /> right now gets
you:
</h3>
<SwapResults tokenSymbol={tokenSymbol} amount={debouncedAmount} />
<SwapResults
tokenSymbol={tokenSymbol}
amount={debouncedAmount}
market={market}
/>
</div>
)
}

View File

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

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,91 @@
'use client'
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'
export * from './hooks'
export * from './types'

View File

@ -9,4 +9,5 @@
font-size: 1.2rem;
color: rgb(var(--foreground-rgb-highlight));
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
}
}