import React, { FunctionComponent, useEffect, useMemo, useState } from 'react' import { Disclosure } from '@headlessui/react' import { ExclamationCircleIcon, InformationCircleIcon, } from '@heroicons/react/outline' import { ChevronLeftIcon, ChevronDownIcon, ChevronUpIcon, } from '@heroicons/react/solid' import { nativeToUi, sleep, } from '@blockworks-foundation/mango-client/lib/utils' import { MarginAccount, uiToNative } from '@blockworks-foundation/mango-client' import Modal from './Modal' import Input from './Input' import AccountSelect from './AccountSelect' import { ElementTitle } from './styles' import useMangoStore from '../stores/useMangoStore' import useMarketList from '../hooks/useMarketList' import { getSymbolForTokenMintAddress, DECIMALS, trimDecimals, } from '../utils/index' import useConnection from '../hooks/useConnection' import { deposit, initMarginAccountAndDeposit } from '../utils/mango' import { PublicKey } from '@solana/web3.js' import Loading from './Loading' import Button, { LinkButton } from './Button' import Tooltip from './Tooltip' import Slider from './Slider' import InlineNotification from './InlineNotification' import { notify } from '../utils/notifications' interface DepositModalProps { onClose: () => void isOpen: boolean settleDeficit?: number tokenSymbol?: string } const DepositModal: FunctionComponent = ({ isOpen, onClose, settleDeficit, tokenSymbol = '', }) => { const [inputAmount, setInputAmount] = useState(settleDeficit || 0) const [submitting, setSubmitting] = useState(false) const [simulation, setSimulation] = useState(null) const [showSimulation, setShowSimulation] = useState(false) const [invalidAmountMessage, setInvalidAmountMessage] = useState('') const [sliderPercentage, setSliderPercentage] = useState(0) const [maxButtonTransition, setMaxButtonTransition] = useState(false) const { getTokenIndex, symbols } = useMarketList() const { connection, programId } = useConnection() const mintDecimals = useMangoStore((s) => s.selectedMangoGroup.mintDecimals) const walletAccounts = useMangoStore((s) => s.wallet.balances) const actions = useMangoStore((s) => s.actions) const depositAccounts = useMemo( () => walletAccounts.filter((acc) => Object.values(symbols).includes(acc.account.mint.toString()) ), [symbols, walletAccounts] ) const [selectedAccount, setSelectedAccount] = useState(depositAccounts[0]) const prices = useMangoStore((s) => s.selectedMangoGroup.prices) const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup.current) const selectedMarginAccount = useMangoStore( (s) => s.selectedMarginAccount.current ) const mintAddress = useMemo( () => selectedAccount?.account.mint.toString(), [selectedAccount] ) const tokenIndex = useMemo( () => getTokenIndex(mintAddress), [mintAddress, getTokenIndex] ) const symbol = getSymbolForTokenMintAddress( selectedAccount?.account?.mint.toString() ) useEffect(() => { if (tokenSymbol) { const symbolMint = symbols[tokenSymbol] const symbolAccount = walletAccounts.find( (a) => a.account.mint.toString() === symbolMint ) if (symbolAccount) { setSelectedAccount(symbolAccount) } else { setSelectedAccount(null) } } }, [tokenSymbol]) useEffect(() => { if (!selectedMangoGroup || !selectedMarginAccount || !selectedAccount) return const mintDecimals = selectedMangoGroup.mintDecimals[tokenIndex] const groupIndex = selectedMangoGroup.indexes[tokenIndex] const deposits = selectedMarginAccount.getUiDeposit( selectedMangoGroup, tokenIndex ) // simulate change to deposits based on input amount const newDeposit = 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 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, }) }, [ inputAmount, prices, tokenIndex, selectedMarginAccount, selectedMangoGroup, ]) const handleAccountSelect = (account) => { setInputAmount(0) setSliderPercentage(0) setInvalidAmountMessage('') setSelectedAccount(account) } // TODO: remove duplication in AccountSelect const getBalanceForAccount = (account) => { const mintAddress = account?.account.mint.toString() const balance = nativeToUi( account?.account?.amount, mintDecimals[getTokenIndex(mintAddress)] ) return balance } const setMaxForSelectedAccount = () => { const max = getBalanceForAccount(selectedAccount) setInputAmount(max) setSliderPercentage(100) setInvalidAmountMessage('') setMaxButtonTransition(true) } const handleDeposit = () => { setSubmitting(true) const marginAccount = useMangoStore.getState().selectedMarginAccount.current const mangoGroup = useMangoStore.getState().selectedMangoGroup.current const wallet = useMangoStore.getState().wallet.current if (!marginAccount && mangoGroup) { initMarginAccountAndDeposit( connection, new PublicKey(programId), mangoGroup, wallet, selectedAccount.account.mint, selectedAccount.publicKey, Number(inputAmount) ) .then(async (_response: Array) => { await sleep(1000) actions.fetchWalletBalances() actions.fetchMarginAccounts() setSubmitting(false) onClose() }) .catch((err) => { setSubmitting(false) console.error(err) notify({ message: 'Could not perform init margin account and deposit operation', type: 'error', }) onClose() }) } else { deposit( connection, new PublicKey(programId), mangoGroup, marginAccount, wallet, selectedAccount.account.mint, selectedAccount.publicKey, Number(inputAmount) ) .then(async (_response: string) => { setSubmitting(false) onClose() await sleep(750) actions.fetchWalletBalances() actions.fetchMarginAccounts() }) .catch((err) => { setSubmitting(false) console.error(err) notify({ message: 'Could not perform deposit operation', type: 'error', }) onClose() }) } } const renderAccountRiskStatus = ( collateralRatio: number, isRiskLevel?: boolean, isStatusIcon?: boolean ) => { if (collateralRatio < 1.25) { return isRiskLevel ? (
High
) : isStatusIcon ? ( 'bg-th-red' ) : ( 'border-th-red text-th-red' ) } else if (collateralRatio > 1.25 && collateralRatio < 1.5) { return isRiskLevel ? (
Moderate
) : isStatusIcon ? ( 'bg-th-orange' ) : ( 'border-th-orange text-th-orange' ) } else { return isRiskLevel ? (
Low
) : isStatusIcon ? ( 'bg-th-green' ) : ( 'border-th-green text-th-green' ) } } const validateAmountInput = (amount) => { if (Number(amount) <= 0) { setInvalidAmountMessage('Enter an amount to deposit') } if (Number(amount) > getBalanceForAccount(selectedAccount)) { setInvalidAmountMessage( 'Insufficient balance. Reduce the amount to deposit' ) } } const onChangeAmountInput = (amount) => { const max = getBalanceForAccount(selectedAccount) setInputAmount(amount) setSliderPercentage((amount / max) * 100) setInvalidAmountMessage('') } const onChangeSlider = async (percentage) => { const max = getBalanceForAccount(selectedAccount) const amount = (percentage / 100) * max if (percentage === 100) { setInputAmount(amount) } else { setInputAmount(trimDecimals(amount, DECIMALS[symbol])) } setSliderPercentage(percentage) setInvalidAmountMessage('') validateAmountInput(amount) } // turn off slider transition for dragging slider handle interaction useEffect(() => { if (maxButtonTransition) { setMaxButtonTransition(false) } }, [maxButtonTransition]) return ( {!showSimulation ? ( <> Deposit Funds {tokenSymbol && !selectedAccount ? ( ) : null} {settleDeficit ? ( ) : null}
Amount
Max
validateAmountInput(e.target.value)} value={inputAmount} onChange={(e) => onChangeAmountInput(e.target.value)} suffix={symbol} /> {/* {simulation ? ( {simulation?.leverage < 5 ? simulation?.leverage.toFixed(2) : '>5'} x ) : null} */}
{invalidAmountMessage ? (
{invalidAmountMessage}
) : null}
onChangeSlider(v)} step={1} maxButtonTransition={maxButtonTransition} />
) : ( <> Confirm Deposit
{`You're about to deposit`}
{inputAmount} {symbol}
{simulation ? ( {({ open }) => ( <>
Account Health Check
{open ? ( ) : ( )}
Account Value
${simulation?.assetsVal.toFixed(2)}
Account Risk
{renderAccountRiskStatus( 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}
)}
) : null}
setShowSimulation(false)} > Back )}
) } export default React.memo(DepositModal)