mars-v2-frontend/components/Trade/TradeActionModule.tsx
Gustavo Mauricio 8aaeb36efa
update balance table when performing an action (#56)
* update balance table when performing an action

* filter assets with no amount as a result of an action

* feat: slippage tolerance input added

* update default slippage to 1%
2022-12-05 15:14:58 +01:00

352 lines
12 KiB
TypeScript

import { Switch } from '@headlessui/react'
import BigNumber from 'bignumber.js'
import React, { useEffect, useMemo, useState } from 'react'
import { toast } from 'react-toastify'
import Button from 'components/Button'
import CircularProgress from 'components/CircularProgress'
import ArrowsUpDownIcon from 'components/Icons/arrows-up-down.svg'
import Slider from 'components/Slider'
import useTradeAsset from 'hooks/mutations/useTradeAsset'
import useAllBalances from 'hooks/useAllBalances'
import useAllowedCoins from 'hooks/useAllowedCoins'
import useCalculateMaxTradeAmount from 'hooks/useCalculateMaxTradeAmount'
import useCreditAccountPositions from 'hooks/useCreditAccountPositions'
import useMarkets from 'hooks/useMarkets'
import useTokenPrices from 'hooks/useTokenPrices'
import useCreditManagerStore from 'stores/useCreditManagerStore'
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
enum FundingMode {
Account = 'Account',
WalletAndAccount = 'WalletAndAccount',
}
const TradeActionModule = () => {
const [selectedTokenIn, setSelectedTokenIn] = useState('')
const [selectedTokenOut, setSelectedTokenOut] = useState('')
const [amountIn, setAmountIn] = useState(0)
const [amountOut, setAmountOut] = useState(0)
const [slippageTolerance, setSlippageTolerance] = useState(1)
const [fundingMode, setFundingMode] = useState<FundingMode>(FundingMode.WalletAndAccount)
const [isMarginEnabled, setIsMarginEnabled] = React.useState(false)
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
const { data: allowedCoinsData } = useAllowedCoins()
const { data: balancesData } = useAllBalances()
const { data: marketsData } = useMarkets()
const { data: tokenPrices } = useTokenPrices()
const { data: positionsData } = useCreditAccountPositions(selectedAccount ?? '')
const resetAmounts = () => {
setAmountIn(0)
setAmountOut(0)
}
useEffect(() => {
resetAmounts()
}, [selectedAccount])
const accountAmount = useMemo(() => {
return Number(positionsData?.coins?.find((coin) => coin.denom === selectedTokenIn)?.amount ?? 0)
}, [positionsData, selectedTokenIn])
const walletAmount = useMemo(() => {
return Number(balancesData?.find((balance) => balance.denom === selectedTokenIn)?.amount ?? 0)
}, [balancesData, selectedTokenIn])
const { swapAmount, borrowAmount, depositAmount } = useMemo(() => {
const swapAmount = amountIn
let borrowAmount = 0
let depositAmount = 0
if (fundingMode === FundingMode.WalletAndAccount) {
const walletAndAccountAmount = walletAmount + accountAmount
borrowAmount =
amountIn > walletAndAccountAmount
? BigNumber(amountIn).minus(walletAndAccountAmount).toNumber()
: 0
depositAmount = amountIn > walletAmount ? walletAmount : amountIn
}
if (fundingMode === FundingMode.Account) {
borrowAmount =
amountIn > accountAmount ? BigNumber(amountIn).minus(accountAmount).toNumber() : 0
}
return { swapAmount, borrowAmount, depositAmount }
}, [accountAmount, amountIn, fundingMode, walletAmount])
const { mutate, isLoading } = useTradeAsset(
swapAmount,
borrowAmount,
depositAmount,
selectedTokenIn,
selectedTokenOut,
slippageTolerance / 100,
{
onSuccess: () => {
toast.success(
`${amountIn} ${getTokenSymbol(selectedTokenIn)} swapped for ${amountOut} ${getTokenSymbol(
selectedTokenOut,
)}`,
)
resetAmounts()
},
},
)
useEffect(() => {
if (allowedCoinsData && allowedCoinsData.length > 0) {
// initialize selected token when allowedCoins fetch data is available
setSelectedTokenIn(allowedCoinsData[0])
if (allowedCoinsData.length > 1) {
setSelectedTokenOut(allowedCoinsData[1])
} else {
setSelectedTokenOut(allowedCoinsData[0])
}
}
}, [allowedCoinsData])
const handleSelectedTokenInChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedTokenIn(e.target.value)
resetAmounts()
}
const handleSelectedTokenOutChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedTokenOut(e.target.value)
resetAmounts()
}
const handleFundingModeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFundingMode(e.target.value as FundingMode)
resetAmounts()
}
// max amount that can be traded without considering wallet amount
// wallet amount should just be directly added to this amount in case user wants to include wallet as funding source
const maxTradeAmount = useCalculateMaxTradeAmount(
selectedTokenIn,
selectedTokenOut,
isMarginEnabled,
)
// if funding from wallet & account, add wallet amount to the max trade amount
const maxAmount = useMemo(() => {
if (fundingMode === FundingMode.WalletAndAccount) {
return walletAmount + maxTradeAmount
}
return maxTradeAmount
}, [fundingMode, maxTradeAmount, walletAmount])
const percentageValue = useMemo(() => {
if (isNaN(amountIn) || amountIn === 0) return 0
return amountIn / maxAmount > 1 ? 100 : (amountIn / maxAmount) * 100
}, [amountIn, maxAmount])
const borrowRate = Number(marketsData?.[selectedTokenOut]?.borrow_rate)
const handleAmountChange = (value: number, mode: 'in' | 'out') => {
const tokenInPrice = tokenPrices?.[selectedTokenIn] ?? 1
const tokenOutPrice = tokenPrices?.[selectedTokenOut] ?? 1
const priceRatio = BigNumber(tokenInPrice).div(tokenOutPrice)
if (mode === 'in') {
setAmountIn(value)
setAmountOut(BigNumber(value).times(priceRatio).decimalPlaces(0).toNumber())
} else {
setAmountOut(value)
setAmountIn(BigNumber(value).div(BigNumber(1).times(priceRatio)).decimalPlaces(0).toNumber())
}
}
const submitDisabled = selectedTokenIn === selectedTokenOut || !amountIn || amountIn > maxAmount
return (
<div>
{isLoading && (
<div className='fixed inset-0 z-40 grid place-items-center bg-black/50'>
<CircularProgress />
</div>
)}
<div className='border-b border-b-white/20 p-2'>
<div className='mb-2'>
<p className='mb-1'>From:</p>
<div className='flex gap-2'>
<select
className='h-8 w-20 text-black'
onChange={handleSelectedTokenInChange}
value={selectedTokenIn}
>
{allowedCoinsData?.map((entry) => (
<option key={entry} value={entry}>
{getTokenSymbol(entry)}
</option>
))}
</select>
<input
type='number'
className='h-8 flex-1 px-2 text-black outline-0'
value={amountIn / 10 ** getTokenDecimals(selectedTokenIn)}
min='0'
placeholder='0.00'
onChange={(e) => {
const valueAsNumber = e.target.valueAsNumber
const valueWithDecimals = valueAsNumber * 10 ** getTokenDecimals(selectedTokenIn)
handleAmountChange(valueWithDecimals, 'in')
}}
/>
</div>
</div>
<div
className='mx-auto h-5 w-6 cursor-pointer text-white/70 hover:text-white'
onClick={() => {
setSelectedTokenIn(selectedTokenOut)
setSelectedTokenOut(selectedTokenIn)
resetAmounts()
}}
>
<ArrowsUpDownIcon />
</div>
<div className='mb-5'>
<p className='mb-1'>To:</p>
<div className='flex gap-2'>
<select
className='h-8 w-20 text-black'
onChange={handleSelectedTokenOutChange}
value={selectedTokenOut}
>
{allowedCoinsData?.map((entry) => (
<option key={entry} value={entry}>
{getTokenSymbol(entry)}
</option>
))}
</select>
<input
type='number'
className='h-8 flex-1 px-2 text-black outline-0'
value={amountOut / 10 ** getTokenDecimals(selectedTokenOut)}
min='0'
placeholder='0.00'
onChange={(e) => {
const valueAsNumber = e.target.valueAsNumber
const valueWithDecimals = valueAsNumber * 10 ** getTokenDecimals(selectedTokenOut)
handleAmountChange(valueWithDecimals, 'out')
}}
/>
</div>
</div>
<div className='mb-1'>
In Wallet:{' '}
{BigNumber(walletAmount)
.dividedBy(10 ** getTokenDecimals(selectedTokenIn))
.toNumber()
.toLocaleString(undefined, {
maximumFractionDigits: getTokenDecimals(selectedTokenIn),
})}{' '}
<span>{getTokenSymbol(selectedTokenIn)}</span>
</div>
<div className='mb-4'>
In Account:{' '}
{BigNumber(accountAmount)
.dividedBy(10 ** getTokenDecimals(selectedTokenIn))
.toNumber()
.toLocaleString(undefined, {
maximumFractionDigits: getTokenDecimals(selectedTokenIn),
})}{' '}
<span>{getTokenSymbol(selectedTokenIn)}</span>
</div>
<Slider
className='mb-6'
value={percentageValue}
onChange={(value) => {
const decimal = value[0] / 100
const tokenDecimals = getTokenDecimals(selectedTokenIn)
// limit decimal precision based on token contract decimals
const newAmount = Number((decimal * maxAmount).toFixed(0))
handleAmountChange(newAmount, 'in')
}}
onMaxClick={() => handleAmountChange(maxAmount, 'in')}
/>
</div>
<div className='border-b border-b-white/20 p-2'>
<div className='mb-4 flex items-center'>
<p className='mr-2'>Margin</p>
<Switch
checked={isMarginEnabled}
onChange={(value: boolean) => {
// reset amounts only if margin is turned off
if (!value) resetAmounts()
setIsMarginEnabled(value)
}}
className={`${
isMarginEnabled ? 'bg-[#524BB1]' : 'bg-gray-400'
} relative inline-flex h-4 w-8 items-center rounded-full`}
>
<span
className={`${
isMarginEnabled ? 'translate-x-4' : ''
} inline-block h-4 w-4 transform rounded-full bg-white transition`}
/>
</Switch>
</div>
<div className='mb-1 flex justify-between'>
<p>Borrow</p>
<p>
{isMarginEnabled
? BigNumber(borrowAmount)
.dividedBy(10 ** getTokenDecimals(selectedTokenIn))
.toNumber()
.toLocaleString(undefined, {
maximumFractionDigits: getTokenDecimals(selectedTokenIn),
})
: '-'}
</p>
</div>
<div className='flex justify-between'>
<p>Borrow Rate</p>
<p>{isMarginEnabled ? `${(borrowRate * 100).toFixed(2)}%` : '-'}</p>
</div>
</div>
<div className='p-2'>
<div className='mb-6'>OTHER INFO PLACEHOLDER</div>
<div className='mb-2 flex justify-between'>
<p>Slippage Tolerance:</p>
<input
type='number'
step='0.1'
className='w-20 px-2 text-black'
onChange={(e) => setSlippageTolerance(e.target.valueAsNumber)}
value={slippageTolerance}
/>
</div>
<div className='flex justify-between'>
<p>Funded From</p>
<select value={fundingMode} className='text-black' onChange={handleFundingModeChange}>
<option value={FundingMode.Account}>Account</option>
<option value={FundingMode.WalletAndAccount}>Wallet & Account</option>
</select>
</div>
</div>
<Button className='w-full' onClick={() => mutate()} disabled={submitDisabled}>
Create Order
</Button>
</div>
)
}
export default TradeActionModule