* upgrade to next 13 * WIP: adjust to app dir * add docker + wallet connector * fix: update the wallet connect component * tidy: format * wip: make the wallet balance fetcher work * fix balance retrieval * MP-2258: added estimateFee hook (#94) * Mp 2259 queries to api (#96) * update next config for build errors * Convert queries to API + remove config * tidy: save some bytes by adding constants/env.ts * tidy: added URL_ prefix to REST, RPC and GQL --------- Co-authored-by: Linkie Link <linkielink.dev@gmail.com> * MP-2261: created useBroadcast hook for transactions (#95) * tidy: remove unneeded wallet images * Mp 2264 convert store (#97) * Merge stores into 1 * refactor codebase to use new store * fiex build and rename whitelisted to marketassets * tidy: import refactor * updated account navigation basics * feat: added loading component and fixed the disconnect button * fix: format * update new routing system * update config and dependencies * feat: create and delete credit account are restored * tidy: format * fix: fixed the deployment * update route structure (#98) * fix: creditAccountDeposit works again * fix: bugfixes * add apis, remove allowedCoins, get basic borrow tables (#99) Co-authored-by: bwvdhelm <34470358+bobthebuidlr@users.noreply.github.com> --------- Co-authored-by: bwvdhelm <34470358+bobthebuidlr@users.noreply.github.com>
314 lines
13 KiB
TypeScript
314 lines
13 KiB
TypeScript
import { Dialog, Switch, Transition } from '@headlessui/react'
|
|
import BigNumber from 'bignumber.js'
|
|
import React, { useMemo, useState } from 'react'
|
|
import { NumericFormat } from 'react-number-format'
|
|
import { toast } from 'react-toastify'
|
|
|
|
import { Button } from 'components/Button'
|
|
import { CircularProgress } from 'components/CircularProgress'
|
|
import { ContainerSecondary } from 'components/ContainerSecondary'
|
|
import { Gauge } from 'components/Gauge'
|
|
import { PositionsList } from 'components/PositionsList'
|
|
import { ProgressBar } from 'components/ProgressBar'
|
|
import { Slider } from 'components/Slider'
|
|
import { Text } from 'components/Text'
|
|
import { Tooltip } from 'components/Tooltip'
|
|
import { useAccountStats } from 'hooks/data/useAccountStats'
|
|
import { useBalances } from 'hooks/data/useBalances'
|
|
import { useCalculateMaxBorrowAmount } from 'hooks/data/useCalculateMaxBorrowAmount'
|
|
import { useBorrowFunds } from 'hooks/mutations/useBorrowFunds'
|
|
import { useAllBalances } from 'hooks/queries/useAllBalances'
|
|
import { useMarkets } from 'hooks/queries/useMarkets'
|
|
import { useTokenPrices } from 'hooks/queries/useTokenPrices'
|
|
import { formatCurrency, formatValue } from 'utils/formatters'
|
|
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
|
import useStore from 'store'
|
|
import { getBaseAsset, getMarketAssets } from 'utils/assets'
|
|
|
|
type Props = {
|
|
show: boolean
|
|
onClose: () => void
|
|
tokenDenom: string
|
|
}
|
|
|
|
export const BorrowModal = ({ show, onClose, tokenDenom }: Props) => {
|
|
const [amount, setAmount] = useState(0)
|
|
const [isBorrowToCreditAccount, setIsBorrowToCreditAccount] = useState(false)
|
|
|
|
const selectedAccount = useStore((s) => s.selectedAccount)
|
|
const marketAssets = getMarketAssets()
|
|
const baseAsset = getBaseAsset()
|
|
|
|
const balances = useBalances()
|
|
|
|
const { actions, borrowAmount } = useMemo(() => {
|
|
const borrowAmount = BigNumber(amount)
|
|
.times(10 ** getTokenDecimals(tokenDenom, marketAssets))
|
|
.toNumber()
|
|
|
|
const withdrawAmount = isBorrowToCreditAccount ? 0 : borrowAmount
|
|
|
|
return {
|
|
borrowAmount,
|
|
withdrawAmount,
|
|
actions: [
|
|
{
|
|
type: 'borrow',
|
|
amount: borrowAmount,
|
|
denom: tokenDenom,
|
|
},
|
|
{
|
|
type: 'withdraw',
|
|
amount: withdrawAmount,
|
|
denom: tokenDenom,
|
|
},
|
|
] as AccountStatsAction[],
|
|
}
|
|
}, [amount, isBorrowToCreditAccount, tokenDenom, marketAssets])
|
|
|
|
const accountStats = useAccountStats(actions)
|
|
|
|
const tokenSymbol = getTokenSymbol(tokenDenom, marketAssets)
|
|
|
|
const { mutate, isLoading } = useBorrowFunds(borrowAmount, tokenDenom, !isBorrowToCreditAccount, {
|
|
onSuccess: () => {
|
|
onClose()
|
|
toast.success(`${amount} ${tokenSymbol} successfully Borrowed`)
|
|
},
|
|
})
|
|
|
|
const { data: tokenPrices } = useTokenPrices()
|
|
const { data: balancesData } = useAllBalances()
|
|
const { data: marketsData } = useMarkets()
|
|
|
|
const handleSubmit = () => {
|
|
mutate()
|
|
}
|
|
|
|
const walletAmount = useMemo(() => {
|
|
return BigNumber(balancesData?.find((balance) => balance.denom === tokenDenom)?.amount ?? 0)
|
|
.div(10 ** getTokenDecimals(tokenDenom, marketAssets))
|
|
.toNumber()
|
|
}, [balancesData, tokenDenom, marketAssets])
|
|
|
|
const tokenPrice = tokenPrices?.[tokenDenom] ?? 0
|
|
const borrowRate = Number(marketsData?.[tokenDenom]?.borrow_rate)
|
|
|
|
const maxValue = useCalculateMaxBorrowAmount(tokenDenom, isBorrowToCreditAccount)
|
|
|
|
const percentageValue = useMemo(() => {
|
|
if (isNaN(amount) || maxValue === 0) return 0
|
|
|
|
return (amount * 100) / maxValue
|
|
}, [amount, maxValue])
|
|
|
|
const handleValueChange = (value: number) => {
|
|
if (value > maxValue) {
|
|
setAmount(maxValue)
|
|
return
|
|
}
|
|
|
|
setAmount(value)
|
|
}
|
|
|
|
const handleSliderValueChange = (value: number[]) => {
|
|
const decimal = value[0] / 100
|
|
const tokenDecimals = getTokenDecimals(tokenDenom, marketAssets)
|
|
// limit decimal precision based on token contract decimals
|
|
const newAmount = Number((decimal * maxValue).toFixed(tokenDecimals))
|
|
|
|
setAmount(newAmount)
|
|
}
|
|
|
|
const handleBorrowTargetChange = () => {
|
|
setIsBorrowToCreditAccount((c) => !c)
|
|
// reset amount due to max value calculations changing depending on borrow target
|
|
setAmount(0)
|
|
}
|
|
|
|
return (
|
|
<Transition appear show={show} as={React.Fragment}>
|
|
<Dialog as='div' className='relative z-10' onClose={onClose}>
|
|
<Transition.Child
|
|
as={React.Fragment}
|
|
enter='ease-out duration-300'
|
|
enterFrom='opacity-0'
|
|
enterTo='opacity-100'
|
|
leave='ease-in duration-200'
|
|
leaveFrom='opacity-100'
|
|
leaveTo='opacity-0'
|
|
>
|
|
<div className='fixed inset-0 bg-black bg-opacity-80' />
|
|
</Transition.Child>
|
|
|
|
<div className='fixed inset-0 overflow-y-auto'>
|
|
<div className='flex min-h-full items-center justify-center p-4'>
|
|
<Transition.Child
|
|
as={React.Fragment}
|
|
enter='ease-out duration-300'
|
|
enterFrom='opacity-0 scale-95'
|
|
enterTo='opacity-100 scale-100'
|
|
leave='ease-in duration-200'
|
|
leaveFrom='opacity-100 scale-100'
|
|
leaveTo='opacity-0 scale-95'
|
|
>
|
|
<Dialog.Panel className='flex w-full max-w-3xl transform overflow-hidden rounded-2xl bg-[#585A74] align-middle shadow-xl transition-all'>
|
|
{isLoading && (
|
|
<div className='absolute inset-0 z-40 grid place-items-center bg-black/50'>
|
|
<CircularProgress />
|
|
</div>
|
|
)}
|
|
<div className='flex flex-1 flex-col p-4'>
|
|
<Dialog.Title as='h3' className='mb-4 text-center font-medium'>
|
|
Borrow {tokenSymbol}
|
|
</Dialog.Title>
|
|
<div className='mb-4 flex flex-col gap-2 text-sm'>
|
|
<ContainerSecondary>
|
|
<p className='mb-1'>
|
|
In wallet: {walletAmount.toLocaleString()} {tokenSymbol}
|
|
</p>
|
|
<p className='mb-5'>Borrow Rate: {(borrowRate * 100).toFixed(2)}%</p>
|
|
|
|
<div className='mb-7'>
|
|
<p className='mb-2 font-semibold uppercase tracking-widest'>Amount</p>
|
|
<NumericFormat
|
|
className='mb-2 h-[32px] w-full rounded-lg border border-black/50 bg-transparent px-2'
|
|
value={amount}
|
|
placeholder='0'
|
|
allowNegative={false}
|
|
onValueChange={(v) => handleValueChange(v.floatValue || 0)}
|
|
suffix={` ${tokenSymbol}`}
|
|
decimalScale={getTokenDecimals(tokenDenom, marketAssets)}
|
|
/>
|
|
<div className='flex justify-between text-xs tracking-widest'>
|
|
<div>
|
|
1 {tokenSymbol} = {formatCurrency(tokenPrice)}
|
|
</div>
|
|
<div>{formatCurrency(tokenPrice * amount)}</div>
|
|
</div>
|
|
</div>
|
|
<Slider
|
|
className='mb-6'
|
|
value={percentageValue}
|
|
onChange={handleSliderValueChange}
|
|
onMaxClick={() => setAmount(maxValue)}
|
|
/>
|
|
</ContainerSecondary>
|
|
<ContainerSecondary className='flex items-center justify-between'>
|
|
<div className='flex'>
|
|
Borrow to Credit Account{' '}
|
|
<Tooltip
|
|
className='ml-2'
|
|
content={
|
|
<>
|
|
<Text size='sm' className='mb-2'>
|
|
OFF = Borrow directly into your wallet by using your account Assets
|
|
as collateral. The borrowed asset will become a liability in your
|
|
account.
|
|
</Text>
|
|
<Text size='sm'>
|
|
ON = Borrow into your Account. The borrowed asset will be available
|
|
in the account as an Asset and appear also as a liability in your
|
|
account.
|
|
</Text>
|
|
</>
|
|
}
|
|
/>
|
|
</div>
|
|
<Switch
|
|
checked={isBorrowToCreditAccount}
|
|
onChange={handleBorrowTargetChange}
|
|
className={`${
|
|
isBorrowToCreditAccount ? 'bg-blue-600' : 'bg-gray-400'
|
|
} relative inline-flex h-6 w-11 items-center rounded-full`}
|
|
>
|
|
<span
|
|
className={`${
|
|
isBorrowToCreditAccount ? 'translate-x-6' : 'translate-x-1'
|
|
} inline-block h-4 w-4 transform rounded-full bg-white transition`}
|
|
/>
|
|
</Switch>
|
|
</ContainerSecondary>
|
|
</div>
|
|
<Button
|
|
className='mt-auto'
|
|
onClick={handleSubmit}
|
|
disabled={amount === 0 || !amount}
|
|
>
|
|
Borrow
|
|
</Button>
|
|
</div>
|
|
|
|
<div className='flex w-1/2 flex-col justify-center bg-[#4A4C60] p-4'>
|
|
<p className='text-bold mb-3 text-xs uppercase text-white/50'>About</p>
|
|
<h4 className='mb-4 text-xl'>Account {selectedAccount}</h4>
|
|
<div className='mb-2 rounded-md border border-white/20 p-3'>
|
|
{accountStats && (
|
|
<div className='flex items-center gap-x-3'>
|
|
<p className='flex-1 text-xs'>
|
|
{formatCurrency(
|
|
BigNumber(accountStats.netWorth)
|
|
.dividedBy(10 ** baseAsset.decimals)
|
|
.toNumber(),
|
|
)}
|
|
</p>
|
|
<Gauge
|
|
value={accountStats.currentLeverage / accountStats.maxLeverage}
|
|
label='Lvg'
|
|
tooltip={
|
|
<Text size='sm'>
|
|
Current Leverage:{' '}
|
|
{formatValue(accountStats.currentLeverage, 0, 2, true, false, 'x')}
|
|
<br />
|
|
Max Leverage:{' '}
|
|
{formatValue(accountStats.maxLeverage, 0, 2, true, false, 'x')}
|
|
</Text>
|
|
}
|
|
/>
|
|
<Gauge
|
|
value={accountStats.risk}
|
|
label='Risk'
|
|
tooltip={
|
|
<Text size='sm'>
|
|
Current Risk:{' '}
|
|
{formatValue(accountStats.risk * 100, 0, 2, true, false, '%')}
|
|
</Text>
|
|
}
|
|
/>
|
|
<ProgressBar value={accountStats.health} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className='mb-2 rounded-md border border-white/20 p-3 text-sm'>
|
|
<div className='mb-1 flex justify-between'>
|
|
<div>Total Position:</div>
|
|
<div className='font-semibold'>
|
|
{formatCurrency(
|
|
BigNumber(accountStats?.totalPosition ?? 0)
|
|
.dividedBy(10 ** baseAsset.decimals)
|
|
.toNumber(),
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className='flex justify-between'>
|
|
<div>Total Liabilities:</div>
|
|
<div className='font-semibold'>
|
|
{formatCurrency(
|
|
BigNumber(accountStats?.totalDebt ?? 0)
|
|
.dividedBy(10 ** baseAsset.decimals)
|
|
.toNumber(),
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<PositionsList title='Balances' data={balances} />
|
|
</div>
|
|
</Dialog.Panel>
|
|
</Transition.Child>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
</Transition>
|
|
)
|
|
}
|