2023-07-18 05:13:08 -07:00
|
|
|
import {
|
|
|
|
FunctionComponent,
|
|
|
|
useCallback,
|
|
|
|
useEffect,
|
|
|
|
useMemo,
|
|
|
|
useState,
|
|
|
|
} from 'react'
|
2023-01-19 18:12:51 -08:00
|
|
|
import mangoStore from '@store/mangoStore'
|
|
|
|
import { useTranslation } from 'next-i18next'
|
|
|
|
import {
|
|
|
|
PerpOrderSide,
|
|
|
|
PerpOrderType,
|
|
|
|
PerpPosition,
|
|
|
|
} from '@blockworks-foundation/mango-v4'
|
|
|
|
import Modal from '@components/shared/Modal'
|
|
|
|
import Button, { LinkButton } from '@components/shared/Button'
|
|
|
|
import { calculateEstPriceForBaseSize } from 'utils/tradeForm'
|
|
|
|
import { notify } from 'utils/notifications'
|
|
|
|
import Loading from '@components/shared/Loading'
|
2023-01-22 18:03:09 -08:00
|
|
|
import useLocalStorageState from 'hooks/useLocalStorageState'
|
|
|
|
import { SOUND_SETTINGS_KEY } from 'utils/constants'
|
|
|
|
import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings'
|
|
|
|
import { Howl } from 'howler'
|
2023-02-27 23:20:11 -08:00
|
|
|
import { isMangoError } from 'types'
|
2023-07-17 18:18:01 -07:00
|
|
|
import { decodeBook, decodeBookL2 } from 'utils/orderbook'
|
2023-07-18 05:13:08 -07:00
|
|
|
import InlineNotification from '@components/shared/InlineNotification'
|
2023-01-19 18:12:51 -08:00
|
|
|
|
|
|
|
interface MarketCloseModalProps {
|
|
|
|
onClose: () => void
|
|
|
|
isOpen: boolean
|
|
|
|
position: PerpPosition
|
|
|
|
}
|
|
|
|
|
2023-01-22 18:03:09 -08:00
|
|
|
const set = mangoStore.getState().set
|
|
|
|
|
|
|
|
const successSound = new Howl({
|
|
|
|
src: ['/sounds/swap-success.mp3'],
|
|
|
|
volume: 0.5,
|
|
|
|
})
|
|
|
|
|
2023-03-28 03:12:11 -07:00
|
|
|
type BidsAndAsks = number[][] | null
|
|
|
|
|
2023-01-19 18:12:51 -08:00
|
|
|
const MarketCloseModal: FunctionComponent<MarketCloseModalProps> = ({
|
|
|
|
onClose,
|
|
|
|
isOpen,
|
|
|
|
position,
|
|
|
|
}) => {
|
2023-01-19 19:43:44 -08:00
|
|
|
const { t } = useTranslation(['common', 'trade'])
|
2023-01-19 18:12:51 -08:00
|
|
|
const [submitting, setSubmitting] = useState(false)
|
2023-03-28 03:12:11 -07:00
|
|
|
const connection = mangoStore((s) => s.connection)
|
|
|
|
const group = mangoStore((s) => s.group)
|
2023-01-22 18:03:09 -08:00
|
|
|
const [soundSettings] = useLocalStorageState(
|
|
|
|
SOUND_SETTINGS_KEY,
|
2023-07-21 11:47:53 -07:00
|
|
|
INITIAL_SOUND_SETTINGS,
|
2023-01-22 18:03:09 -08:00
|
|
|
)
|
2023-03-28 03:12:11 -07:00
|
|
|
const [asks, setAsks] = useState<BidsAndAsks>(null)
|
|
|
|
const [bids, setBids] = useState<BidsAndAsks>(null)
|
2023-01-19 18:12:51 -08:00
|
|
|
|
2023-07-18 10:01:14 -07:00
|
|
|
const perpMarket = useMemo(() => {
|
|
|
|
return group?.getPerpMarketByMarketIndex(position.marketIndex)
|
|
|
|
}, [group, position?.marketIndex])
|
|
|
|
|
2023-03-28 03:12:11 -07:00
|
|
|
// subscribe to the bids and asks orderbook accounts
|
|
|
|
useEffect(() => {
|
|
|
|
console.log('setting up orderbook websockets')
|
2023-01-19 18:12:51 -08:00
|
|
|
const client = mangoStore.getState().client
|
2023-03-28 03:12:11 -07:00
|
|
|
if (!group || !perpMarket) return
|
2023-01-19 18:12:51 -08:00
|
|
|
|
2023-03-28 03:12:11 -07:00
|
|
|
let bidSubscriptionId: number | undefined = undefined
|
|
|
|
let askSubscriptionId: number | undefined = undefined
|
|
|
|
let lastSeenBidsSlot: number
|
|
|
|
let lastSeenAsksSlot: number
|
|
|
|
const bidsPk = perpMarket.bids
|
|
|
|
if (bidsPk) {
|
|
|
|
connection
|
|
|
|
.getAccountInfoAndContext(bidsPk)
|
|
|
|
.then(({ context, value: info }) => {
|
|
|
|
if (!info) return
|
|
|
|
const decodedBook = decodeBook(client, perpMarket, info, 'bids')
|
|
|
|
setBids(decodeBookL2(decodedBook))
|
|
|
|
lastSeenBidsSlot = context.slot
|
|
|
|
})
|
|
|
|
bidSubscriptionId = connection.onAccountChange(
|
|
|
|
bidsPk,
|
|
|
|
(info, context) => {
|
|
|
|
if (context.slot > lastSeenBidsSlot) {
|
|
|
|
const decodedBook = decodeBook(client, perpMarket, info, 'bids')
|
|
|
|
setBids(decodeBookL2(decodedBook))
|
|
|
|
}
|
|
|
|
},
|
2023-07-21 11:47:53 -07:00
|
|
|
'processed',
|
2023-01-19 18:12:51 -08:00
|
|
|
)
|
2023-03-28 03:12:11 -07:00
|
|
|
}
|
2023-01-19 18:12:51 -08:00
|
|
|
|
2023-03-28 03:12:11 -07:00
|
|
|
const asksPk = perpMarket.asks
|
|
|
|
if (asksPk) {
|
|
|
|
connection
|
|
|
|
.getAccountInfoAndContext(asksPk)
|
|
|
|
.then(({ context, value: info }) => {
|
|
|
|
if (!info) return
|
|
|
|
const decodedBook = decodeBook(client, perpMarket, info, 'asks')
|
|
|
|
setAsks(decodeBookL2(decodedBook))
|
|
|
|
lastSeenAsksSlot = context.slot
|
|
|
|
})
|
|
|
|
askSubscriptionId = connection.onAccountChange(
|
|
|
|
asksPk,
|
|
|
|
(info, context) => {
|
|
|
|
if (context.slot > lastSeenAsksSlot) {
|
|
|
|
const decodedBook = decodeBook(client, perpMarket, info, 'asks')
|
|
|
|
setAsks(decodeBookL2(decodedBook))
|
|
|
|
}
|
|
|
|
},
|
2023-07-21 11:47:53 -07:00
|
|
|
'processed',
|
2023-01-19 18:12:51 -08:00
|
|
|
)
|
2023-03-28 03:12:11 -07:00
|
|
|
}
|
|
|
|
return () => {
|
|
|
|
if (typeof bidSubscriptionId !== 'undefined') {
|
|
|
|
connection.removeAccountChangeListener(bidSubscriptionId)
|
|
|
|
}
|
|
|
|
if (typeof askSubscriptionId !== 'undefined') {
|
|
|
|
connection.removeAccountChangeListener(askSubscriptionId)
|
2023-01-22 18:03:09 -08:00
|
|
|
}
|
2023-03-28 03:12:11 -07:00
|
|
|
}
|
2023-03-28 12:23:30 -07:00
|
|
|
}, [connection, perpMarket, group])
|
2023-03-28 03:12:11 -07:00
|
|
|
|
2023-07-18 05:13:08 -07:00
|
|
|
const insufficientLiquidity = useMemo(() => {
|
2023-08-27 21:05:42 -07:00
|
|
|
if (!perpMarket) return false
|
2023-07-18 05:13:08 -07:00
|
|
|
const baseSize = position.getBasePositionUi(perpMarket)
|
|
|
|
const isBids = baseSize < 0
|
|
|
|
if (isBids) {
|
2023-08-27 21:05:42 -07:00
|
|
|
if (!bids || !bids.length) return false
|
2023-07-18 05:13:08 -07:00
|
|
|
const liquidityMax = bids.reduce((a, c) => a + c[1], 0)
|
|
|
|
return liquidityMax < baseSize
|
|
|
|
} else {
|
2023-08-27 21:05:42 -07:00
|
|
|
if (!asks || !asks.length) return false
|
2023-07-18 05:13:08 -07:00
|
|
|
const liquidityMax = asks.reduce((a, c) => a + c[1], 0)
|
|
|
|
return liquidityMax < baseSize
|
|
|
|
}
|
|
|
|
}, [perpMarket, position, bids, asks])
|
|
|
|
|
2023-03-28 03:12:11 -07:00
|
|
|
const handleMarketClose = useCallback(
|
|
|
|
async (bids: BidsAndAsks, asks: BidsAndAsks) => {
|
|
|
|
const client = mangoStore.getState().client
|
|
|
|
const mangoAccount = mangoStore.getState().mangoAccount.current
|
|
|
|
const actions = mangoStore.getState().actions
|
|
|
|
|
|
|
|
if (!group || !mangoAccount || !perpMarket || !bids || !asks) {
|
2023-02-27 23:20:11 -08:00
|
|
|
notify({
|
2023-03-28 03:12:11 -07:00
|
|
|
title: 'Something went wrong. Try again later',
|
2023-02-27 23:20:11 -08:00
|
|
|
type: 'error',
|
|
|
|
})
|
2023-03-28 03:12:11 -07:00
|
|
|
return
|
2023-02-27 23:20:11 -08:00
|
|
|
}
|
2023-03-28 03:12:11 -07:00
|
|
|
setSubmitting(true)
|
|
|
|
try {
|
|
|
|
const baseSize = position.getBasePositionUi(perpMarket)
|
|
|
|
const sideToClose = baseSize > 0 ? 'sell' : 'buy'
|
|
|
|
const orderbook = { bids, asks }
|
|
|
|
const price = calculateEstPriceForBaseSize(
|
|
|
|
orderbook,
|
|
|
|
baseSize,
|
2023-07-21 11:47:53 -07:00
|
|
|
sideToClose,
|
2023-03-28 03:12:11 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
const maxSlippage = 0.025
|
2023-08-12 11:40:09 -07:00
|
|
|
const { signature: tx } = await client.perpPlaceOrder(
|
2023-03-28 03:12:11 -07:00
|
|
|
group,
|
|
|
|
mangoAccount,
|
|
|
|
perpMarket.perpMarketIndex,
|
|
|
|
sideToClose === 'buy' ? PerpOrderSide.bid : PerpOrderSide.ask,
|
|
|
|
price * (sideToClose === 'buy' ? 1 + maxSlippage : 1 - maxSlippage),
|
|
|
|
Math.abs(baseSize) * 2, // send a larger size to ensure full order is closed
|
|
|
|
undefined, // maxQuoteQuantity
|
|
|
|
Date.now(),
|
|
|
|
PerpOrderType.immediateOrCancel,
|
|
|
|
true, // reduce only
|
|
|
|
undefined,
|
2023-07-21 11:47:53 -07:00
|
|
|
undefined,
|
2023-03-28 03:12:11 -07:00
|
|
|
)
|
|
|
|
actions.fetchOpenOrders()
|
|
|
|
set((s) => {
|
|
|
|
s.successAnimation.trade = true
|
|
|
|
})
|
|
|
|
if (soundSettings['swap-success']) {
|
|
|
|
successSound.play()
|
|
|
|
}
|
|
|
|
notify({
|
|
|
|
type: 'success',
|
|
|
|
title: 'Transaction successful',
|
|
|
|
txid: tx,
|
|
|
|
})
|
|
|
|
} catch (e) {
|
|
|
|
if (isMangoError(e)) {
|
|
|
|
notify({
|
|
|
|
title: 'There was an issue.',
|
|
|
|
description: e.message,
|
|
|
|
txid: e?.txid,
|
|
|
|
type: 'error',
|
|
|
|
})
|
|
|
|
}
|
|
|
|
console.error('Place trade error:', e)
|
|
|
|
} finally {
|
|
|
|
setSubmitting(false)
|
|
|
|
onClose()
|
|
|
|
}
|
|
|
|
},
|
2023-07-21 11:47:53 -07:00
|
|
|
[perpMarket, position, group, onClose, soundSettings],
|
2023-03-28 03:12:11 -07:00
|
|
|
)
|
2023-01-19 18:12:51 -08:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Modal onClose={onClose} isOpen={isOpen}>
|
2023-03-04 09:46:35 -08:00
|
|
|
<h3 className="mb-2 text-center">
|
2023-01-19 19:43:44 -08:00
|
|
|
{t('trade:close-confirm', { config_name: perpMarket?.name })}
|
2023-01-19 18:12:51 -08:00
|
|
|
</h3>
|
2023-01-19 19:43:44 -08:00
|
|
|
<div className="pb-6 text-th-fgd-3">{t('trade:price-expect')}</div>
|
2023-07-18 05:13:08 -07:00
|
|
|
{insufficientLiquidity ? (
|
|
|
|
<div className="mb-3">
|
|
|
|
<InlineNotification
|
|
|
|
type="error"
|
|
|
|
desc={t('trade:insufficient-perp-liquidity')}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
) : null}
|
2023-01-20 03:03:31 -08:00
|
|
|
<Button
|
|
|
|
className="mb-4 flex w-full items-center justify-center"
|
2023-07-18 05:13:08 -07:00
|
|
|
disabled={insufficientLiquidity}
|
2023-03-28 03:12:11 -07:00
|
|
|
onClick={() => handleMarketClose(bids, asks)}
|
2023-01-20 03:03:31 -08:00
|
|
|
size="large"
|
|
|
|
>
|
2023-01-19 19:43:44 -08:00
|
|
|
{submitting ? <Loading /> : <span>{t('trade:close-position')}</span>}
|
|
|
|
</Button>
|
|
|
|
<LinkButton
|
2023-04-20 19:32:20 -07:00
|
|
|
className="inline-flex w-full items-center justify-center"
|
2023-01-19 19:43:44 -08:00
|
|
|
onClick={onClose}
|
|
|
|
>
|
|
|
|
{t('cancel')}
|
|
|
|
</LinkButton>
|
2023-01-19 18:12:51 -08:00
|
|
|
</Modal>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export default MarketCloseModal
|