import React, { FunctionComponent, useEffect, useMemo, useState } from 'react' import Modal from './Modal' import Input from './Input' import { ElementTitle } from './styles' import useMangoStore from '../stores/useMangoStore' import useMarketList from '../hooks/useMarketList' import { DECIMALS, floorToDecimal, tokenPrecision, displayDepositsForMangoAccount, } from '../utils/index' import useConnection from '../hooks/useConnection' // import { borrowAndWithdraw, withdraw } from '../utils/mango' import Loading from './Loading' import Slider from './Slider' import Button, { LinkButton } from './Button' import { notify } from '../utils/notifications' import Switch from './Switch' import Tooltip from './Tooltip' import { ExclamationCircleIcon, InformationCircleIcon, } from '@heroicons/react/outline' import { ChevronLeftIcon, ChevronDownIcon, ChevronUpIcon, } from '@heroicons/react/solid' import { Disclosure } from '@headlessui/react' import { PublicKey } from '@solana/web3.js' import { MerpsAccount as MangoAccount, uiToNative, } from '@blockworks-foundation/mango-client' import Select from './Select' interface WithdrawModalProps { onClose: () => void isOpen: boolean tokenSymbol?: string } const WithdrawModal: FunctionComponent = ({ isOpen, onClose, tokenSymbol = '', }) => { return <> const [withdrawTokenSymbol, setWithdrawTokenSymbol] = useState( tokenSymbol || 'USDC' ) 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 prices = useMangoStore((s) => s.selectedMangoGroup.prices) const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup.current) const selectedMangoAccount = useMangoStore( (s) => s.selectedMangoAccount.current ) const actions = useMangoStore((s) => s.actions) const tokenIndex = useMemo( () => getTokenIndex(symbols[withdrawTokenSymbol]), [withdrawTokenSymbol, getTokenIndex] ) useEffect(() => { if (!selectedMangoGroup || !selectedMangoAccount || !withdrawTokenSymbol) return const mintDecimals = selectedMangoGroup.mintDecimals[tokenIndex] const groupIndex = selectedMangoGroup.indexes[tokenIndex] const deposits = selectedMangoAccount.getUiDeposit( selectedMangoGroup, tokenIndex ) const borrows = selectedMangoAccount.getUiBorrow( selectedMangoGroup, tokenIndex ) const currentAssetsVal = selectedMangoAccount.getAssetsVal(selectedMangoGroup, prices) - getMaxForSelectedAsset() * prices[tokenIndex] const currentLiabs = selectedMangoAccount.getLiabsVal( selectedMangoGroup, prices ) // multiply by 0.99 and subtract 0.01 to account for rounding issues const liabsAvail = (currentAssetsVal / 1.2 - currentLiabs) * 0.99 - 0.01 // calculate max withdraw amount const amountToWithdraw = includeBorrow ? liabsAvail / prices[tokenIndex] + getMaxForSelectedAsset() : getMaxForSelectedAsset() if (amountToWithdraw > 0) { setMaxAmount(amountToWithdraw) } else { 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 MangoAccount and arrays to not modify selectedMangoAccount const simulation = new MangoAccount(null, selectedMangoAccount) simulation.deposits = [...selectedMangoAccount.deposits] simulation.borrows = [...selectedMangoAccount.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, selectedMangoAccount, selectedMangoGroup, ]) const handleWithdraw = () => { setSubmitting(true) const mangoAccount = useMangoStore.getState().selectedMangoAccount.current const mangoGroup = useMangoStore.getState().selectedMangoGroup.current const wallet = useMangoStore.getState().wallet.current if (!mangoAccount || !mangoGroup) return if (!includeBorrow) { withdraw( connection, new PublicKey(programId), mangoGroup, mangoAccount, wallet, new PublicKey(symbols[withdrawTokenSymbol]), Number(inputAmount) ) .then((_transSig: string) => { setSubmitting(false) actions.fetchMangoGroup() actions.fetchMangoAccounts() actions.fetchWalletTokens() onClose() }) .catch((err) => { setSubmitting(false) console.warn('Error withdrawing:', err) notify({ message: 'Could not perform withdraw', txid: err.txid, type: 'error', }) onClose() }) } else { borrowAndWithdraw( connection, new PublicKey(programId), mangoGroup, mangoAccount, wallet, new PublicKey(symbols[withdrawTokenSymbol]), Number(inputAmount) ) .then((_transSig: string) => { setSubmitting(false) actions.fetchMangoGroup() actions.fetchMangoAccounts() actions.fetchWalletTokens() onClose() }) .catch((err) => { setSubmitting(false) console.warn('Error borrowing and withdrawing:', err) notify({ message: 'Could not perform borrow and withdraw', description: `${err}`, txid: err.txid, type: 'error', }) onClose() }) } } const handleSetSelectedAsset = (symbol) => { setInputAmount(0) setSliderPercentage(0) setWithdrawTokenSymbol(symbol) } const getMaxForSelectedAsset = () => { return displayDepositsForMangoAccount( selectedMangoAccount, selectedMangoGroup, tokenIndex ) } const getBorrowAmount = () => { const tokenBalance = getMaxForSelectedAsset() const borrowAmount = inputAmount - tokenBalance return borrowAmount > 0 ? borrowAmount : 0 } const getAccountStatusColor = ( collateralRatio: number, isRisk?: boolean, isStatus?: boolean ) => { if (collateralRatio < 1.25) { return isRisk ? (
High
) : isStatus ? ( 'bg-th-red' ) : ( 'border-th-red text-th-red' ) } else if (collateralRatio > 1.25 && collateralRatio < 1.5) { return isRisk ? (
Moderate
) : isStatus ? ( 'bg-th-orange' ) : ( 'border-th-orange text-th-orange' ) } else { return isRisk ? (
Low
) : isStatus ? ( 'bg-th-green' ) : ( 'border-th-green text-th-green' ) } } const handleIncludeBorrowSwitch = (checked) => { setIncludeBorrow(checked) setInputAmount(0) setSliderPercentage(0) setInvalidAmountMessage('') } const setMaxForSelectedAsset = () => { setInputAmount(getMaxForSelectedAsset()) setSliderPercentage(100) setInvalidAmountMessage('') setMaxButtonTransition(true) } const setMaxBorrowForSelectedAsset = async () => { setInputAmount(trimDecimals(maxAmount, DECIMALS[withdrawTokenSymbol] + 4)) setSliderPercentage(100) setInvalidAmountMessage('') setMaxButtonTransition(true) } const onChangeAmountInput = (amount) => { setInputAmount(amount) setSliderPercentage((amount / maxAmount) * 100) setInvalidAmountMessage('') } const onChangeSlider = async (percentage) => { const amount = (percentage / 100) * maxAmount if (percentage === 100) { setInputAmount(trimDecimals(maxAmount, DECIMALS[withdrawTokenSymbol] + 4)) } else { setInputAmount(trimDecimals(amount, DECIMALS[withdrawTokenSymbol] + 2)) } setSliderPercentage(percentage) setInvalidAmountMessage('') validateAmountInput(amount) } const validateAmountInput = (amount) => { if ( (Number(amount) <= 0 && getMaxForSelectedAsset() > 0) || (Number(amount) <= 0 && includeBorrow) ) { setInvalidAmountMessage('Enter an amount to withdraw') } if ( (getMaxForSelectedAsset() === 0 || Number(amount) > getMaxForSelectedAsset()) && !includeBorrow ) { setInvalidAmountMessage('Insufficient balance. Borrow funds to withdraw') } } useEffect(() => { if (simulation && simulation.collateralRatio < 1.2 && includeBorrow) { setInvalidAmountMessage( 'Leverage too high. Reduce the amount to withdraw' ) } }, [simulation]) const trimDecimals = (n, digits) => { const step = Math.pow(10, digits || 0) const temp = Math.trunc(step * n) return temp / step } const getTokenBalances = () => Object.entries(symbols).map(([name], i) => { return { symbol: name, balance: floorToDecimal( selectedMangoAccount.getUiDeposit(selectedMangoGroup, i), tokenPrecision[name] ), } }) // turn off slider transition for dragging slider handle interaction useEffect(() => { if (maxButtonTransition) { setMaxButtonTransition(false) } }, [maxButtonTransition]) // turn on borrow toggle when asset balance is zero // useEffect(() => { // if (withdrawTokenSymbol && getMaxForSelectedAsset() === 0) { // setIncludeBorrow(true) // } // }, [withdrawTokenSymbol]) if (!withdrawTokenSymbol) return null return ( <> {!showSimulation ? ( <> Withdraw Funds
Asset
Borrow Funds
handleIncludeBorrowSwitch(checked)} />
Amount
validateAmountInput(e.target.value)} onChange={(e) => onChangeAmountInput(e.target.value)} suffix={withdrawTokenSymbol} /> {/* {simulation ? ( {simulation.leverage < 5 ? simulation.leverage.toFixed(2) : '>5'} x ) : null} */}
{invalidAmountMessage ? (
{invalidAmountMessage}
) : null}
onChangeSlider(v)} step={1} maxButtonTransition={maxButtonTransition} />
) : null} {showSimulation && simulation ? ( <> Confirm Withdraw {simulation.collateralRatio < 1.2 ? (
Prices have changed and increased your leverage. Reduce the withdrawal amount.
) : null}
{`You're about to withdraw`}
{inputAmount} {withdrawTokenSymbol}
{getBorrowAmount() > 0 ? (
{`Includes borrow of ~${getBorrowAmount().toFixed( DECIMALS[withdrawTokenSymbol] )} ${withdrawTokenSymbol}`}
) : null}
{({ open }) => ( <>
Account Health Check
{open ? ( ) : ( )}
Account Value
${simulation.assetsVal.toFixed(2)}
Account Risk
{getAccountStatusColor( simulation.collateralRatio, true )}
Leverage
{simulation.leverage.toFixed(2)}x
Collateral Ratio
{simulation.collateralRatio * 100 < 200 ? Math.floor(simulation.collateralRatio * 100) : '>200'} %
{simulation.liabsVal > 0.05 ? (
Borrow Value
${simulation.liabsVal.toFixed(2)}
) : null}
)}
setShowSimulation(false)} > Back ) : null}
) } export default React.memo(WithdrawModal)