simulate withdraw operation so we can preview equity & coll (#10)
* show margin details in withdraw ui Co-authored-by: saml33 <slam.uke@gmail.com>
This commit is contained in:
parent
8aa8c5929b
commit
935129c45e
6
.babelrc
6
.babelrc
|
@ -10,5 +10,9 @@
|
|||
}
|
||||
]
|
||||
],
|
||||
"plugins": ["@emotion/babel-plugin"]
|
||||
"plugins": [
|
||||
"babel-plugin-macros",
|
||||
["styled-components", { "ssr": true }],
|
||||
"@emotion/babel-plugin"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ interface InputProps {
|
|||
onChange: (e) => void
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
error?: boolean
|
||||
[x: string]: any
|
||||
}
|
||||
|
||||
|
@ -22,6 +23,7 @@ const Input = ({
|
|||
value,
|
||||
onChange,
|
||||
className,
|
||||
error,
|
||||
wrapperClassName = 'w-full',
|
||||
disabled,
|
||||
prefix,
|
||||
|
@ -43,7 +45,9 @@ const Input = ({
|
|||
value={value}
|
||||
onChange={onChange}
|
||||
className={`${className} px-2 w-full bg-th-bkg-1 rounded h-10 text-th-fgd-1
|
||||
border border-th-fgd-4 default-transition hover:border-th-primary
|
||||
border ${
|
||||
error ? 'border-th-red' : 'border-th-fgd-4'
|
||||
} default-transition hover:border-th-primary
|
||||
focus:border-th-primary focus:outline-none
|
||||
${
|
||||
disabled
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
import { FunctionComponent, useEffect, useState } from 'react'
|
||||
import tw from 'twin.macro'
|
||||
import styled from '@emotion/styled'
|
||||
import Slider from 'rc-slider'
|
||||
import 'rc-slider/assets/index.css'
|
||||
|
||||
type StyledSliderProps = {
|
||||
enableTransition?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const StyledSlider = styled(Slider)<StyledSliderProps>`
|
||||
.rc-slider-rail {
|
||||
${tw`bg-th-bkg-3 h-2.5 rounded-full`}
|
||||
}
|
||||
.rc-slider-track {
|
||||
${tw`bg-th-primary h-2.5 rounded-full ring-1 ring-th-primary ring-inset`}
|
||||
${({ enableTransition }) =>
|
||||
enableTransition && tw`transition-all duration-500`}
|
||||
}
|
||||
.rc-slider-step {
|
||||
${tw`hidden`}
|
||||
}
|
||||
.rc-slider-handle {
|
||||
${tw`border-4 border-th-primary h-4 w-4`}
|
||||
background: #fff;
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
margin-top: -3px;
|
||||
${({ enableTransition }) =>
|
||||
enableTransition && tw`transition-all duration-500`}
|
||||
}
|
||||
.rc-slider-mark-text {
|
||||
${tw`font-display transition-all duration-300 text-th-fgd-2 hover:text-th-primary`}
|
||||
font-size: 10px;
|
||||
}
|
||||
.rc-slider-mark-text-active {
|
||||
${tw`opacity-60 hover:opacity-100`}
|
||||
}
|
||||
.rc-slider-mark-text:first-of-type {
|
||||
padding-left: 12px;
|
||||
}
|
||||
.rc-slider-mark-text:last-of-type {
|
||||
padding-right: 24px;
|
||||
}
|
||||
${({ disabled }) => disabled && 'background-color: transparent'}
|
||||
`
|
||||
|
||||
const StyledSliderButtonWrapper = styled.div`
|
||||
${tw`absolute left-0 top-5 w-full`}
|
||||
`
|
||||
|
||||
type StyledSliderButtonProps = {
|
||||
styleValue: number
|
||||
sliderValue: number
|
||||
}
|
||||
|
||||
const StyledSliderButton = styled.button<StyledSliderButtonProps>`
|
||||
${tw`bg-none text-th-fgd-3 transition-all duration-300 hover:text-th-primary focus:outline-none`}
|
||||
font-size: 0.65rem;
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
left: 0%;
|
||||
:nth-of-type(2) {
|
||||
left: 23%;
|
||||
transform: translateX(-23%);
|
||||
}
|
||||
:nth-of-type(3) {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
:nth-of-type(4) {
|
||||
left: 76%;
|
||||
transform: translateX(-76%);
|
||||
}
|
||||
:nth-of-type(5) {
|
||||
left: 100%;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
${({ styleValue, sliderValue }) => styleValue < sliderValue && tw`opacity-40`}
|
||||
${({ styleValue, sliderValue }) =>
|
||||
styleValue === sliderValue && tw`text-th-primary`}
|
||||
`
|
||||
|
||||
type SliderProps = {
|
||||
onChange: (...args: any[]) => any
|
||||
step: number
|
||||
value: number
|
||||
disabled: boolean
|
||||
max?: number
|
||||
maxButtonTransition?: boolean
|
||||
}
|
||||
|
||||
const AmountSlider: FunctionComponent<SliderProps> = ({
|
||||
onChange,
|
||||
step,
|
||||
value,
|
||||
disabled,
|
||||
max,
|
||||
maxButtonTransition,
|
||||
}) => {
|
||||
const [enableTransition, setEnableTransition] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (maxButtonTransition) {
|
||||
setEnableTransition(true)
|
||||
}
|
||||
}, [maxButtonTransition])
|
||||
|
||||
useEffect(() => {
|
||||
if (enableTransition) {
|
||||
const transitionTimer = setTimeout(() => {
|
||||
setEnableTransition(false)
|
||||
}, 500)
|
||||
return () => clearTimeout(transitionTimer)
|
||||
}
|
||||
}, [enableTransition])
|
||||
|
||||
const handleSliderButtonClick = (value) => {
|
||||
onChange(value)
|
||||
setEnableTransition(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<StyledSlider
|
||||
min={0}
|
||||
max={max}
|
||||
value={value || 0}
|
||||
onChange={onChange}
|
||||
step={step}
|
||||
enableTransition={enableTransition}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<StyledSliderButtonWrapper>
|
||||
<StyledSliderButton
|
||||
onClick={() => handleSliderButtonClick(0)}
|
||||
styleValue={0}
|
||||
sliderValue={value}
|
||||
>
|
||||
0%
|
||||
</StyledSliderButton>
|
||||
<StyledSliderButton
|
||||
onClick={() => handleSliderButtonClick(25)}
|
||||
styleValue={25}
|
||||
sliderValue={value}
|
||||
>
|
||||
25%
|
||||
</StyledSliderButton>
|
||||
<StyledSliderButton
|
||||
onClick={() => handleSliderButtonClick(50)}
|
||||
styleValue={50}
|
||||
sliderValue={value}
|
||||
>
|
||||
50%
|
||||
</StyledSliderButton>
|
||||
<StyledSliderButton
|
||||
onClick={() => handleSliderButtonClick(75)}
|
||||
styleValue={75}
|
||||
sliderValue={value}
|
||||
>
|
||||
75%
|
||||
</StyledSliderButton>
|
||||
<StyledSliderButton
|
||||
onClick={() => handleSliderButtonClick(100)}
|
||||
styleValue={100}
|
||||
sliderValue={value}
|
||||
>
|
||||
100%
|
||||
</StyledSliderButton>
|
||||
</StyledSliderButtonWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AmountSlider
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useMemo, useState } from 'react'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import Modal from './Modal'
|
||||
import Input from './Input'
|
||||
import AccountSelect from './AccountSelect'
|
||||
|
@ -8,28 +8,43 @@ import useMarketList from '../hooks/useMarketList'
|
|||
import {
|
||||
getSymbolForTokenMintAddress,
|
||||
displayDepositsForMarginAccount,
|
||||
floorToDecimal,
|
||||
} from '../utils/index'
|
||||
import useConnection from '../hooks/useConnection'
|
||||
import { borrowAndWithdraw, withdraw } from '../utils/mango'
|
||||
import Loading from './Loading'
|
||||
import Button from './Button'
|
||||
import Slider from './Slider'
|
||||
import Button, { LinkButton } from './Button'
|
||||
import { notify } from '../utils/notifications'
|
||||
import Switch from './Switch'
|
||||
import Tooltip from './Tooltip'
|
||||
import { InformationCircleIcon } from '@heroicons/react/outline'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import {
|
||||
ExclamationCircleIcon,
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
import { Disclosure, Transition } from '@headlessui/react'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import { MarginAccount, uiToNative } from '@blockworks-foundation/mango-client'
|
||||
|
||||
const WithdrawModal = ({ isOpen, onClose }) => {
|
||||
const [inputAmount, setInputAmount] = useState('')
|
||||
const [inputAmount, setInputAmount] = useState(0)
|
||||
const [invalidAmountMessage, setInvalidAmountMessage] = useState('')
|
||||
const [maxAmount, setMaxAmount] = useState(0)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [includeBorrow, setIncludeBorrow] = useState(false)
|
||||
const [simulation, setSimulation] = useState(null)
|
||||
const [showSimulation, setShowSimulation] = useState(false)
|
||||
const [sliderPercentage, setSliderPercentage] = useState(0)
|
||||
const [maxButtonTransition, setMaxButtonTransition] = useState(false)
|
||||
const { getTokenIndex, symbols } = useMarketList()
|
||||
const { connection, programId } = useConnection()
|
||||
const walletAccounts = useMangoStore((s) => s.wallet.balances)
|
||||
const prices = useMangoStore((s) => s.selectedMangoGroup.prices)
|
||||
const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const mintDecimals = useMangoStore((s) => s.selectedMangoGroup.mintDecimals)
|
||||
const selectedMarginAccount = useMangoStore(
|
||||
(s) => s.selectedMarginAccount.current
|
||||
)
|
||||
|
@ -49,38 +64,30 @@ const WithdrawModal = ({ isOpen, onClose }) => {
|
|||
mintAddress,
|
||||
getTokenIndex,
|
||||
])
|
||||
|
||||
const handleSetSelectedAccount = (val) => {
|
||||
setInputAmount('')
|
||||
setSelectedAccount(val)
|
||||
const symbol = getSymbolForTokenMintAddress(
|
||||
selectedAccount?.account?.mint.toString()
|
||||
)
|
||||
const DECIMALS = {
|
||||
BTC: 6,
|
||||
ETH: 5,
|
||||
USDT: 2,
|
||||
}
|
||||
|
||||
const withdrawDisabled = Number(inputAmount) <= 0
|
||||
useEffect(() => {
|
||||
if (!selectedMangoGroup || !selectedMarginAccount) return
|
||||
|
||||
const getMaxForSelectedAccount = () => {
|
||||
const marginAccount = useMangoStore.getState().selectedMarginAccount.current
|
||||
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
|
||||
return displayDepositsForMarginAccount(
|
||||
marginAccount,
|
||||
mangoGroup,
|
||||
const mintDecimals = selectedMangoGroup.mintDecimals[tokenIndex]
|
||||
const groupIndex = selectedMangoGroup.indexes[tokenIndex]
|
||||
const deposits = selectedMarginAccount.getUiDeposit(
|
||||
selectedMangoGroup,
|
||||
tokenIndex
|
||||
)
|
||||
const borrows = selectedMarginAccount.getUiBorrow(
|
||||
selectedMangoGroup,
|
||||
tokenIndex
|
||||
)
|
||||
}
|
||||
|
||||
const setMaxForSelectedAccount = () => {
|
||||
setInputAmount(getMaxForSelectedAccount().toString())
|
||||
}
|
||||
|
||||
const handleIncludeBorrowSwitch = (checked) => {
|
||||
setIncludeBorrow(checked)
|
||||
setInputAmount('')
|
||||
}
|
||||
|
||||
const setMaxBorrowForSelectedAccount = async () => {
|
||||
// get index prices
|
||||
const prices = await selectedMangoGroup.getPrices(connection)
|
||||
// get value of margin account assets minus the selected token
|
||||
const assetsVal =
|
||||
const currentAssetsVal =
|
||||
selectedMarginAccount.getAssetsVal(selectedMangoGroup, prices) -
|
||||
getMaxForSelectedAccount() * prices[tokenIndex]
|
||||
const currentLiabs = selectedMarginAccount.getLiabsVal(
|
||||
|
@ -88,21 +95,57 @@ const WithdrawModal = ({ isOpen, onClose }) => {
|
|||
prices
|
||||
)
|
||||
// multiply by 0.99 and subtract 0.01 to account for rounding issues
|
||||
const liabsAvail = (assetsVal / 1.2 - currentLiabs) * 0.99 - 0.01
|
||||
const amountToWithdraw =
|
||||
liabsAvail / prices[tokenIndex] + getMaxForSelectedAccount()
|
||||
const liabsAvail = (currentAssetsVal / 1.2 - currentLiabs) * 0.99 - 0.01
|
||||
|
||||
// calculate max withdraw amount
|
||||
const amountToWithdraw = includeBorrow
|
||||
? liabsAvail / prices[tokenIndex] + getMaxForSelectedAccount()
|
||||
: getMaxForSelectedAccount()
|
||||
|
||||
if (amountToWithdraw > 0) {
|
||||
setInputAmount(
|
||||
floorToDecimal(
|
||||
amountToWithdraw,
|
||||
mintDecimals[getTokenIndex(mintAddress)]
|
||||
).toString()
|
||||
)
|
||||
setMaxAmount(amountToWithdraw)
|
||||
} else {
|
||||
setInputAmount('0')
|
||||
setMaxAmount(0)
|
||||
}
|
||||
}
|
||||
|
||||
// simulate change to deposits & borrow based on input amount
|
||||
const newDeposit = Math.max(0, deposits - inputAmount)
|
||||
const newBorrows = borrows + Math.max(0, inputAmount - deposits)
|
||||
|
||||
// clone MarginAccount and arrays to not modify selectedMarginAccount
|
||||
const simulation = new MarginAccount(null, selectedMarginAccount)
|
||||
simulation.deposits = [...selectedMarginAccount.deposits]
|
||||
simulation.borrows = [...selectedMarginAccount.borrows]
|
||||
|
||||
// update with simulated values
|
||||
simulation.deposits[tokenIndex] =
|
||||
uiToNative(newDeposit, mintDecimals).toNumber() / groupIndex.deposit
|
||||
simulation.borrows[tokenIndex] =
|
||||
uiToNative(newBorrows, mintDecimals).toNumber() / groupIndex.borrow
|
||||
|
||||
const equity = simulation.computeValue(selectedMangoGroup, prices)
|
||||
const assetsVal = simulation.getAssetsVal(selectedMangoGroup, prices)
|
||||
const liabsVal = simulation.getLiabsVal(selectedMangoGroup, prices)
|
||||
const collateralRatio = simulation.getCollateralRatio(
|
||||
selectedMangoGroup,
|
||||
prices
|
||||
)
|
||||
const leverage = 1 / Math.max(0, collateralRatio - 1)
|
||||
setSimulation({
|
||||
equity,
|
||||
assetsVal,
|
||||
liabsVal,
|
||||
collateralRatio,
|
||||
leverage,
|
||||
})
|
||||
}, [
|
||||
includeBorrow,
|
||||
inputAmount,
|
||||
prices,
|
||||
tokenIndex,
|
||||
selectedMarginAccount,
|
||||
selectedMangoGroup,
|
||||
])
|
||||
|
||||
const handleWithdraw = () => {
|
||||
setSubmitting(true)
|
||||
|
@ -170,118 +213,389 @@ const WithdrawModal = ({ isOpen, onClose }) => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleSetSelectedAccount = (val) => {
|
||||
setInputAmount(0)
|
||||
setSliderPercentage(0)
|
||||
setSelectedAccount(val)
|
||||
}
|
||||
|
||||
const getMaxForSelectedAccount = () => {
|
||||
return displayDepositsForMarginAccount(
|
||||
selectedMarginAccount,
|
||||
selectedMangoGroup,
|
||||
tokenIndex
|
||||
)
|
||||
}
|
||||
|
||||
const getBorrowAmount = () => {
|
||||
const tokenBalance = getMaxForSelectedAccount()
|
||||
const borrowAmount = parseFloat(inputAmount) - tokenBalance
|
||||
const borrowAmount = inputAmount - tokenBalance
|
||||
return borrowAmount > 0 ? borrowAmount : 0
|
||||
}
|
||||
|
||||
const getAccountStatusColor = (
|
||||
collateralRatio: number,
|
||||
isRisk?: boolean,
|
||||
isStatus?: boolean
|
||||
) => {
|
||||
if (collateralRatio < 1.25) {
|
||||
return isRisk ? (
|
||||
<div className="text-th-red">High</div>
|
||||
) : isStatus ? (
|
||||
'bg-th-red'
|
||||
) : (
|
||||
'border-th-red text-th-red'
|
||||
)
|
||||
} else if (collateralRatio > 1.25 && collateralRatio < 1.5) {
|
||||
return isRisk ? (
|
||||
<div className="text-th-orange">Moderate</div>
|
||||
) : isStatus ? (
|
||||
'bg-th-orange'
|
||||
) : (
|
||||
'border-th-orange text-th-orange'
|
||||
)
|
||||
} else {
|
||||
return isRisk ? (
|
||||
<div className="text-th-green">Low</div>
|
||||
) : isStatus ? (
|
||||
'bg-th-green'
|
||||
) : (
|
||||
'border-th-green text-th-green'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleIncludeBorrowSwitch = (checked) => {
|
||||
setIncludeBorrow(checked)
|
||||
setInputAmount(0)
|
||||
setSliderPercentage(0)
|
||||
setInvalidAmountMessage('')
|
||||
}
|
||||
|
||||
const setMaxForSelectedAccount = () => {
|
||||
setInputAmount(getMaxForSelectedAccount())
|
||||
setSliderPercentage(100)
|
||||
setInvalidAmountMessage('')
|
||||
setMaxButtonTransition(true)
|
||||
}
|
||||
|
||||
const setMaxBorrowForSelectedAccount = async () => {
|
||||
setInputAmount(trimDecimals(maxAmount, DECIMALS[symbol]))
|
||||
setSliderPercentage(100)
|
||||
setInvalidAmountMessage('')
|
||||
setMaxButtonTransition(true)
|
||||
}
|
||||
|
||||
const onChangeAmountInput = (amount) => {
|
||||
setInputAmount(amount)
|
||||
setSliderPercentage((amount / maxAmount) * 100)
|
||||
setInvalidAmountMessage('')
|
||||
}
|
||||
|
||||
const onChangeSlider = async (percentage) => {
|
||||
const amount = (percentage / 100) * maxAmount
|
||||
setInputAmount(trimDecimals(amount, DECIMALS[symbol]))
|
||||
setSliderPercentage(percentage)
|
||||
setInvalidAmountMessage('')
|
||||
}
|
||||
|
||||
const validateAmountInput = (e) => {
|
||||
const amount = e.target.value
|
||||
if (Number(amount) <= 0) {
|
||||
setInvalidAmountMessage('Withdrawal amount must be greater than 0')
|
||||
}
|
||||
if (simulation.collateralRatio < 1.2) {
|
||||
setInvalidAmountMessage(
|
||||
'Leverage too high. Reduce the amount to withdraw'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const trimDecimals = (n, digits) => {
|
||||
var step = Math.pow(10, digits || 0)
|
||||
var temp = Math.trunc(step * n)
|
||||
|
||||
return temp / step
|
||||
}
|
||||
|
||||
// turn off slider transition for dragging slider handle interaction
|
||||
useEffect(() => {
|
||||
if (maxButtonTransition) {
|
||||
setMaxButtonTransition(false)
|
||||
}
|
||||
}, [maxButtonTransition])
|
||||
|
||||
if (!selectedAccount) return null
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<Modal.Header>
|
||||
<ElementTitle noMarignBottom>Withdraw Funds</ElementTitle>
|
||||
</Modal.Header>
|
||||
<>
|
||||
<AccountSelect
|
||||
hideAddress
|
||||
accounts={withdrawAccounts}
|
||||
selectedAccount={selectedAccount}
|
||||
onSelectAccount={handleSetSelectedAccount}
|
||||
getBalance={getMaxForSelectedAccount}
|
||||
symbols={symbols}
|
||||
/>
|
||||
<div className="flex items-center jusitfy-between text-th-fgd-1 mt-4 p-2 rounded-md bg-th-bkg-3">
|
||||
<div className="flex items-center text-fgd-1 pr-4">
|
||||
<span>Borrow Funds</span>
|
||||
<Tooltip content="Interest is charged on your borrowed balance and is subject to change.">
|
||||
<InformationCircleIcon
|
||||
className={`h-5 w-5 ml-2 text-th-primary cursor-help`}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Switch
|
||||
checked={includeBorrow}
|
||||
className="ml-auto"
|
||||
onChange={(checked) => handleIncludeBorrowSwitch(checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between pb-2 pt-4">
|
||||
<div className="text-th-fgd-1">Amount</div>
|
||||
<div className="flex space-x-4">
|
||||
<div
|
||||
className="text-th-fgd-1 underline cursor-pointer default-transition hover:text-th-primary hover:no-underline"
|
||||
onClick={
|
||||
includeBorrow
|
||||
? setMaxBorrowForSelectedAccount
|
||||
: setMaxForSelectedAccount
|
||||
}
|
||||
>
|
||||
{includeBorrow ? 'Max with Borrow' : 'Max'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
className={`border border-th-fgd-4 flex-grow pr-11`}
|
||||
placeholder="0.00"
|
||||
value={inputAmount}
|
||||
onChange={(e) => setInputAmount(e.target.value)}
|
||||
suffix={getSymbolForTokenMintAddress(
|
||||
selectedAccount?.account?.mint.toString()
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{includeBorrow ? (
|
||||
{simulation ? (
|
||||
<>
|
||||
<Transition
|
||||
appear={true}
|
||||
className="p-2 bg-th-bkg-1 rounded-md mt-4"
|
||||
show={includeBorrow}
|
||||
enter="transition-opacity duration-500"
|
||||
show={!showSimulation}
|
||||
enter="transition ease-out delay-200 duration-500"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-500"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="flex justify-between pb-2">
|
||||
<div className="text-th-fgd-3">Borrow Amount</div>
|
||||
<div className="text-th-fgd-1">{`${getBorrowAmount()} ${getSymbolForTokenMintAddress(
|
||||
selectedAccount?.account?.mint.toString()
|
||||
)}`}</div>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="text-th-fgd-3">Current APR</div>
|
||||
<div className="text-th-fgd-1">
|
||||
{(selectedMangoGroup.getBorrowRate(tokenIndex) * 100).toFixed(
|
||||
2
|
||||
)}
|
||||
%
|
||||
</div>
|
||||
</div>
|
||||
{!showSimulation ? (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<ElementTitle noMarignBottom>Withdraw Funds</ElementTitle>
|
||||
</Modal.Header>
|
||||
<AccountSelect
|
||||
hideAddress
|
||||
accounts={withdrawAccounts}
|
||||
selectedAccount={selectedAccount}
|
||||
onSelectAccount={handleSetSelectedAccount}
|
||||
getBalance={getMaxForSelectedAccount}
|
||||
symbols={symbols}
|
||||
/>
|
||||
<div className="flex items-center jusitfy-between text-th-fgd-1 mt-4 p-2 rounded-md bg-th-bkg-3">
|
||||
<div className="flex items-center text-fgd-1 pr-4">
|
||||
<span>Borrow Funds</span>
|
||||
<Tooltip content="Interest is charged on your borrowed balance and is subject to change.">
|
||||
<InformationCircleIcon
|
||||
className={`h-5 w-5 ml-2 text-th-fgd-3 cursor-help`}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Switch
|
||||
checked={includeBorrow}
|
||||
className="ml-auto"
|
||||
onChange={(checked) => handleIncludeBorrowSwitch(checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between pb-2 pt-4">
|
||||
<div className="text-th-fgd-1">Amount</div>
|
||||
<div className="flex space-x-4">
|
||||
<div
|
||||
className="text-th-fgd-1 underline cursor-pointer default-transition hover:text-th-primary hover:no-underline"
|
||||
onClick={
|
||||
includeBorrow
|
||||
? setMaxBorrowForSelectedAccount
|
||||
: setMaxForSelectedAccount
|
||||
}
|
||||
>
|
||||
Max
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
className={`border border-th-fgd-4 flex-grow pr-11`}
|
||||
error={!!invalidAmountMessage}
|
||||
placeholder="0.00"
|
||||
value={inputAmount}
|
||||
onBlur={validateAmountInput}
|
||||
onChange={(e) => onChangeAmountInput(e.target.value)}
|
||||
suffix={symbol}
|
||||
/>
|
||||
<Tooltip content="Account Leverage" className="py-1">
|
||||
<span
|
||||
className={`${getAccountStatusColor(
|
||||
simulation.collateralRatio
|
||||
)} bg-th-bkg-1 border flex font-semibold h-10 items-center justify-center ml-2 rounded text-th-fgd-1 w-14`}
|
||||
>
|
||||
{simulation.leverage < 5
|
||||
? simulation.leverage.toFixed(2)
|
||||
: '>5'}
|
||||
x
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{invalidAmountMessage ? (
|
||||
<div className="flex items-center pt-1.5 text-th-red">
|
||||
<ExclamationCircleIcon className="h-4 w-4 mr-1.5" />
|
||||
{invalidAmountMessage}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="pt-3 pb-4">
|
||||
<Slider
|
||||
disabled={null}
|
||||
value={sliderPercentage}
|
||||
onChange={(v) => onChangeSlider(v)}
|
||||
step={1}
|
||||
maxButtonTransition={maxButtonTransition}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-5 flex justify-center`}>
|
||||
<Button
|
||||
onClick={() => setShowSimulation(true)}
|
||||
disabled={
|
||||
Number(inputAmount) <= 0 ||
|
||||
simulation.collateralRatio < 1.2
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</Transition>
|
||||
) : null}
|
||||
<div className={`mt-5 flex justify-center`}>
|
||||
<Button
|
||||
onClick={handleWithdraw}
|
||||
disabled={withdrawDisabled}
|
||||
className="w-full"
|
||||
<Transition
|
||||
show={showSimulation}
|
||||
enter="transition ease-out delay-200 duration-500"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
>
|
||||
<div className={`flex items-center justify-center`}>
|
||||
{submitting && <Loading className="-ml-1 mr-3" />}
|
||||
{`Withdraw ${
|
||||
inputAmount ? inputAmount : ''
|
||||
} ${getSymbolForTokenMintAddress(
|
||||
selectedAccount?.account?.mint.toString()
|
||||
)}
|
||||
`}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
{showSimulation ? (
|
||||
<>
|
||||
<Modal.Header>
|
||||
<ElementTitle noMarignBottom>Confirm Withdraw</ElementTitle>
|
||||
</Modal.Header>
|
||||
{simulation.collateralRatio < 1.2 ? (
|
||||
<div className="border border-th-red mb-4 p-2 rounded">
|
||||
<div className="flex items-center text-th-red">
|
||||
<ExclamationCircleIcon className="h-4 w-4 mr-1.5 flex-shrink-0" />
|
||||
Prices have changed and increased your leverage. Reduce
|
||||
the withdrawal amount.
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="bg-th-bkg-1 p-4 rounded-lg text-th-fgd-1 text-center">
|
||||
<div className="text-th-fgd-3 pb-1">{`You're about to withdraw`}</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="font-semibold relative text-xl">
|
||||
{inputAmount}
|
||||
<span className="absolute bottom-0.5 font-normal ml-1.5 text-xs text-th-fgd-4">
|
||||
{symbol}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{getBorrowAmount() > 0 ? (
|
||||
<div className="pt-2 text-th-fgd-4">{`Includes borrow of ~${getBorrowAmount().toFixed(
|
||||
DECIMALS[symbol]
|
||||
)} ${symbol}`}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Disclosure>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button
|
||||
className={`border border-th-fgd-4 default-transition font-normal mt-4 pl-3 pr-2 py-2.5 ${
|
||||
open ? 'rounded-b-none' : 'rounded-md'
|
||||
} text-th-fgd-1 w-full hover:bg-th-bkg-3 focus:outline-none`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<span className="flex h-2 w-2 mr-2.5 relative">
|
||||
<span
|
||||
className={`animate-ping absolute inline-flex h-full w-full rounded-full ${getAccountStatusColor(
|
||||
simulation.collateralRatio,
|
||||
false,
|
||||
true
|
||||
)} opacity-75`}
|
||||
></span>
|
||||
<span
|
||||
className={`relative inline-flex rounded-full h-2 w-2 ${getAccountStatusColor(
|
||||
simulation.collateralRatio,
|
||||
false,
|
||||
true
|
||||
)}`}
|
||||
></span>
|
||||
</span>
|
||||
Account Health Check
|
||||
<Tooltip content="The details of your account after this withdrawal.">
|
||||
<InformationCircleIcon
|
||||
className={`h-5 w-5 ml-2 text-th-fgd-3 cursor-help`}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{open ? (
|
||||
<ChevronUpIcon className="h-5 w-5 mr-1" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-5 w-5 mr-1" />
|
||||
)}
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel
|
||||
className={`border border-th-fgd-4 border-t-0 p-4 rounded-b-md`}
|
||||
>
|
||||
<div>
|
||||
<div className="flex justify-between pb-2">
|
||||
<div className="text-th-fgd-4">Account Value</div>
|
||||
<div className="text-th-fgd-1">
|
||||
${simulation.assetsVal.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between pb-2">
|
||||
<div className="text-th-fgd-4">Account Risk</div>
|
||||
<div className="text-th-fgd-1">
|
||||
{getAccountStatusColor(
|
||||
simulation.collateralRatio,
|
||||
true
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between pb-2">
|
||||
<div className="text-th-fgd-4">Leverage</div>
|
||||
<div className="text-th-fgd-1">
|
||||
{simulation.leverage.toFixed(2)}x
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="text-th-fgd-4">
|
||||
Collateral Ratio
|
||||
</div>
|
||||
<div className="text-th-fgd-1">
|
||||
{simulation.collateralRatio * 100 < 200
|
||||
? Math.floor(simulation.collateralRatio * 100)
|
||||
: '>200'}
|
||||
%
|
||||
</div>
|
||||
</div>
|
||||
{simulation.liabsVal > 0.05 ? (
|
||||
<div className="flex justify-between pt-2">
|
||||
<div className="text-th-fgd-4">Borrow Value</div>
|
||||
<div className="text-th-fgd-1">
|
||||
${simulation.liabsVal.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
<div className={`mt-5 flex justify-center`}>
|
||||
<Button
|
||||
onClick={handleWithdraw}
|
||||
disabled={
|
||||
Number(inputAmount) <= 0 ||
|
||||
simulation.collateralRatio < 1.2
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<div className={`flex items-center justify-center`}>
|
||||
{submitting && <Loading className="-ml-1 mr-3" />}
|
||||
Confirm
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<LinkButton
|
||||
className="flex items-center mt-4 text-th-fgd-3"
|
||||
onClick={() => setShowSimulation(false)}
|
||||
>
|
||||
<ChevronLeftIcon className="h-5 w-5 mr-1" />
|
||||
Back
|
||||
</LinkButton>
|
||||
</>
|
||||
) : null}
|
||||
</Transition>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="animate-pulse bg-th-bkg-3 h-10 mb-4 rounded w-full" />
|
||||
<div className="animate-pulse bg-th-bkg-3 h-10 mb-4 rounded w-full" />
|
||||
<div className="animate-pulse bg-th-bkg-3 h-10 mb-4 rounded w-full" />
|
||||
<div className="animate-pulse bg-th-bkg-3 h-10 mb-4 rounded w-full" />
|
||||
<div className="animate-pulse bg-th-bkg-3 h-10 mb-4 rounded w-full" />
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ const useHydrateStore = () => {
|
|||
const { marketList } = useMarketList()
|
||||
|
||||
useEffect(() => {
|
||||
actions.fetchAllMangoGroups()
|
||||
actions.fetchMangoGroup()
|
||||
}, [actions])
|
||||
|
||||
|
|
14
package.json
14
package.json
|
@ -25,11 +25,16 @@
|
|||
"yarn format"
|
||||
]
|
||||
},
|
||||
"babelMacros": {
|
||||
"twin": {
|
||||
"preset": "styled-components"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@blockworks-foundation/mango-client": "^0.1.16",
|
||||
"@emotion/react": "^11.1.5",
|
||||
"@emotion/styled": "^11.1.5",
|
||||
"@headlessui/react": "^0.3.2-d950146",
|
||||
"@headlessui/react": "^1.2.0",
|
||||
"@heroicons/react": "^1.0.0",
|
||||
"@project-serum/serum": "^0.13.20",
|
||||
"@project-serum/sol-wallet-adapter": "^0.1.8",
|
||||
|
@ -45,6 +50,7 @@
|
|||
"next": "^10.1.3",
|
||||
"next-themes": "^0.0.14",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"rc-slider": "^9.7.2",
|
||||
"react": "^17.0.1",
|
||||
"react-cool-dimensions": "^2.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
|
@ -67,6 +73,8 @@
|
|||
"@typescript-eslint/eslint-plugin": "^4.14.2",
|
||||
"@typescript-eslint/parser": "^4.14.2",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-styled-components": "^1.12.0",
|
||||
"eslint": "^7.19.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
|
@ -78,7 +86,9 @@
|
|||
"lint-staged": "^10.0.10",
|
||||
"postcss": "^8.2.8",
|
||||
"prettier": "^2.0.2",
|
||||
"tailwindcss": "^2.1.1",
|
||||
"react-is": "^17.0.2",
|
||||
"tailwindcss": "^2.1.2",
|
||||
"twin.macro": "^2.4.1",
|
||||
"typescript": "^4.1.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ const DECIMALS = {
|
|||
ETH: 3,
|
||||
USDT: 2,
|
||||
USDC: 2,
|
||||
WUSDT: 2,
|
||||
}
|
||||
|
||||
const icons = {
|
||||
|
|
|
@ -90,6 +90,7 @@ interface MangoStore extends State {
|
|||
[address: string]: Market
|
||||
}
|
||||
mintDecimals: number[]
|
||||
prices: number[]
|
||||
}
|
||||
marginAccounts: MarginAccount[]
|
||||
selectedMarginAccount: {
|
||||
|
@ -137,6 +138,7 @@ const useMangoStore = create<MangoStore>((set, get) => ({
|
|||
markets: {},
|
||||
srmAccount: null,
|
||||
mintDecimals: [],
|
||||
prices: [],
|
||||
},
|
||||
selectedMarket: {
|
||||
name: Object.entries(
|
||||
|
@ -202,12 +204,13 @@ const useMangoStore = create<MangoStore>((set, get) => ({
|
|||
const set = get().set
|
||||
|
||||
if (wallet?.publicKey && connected) {
|
||||
const usersMangoSrmAccounts = await mangoClient.getMangoSrmAccountsForOwner(
|
||||
connection,
|
||||
new PublicKey(IDS[cluster].mango_program_id),
|
||||
selectedMangoGroup,
|
||||
wallet
|
||||
)
|
||||
const usersMangoSrmAccounts =
|
||||
await mangoClient.getMangoSrmAccountsForOwner(
|
||||
connection,
|
||||
new PublicKey(IDS[cluster].mango_program_id),
|
||||
selectedMangoGroup,
|
||||
wallet
|
||||
)
|
||||
if (usersMangoSrmAccounts.length) {
|
||||
set((state) => {
|
||||
state.wallet.srmAccountsForOwner = usersMangoSrmAccounts
|
||||
|
@ -305,14 +308,20 @@ const useMangoStore = create<MangoStore>((set, get) => ({
|
|||
return mangoClient
|
||||
.getMangoGroup(connection, mangoGroupPk, srmVaultPk)
|
||||
.then(async (mangoGroup) => {
|
||||
const srmAccountInfo = await connection.getAccountInfo(
|
||||
const srmAccountInfoPromise = connection.getAccountInfo(
|
||||
mangoGroup.srmVault
|
||||
)
|
||||
const pricesPromise = mangoGroup.getPrices(connection)
|
||||
const [srmAccountInfo, prices] = await Promise.all([
|
||||
srmAccountInfoPromise,
|
||||
pricesPromise,
|
||||
])
|
||||
// Set the mango group
|
||||
set((state) => {
|
||||
state.selectedMangoGroup.current = mangoGroup
|
||||
state.selectedMangoGroup.srmAccount = srmAccountInfo
|
||||
state.selectedMangoGroup.mintDecimals = mangoGroup.mintDecimals
|
||||
state.selectedMangoGroup.prices = prices
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
|
@ -332,9 +341,8 @@ const useMangoStore = create<MangoStore>((set, get) => ({
|
|||
if (!selectedMarginAccount) return
|
||||
if (selectedMarginAccount.openOrdersAccounts.length === 0) return
|
||||
|
||||
const openOrdersAccounts = selectedMarginAccount.openOrdersAccounts.filter(
|
||||
isDefined
|
||||
)
|
||||
const openOrdersAccounts =
|
||||
selectedMarginAccount.openOrdersAccounts.filter(isDefined)
|
||||
const publicKeys = openOrdersAccounts.map((act) =>
|
||||
act.publicKey.toString()
|
||||
)
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
--red-dark: theme('colors.light-theme.red.dark');
|
||||
--green: theme('colors.light-theme.green.DEFAULT');
|
||||
--green-dark: theme('colors.light-theme.green.dark');
|
||||
--orange: theme('colors.light-theme.orange');
|
||||
--bkg-1: theme('colors.light-theme["bkg-1"]');
|
||||
--bkg-2: theme('colors.light-theme["bkg-2"]');
|
||||
--bkg-3: theme('colors.light-theme["bkg-3"]');
|
||||
|
@ -20,11 +21,12 @@
|
|||
}
|
||||
|
||||
[data-theme='Dark'] {
|
||||
--primary: theme('colors.dark-theme.orange');
|
||||
--primary: theme('colors.dark-theme.yellow');
|
||||
--red: theme('colors.dark-theme.red.DEFAULT');
|
||||
--red-dark: theme('colors.dark-theme.red.dark');
|
||||
--green: theme('colors.dark-theme.green.DEFAULT');
|
||||
--green-dark: theme('colors.dark-theme.green.dark');
|
||||
--orange: theme('colors.dark-theme.orange.DEFAULT');
|
||||
--bkg-1: theme('colors.dark-theme["bkg-1"]');
|
||||
--bkg-2: theme('colors.dark-theme["bkg-2"]');
|
||||
--bkg-3: theme('colors.dark-theme["bkg-3"]');
|
||||
|
@ -40,6 +42,7 @@
|
|||
--red-dark: theme('colors.mango-theme.red.dark');
|
||||
--green: theme('colors.mango-theme.green.DEFAULT');
|
||||
--green-dark: theme('colors.mango-theme.green.dark');
|
||||
--orange: theme('colors.mango-theme.orange.DEFAULT');
|
||||
--bkg-1: theme('colors.mango-theme["bkg-1"]');
|
||||
--bkg-2: theme('colors.mango-theme["bkg-2"]');
|
||||
--bkg-3: theme('colors.mango-theme["bkg-3"]');
|
||||
|
|
|
@ -61,9 +61,10 @@ module.exports = {
|
|||
'fgd-4': '#B0B0B0',
|
||||
},
|
||||
'dark-theme': {
|
||||
orange: '#F2C94C',
|
||||
yellow: '#F2C94C',
|
||||
red: { DEFAULT: '#CC2929', dark: '#AA2222' },
|
||||
green: { DEFAULT: '#5EBF4D', dark: '#4BA53B' },
|
||||
orange: { DEFAULT: '#FF9C24' },
|
||||
'bkg-1': '#1C1C1C',
|
||||
'bkg-2': '#2B2B2B',
|
||||
'bkg-3': '#424242',
|
||||
|
@ -74,8 +75,15 @@ module.exports = {
|
|||
},
|
||||
'mango-theme': {
|
||||
yellow: '#F2C94C',
|
||||
red: { DEFAULT: '#E54033', dark: '#C7251A' },
|
||||
green: { DEFAULT: '#AFD803', dark: '#91B503' },
|
||||
red: {
|
||||
DEFAULT: '#E54033',
|
||||
dark: '#C7251A',
|
||||
},
|
||||
green: {
|
||||
DEFAULT: '#AFD803',
|
||||
dark: '#91B503',
|
||||
},
|
||||
orange: { DEFAULT: '#FF9C24' },
|
||||
'bkg-1': '#141026',
|
||||
'bkg-2': '#1D1832',
|
||||
'bkg-3': '#322E47',
|
||||
|
@ -96,6 +104,7 @@ module.exports = {
|
|||
'th-red-dark': 'var(--red-dark)',
|
||||
'th-green': 'var(--green)',
|
||||
'th-green-dark': 'var(--green-dark)',
|
||||
'th-orange': 'var(--orange)',
|
||||
},
|
||||
keyframes: {
|
||||
shake: {
|
||||
|
|
Loading…
Reference in New Issue