mango-v4-ui/components/trade/TradeHotKeys.tsx

394 lines
12 KiB
TypeScript
Raw Normal View History

2023-06-21 17:48:15 -07:00
import {
Group,
2023-06-24 05:44:15 -07:00
MangoAccount,
2023-06-21 17:48:15 -07:00
PerpMarket,
PerpOrderSide,
PerpOrderType,
Serum3Market,
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from '@blockworks-foundation/mango-v4'
import { HotKey } from '@components/settings/HotKeysSettings'
import mangoStore from '@store/mangoStore'
2023-06-24 05:44:15 -07:00
import { ReactNode, useCallback } from 'react'
2023-06-21 17:48:15 -07:00
import Hotkeys from 'react-hot-keys'
2023-06-24 05:44:15 -07:00
import { GenericMarket, isMangoError } from 'types'
2023-06-21 17:48:15 -07:00
import { HOT_KEYS_KEY, SOUND_SETTINGS_KEY } from 'utils/constants'
import { notify } from 'utils/notifications'
import { calculateLimitPriceForMarketOrder } from 'utils/tradeForm'
import { successSound } from './AdvancedTradeForm'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
import useMangoAccount from 'hooks/useMangoAccount'
import { Market } from '@project-serum/serum'
import { useRouter } from 'next/router'
2023-06-22 20:37:34 -07:00
import useUnownedAccount from 'hooks/useUnownedAccount'
2023-06-24 05:44:15 -07:00
import { useTranslation } from 'next-i18next'
2023-06-21 17:48:15 -07:00
const set = mangoStore.getState().set
const calcBaseSize = (
orderDetails: HotKey,
maxSize: number,
market: PerpMarket | Market,
oraclePrice: number,
quoteTokenIndex: number,
group: Group,
2023-07-21 11:47:53 -07:00
limitPrice?: number,
2023-06-21 17:48:15 -07:00
) => {
const { orderSize, orderSide, orderSizeType, orderType } = orderDetails
let baseSize: number
2023-06-24 05:44:15 -07:00
let quoteSize: number
if (orderSide === 'buy') {
// assumes USDC = $1 as tokenIndex is 0
if (!quoteTokenIndex) {
quoteSize =
orderSizeType === 'percentage'
? (Number(orderSize) / 100) * maxSize
: Number(orderSize)
} else {
// required for non USDC quote tokens
const quoteBank = group.getFirstBankByTokenIndex(quoteTokenIndex)
const quotePrice = quoteBank.uiPrice
const orderSizeInQuote = Number(orderSize) / quotePrice
quoteSize =
orderSizeType === 'percentage'
? (orderSizeInQuote / 100) * maxSize
: orderSizeInQuote
}
if (orderType === 'market') {
2023-06-21 17:48:15 -07:00
baseSize = floorToDecimal(
quoteSize / oraclePrice,
2023-07-21 11:47:53 -07:00
getDecimalCount(market.minOrderSize),
2023-06-21 17:48:15 -07:00
).toNumber()
} else {
2023-06-24 05:44:15 -07:00
const price = limitPrice ? limitPrice : 0
baseSize = floorToDecimal(
quoteSize / price,
2023-07-21 11:47:53 -07:00
getDecimalCount(market.minOrderSize),
2023-06-24 05:44:15 -07:00
).toNumber()
}
} else {
if (orderSizeType === 'percentage') {
baseSize = floorToDecimal(
(Number(orderSize) / 100) * maxSize,
2023-07-21 11:47:53 -07:00
getDecimalCount(market.minOrderSize),
2023-06-24 05:44:15 -07:00
).toNumber()
} else {
if (orderType === 'market') {
2023-06-21 17:48:15 -07:00
baseSize = floorToDecimal(
2023-06-24 05:44:15 -07:00
Number(orderSize) / oraclePrice,
2023-07-21 11:47:53 -07:00
getDecimalCount(market.minOrderSize),
2023-06-21 17:48:15 -07:00
).toNumber()
} else {
2023-06-24 05:44:15 -07:00
const price = limitPrice ? limitPrice : 0
2023-06-21 17:48:15 -07:00
baseSize = floorToDecimal(
2023-06-24 05:44:15 -07:00
Number(orderSize) / price,
2023-07-21 11:47:53 -07:00
getDecimalCount(market.minOrderSize),
2023-06-21 17:48:15 -07:00
).toNumber()
}
}
}
return baseSize
}
2023-06-24 05:44:15 -07:00
const calcSpotMarketMax = (
mangoAccount: MangoAccount | undefined,
selectedMarket: GenericMarket | undefined,
side: string,
2023-07-21 11:47:53 -07:00
useMargin: boolean,
2023-06-24 05:44:15 -07:00
) => {
const spotBalances = mangoStore.getState().mangoAccount.spotBalances
const group = mangoStore.getState().group
if (!mangoAccount || !group || !selectedMarket) return 0
if (!(selectedMarket instanceof Serum3Market)) return 0
let leverageMax = 0
let spotMax = 0
try {
if (side === 'buy') {
leverageMax = mangoAccount.getMaxQuoteForSerum3BidUi(
group,
2023-07-21 11:47:53 -07:00
selectedMarket.serumMarketExternal,
2023-06-24 05:44:15 -07:00
)
const bank = group.getFirstBankByTokenIndex(
2023-07-21 11:47:53 -07:00
selectedMarket.quoteTokenIndex,
2023-06-24 05:44:15 -07:00
)
const balance = mangoAccount.getTokenBalanceUi(bank)
const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0
spotMax = balance + unsettled
} else {
leverageMax = mangoAccount.getMaxBaseForSerum3AskUi(
group,
2023-07-21 11:47:53 -07:00
selectedMarket.serumMarketExternal,
2023-06-24 05:44:15 -07:00
)
const bank = group.getFirstBankByTokenIndex(selectedMarket.baseTokenIndex)
const balance = mangoAccount.getTokenBalanceUi(bank)
const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0
spotMax = balance + unsettled
}
return useMargin ? leverageMax : Math.max(spotMax, 0)
} catch (e) {
console.error('Error calculating max size: ', e)
return 0
}
}
const calcPerpMax = (
mangoAccount: MangoAccount,
selectedMarket: GenericMarket,
2023-07-21 11:47:53 -07:00
side: string,
2023-06-24 05:44:15 -07:00
) => {
const group = mangoStore.getState().group
if (
!mangoAccount ||
!group ||
!selectedMarket ||
selectedMarket instanceof Serum3Market
)
return 0
try {
if (side === 'buy') {
return mangoAccount.getMaxQuoteForPerpBidUi(
group,
2023-07-21 11:47:53 -07:00
selectedMarket.perpMarketIndex,
2023-06-24 05:44:15 -07:00
)
} else {
return mangoAccount.getMaxBaseForPerpAskUi(
group,
2023-07-21 11:47:53 -07:00
selectedMarket.perpMarketIndex,
2023-06-24 05:44:15 -07:00
)
}
} catch (e) {
console.error('Error calculating max leverage: ', e)
return 0
}
}
2023-06-21 17:48:15 -07:00
const TradeHotKeys = ({ children }: { children: ReactNode }) => {
2023-06-24 05:44:15 -07:00
const { t } = useTranslation(['common', 'settings'])
const { price: oraclePrice, serumOrPerpMarket } = useSelectedMarket()
const { mangoAccountAddress } = useMangoAccount()
2023-06-22 20:37:34 -07:00
const { isUnownedAccount } = useUnownedAccount()
2023-06-21 17:48:15 -07:00
const { asPath } = useRouter()
const [hotKeys] = useLocalStorageState(HOT_KEYS_KEY, [])
const [soundSettings] = useLocalStorageState(
SOUND_SETTINGS_KEY,
2023-07-21 11:47:53 -07:00
INITIAL_SOUND_SETTINGS,
2023-06-21 17:48:15 -07:00
)
const handlePlaceOrder = useCallback(
async (hkOrder: HotKey) => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
const actions = mangoStore.getState().actions
const selectedMarket = mangoStore.getState().selectedMarket.current
2023-06-24 05:44:15 -07:00
const {
ioc,
orderPrice,
orderSide,
orderType,
postOnly,
reduceOnly,
margin,
} = hkOrder
2023-06-21 17:48:15 -07:00
if (!group || !mangoAccount || !serumOrPerpMarket || !selectedMarket)
return
try {
const orderMax =
2023-06-24 05:44:15 -07:00
serumOrPerpMarket instanceof PerpMarket
? calcPerpMax(mangoAccount, selectedMarket, orderSide)
: calcSpotMarketMax(mangoAccount, selectedMarket, orderSide, margin)
2023-06-21 17:48:15 -07:00
const quoteTokenIndex =
selectedMarket instanceof PerpMarket
? 0
: selectedMarket.quoteTokenIndex
let baseSize: number
let price: number
if (orderType === 'market') {
baseSize = calcBaseSize(
hkOrder,
orderMax,
serumOrPerpMarket,
oraclePrice,
quoteTokenIndex,
2023-07-21 11:47:53 -07:00
group,
2023-06-21 17:48:15 -07:00
)
const orderbook = mangoStore.getState().selectedMarket.orderbook
price = calculateLimitPriceForMarketOrder(
orderbook,
baseSize,
2023-07-21 11:47:53 -07:00
orderSide,
2023-06-21 17:48:15 -07:00
)
} else {
// change in price from oracle for limit order
const priceChange = (Number(orderPrice) / 100) * oraclePrice
// subtract price change for buy limit, add for sell limit
const rawPrice =
orderSide === 'buy'
? oraclePrice - priceChange
: oraclePrice + priceChange
price = floorToDecimal(
rawPrice,
2023-07-21 11:47:53 -07:00
getDecimalCount(serumOrPerpMarket.tickSize),
2023-06-21 17:48:15 -07:00
).toNumber()
baseSize = calcBaseSize(
hkOrder,
orderMax,
serumOrPerpMarket,
oraclePrice,
quoteTokenIndex,
group,
2023-07-21 11:47:53 -07:00
price,
2023-06-21 17:48:15 -07:00
)
}
2023-06-24 05:44:15 -07:00
// check if size < max
if (orderSide === 'buy') {
if (baseSize * price > orderMax) {
notify({
type: 'error',
title: t('settings:error-order-exceeds-max'),
})
return
}
} else {
console.log(baseSize, orderMax)
if (baseSize > orderMax) {
notify({
type: 'error',
title: t('settings:error-order-exceeds-max'),
})
return
}
}
notify({
type: 'info',
title: t('settings:placing-order'),
description: `${t(orderSide)} ${baseSize} ${selectedMarket.name} ${
orderType === 'limit'
? `${t('settings:at')} ${price}`
: `${t('settings:at')} ${t('market')}`
}`,
})
2023-06-21 17:48:15 -07:00
if (selectedMarket instanceof Serum3Market) {
const spotOrderType = ioc
? Serum3OrderType.immediateOrCancel
: postOnly && orderType !== 'market'
? Serum3OrderType.postOnly
: Serum3OrderType.limit
const tx = await client.serum3PlaceOrder(
group,
mangoAccount,
selectedMarket.serumMarketExternal,
orderSide === 'buy' ? Serum3Side.bid : Serum3Side.ask,
price,
baseSize,
Serum3SelfTradeBehavior.decrementTake,
spotOrderType,
Date.now(),
2023-07-21 11:47:53 -07:00
10,
2023-06-21 17:48:15 -07:00
)
actions.fetchOpenOrders(true)
set((s) => {
s.successAnimation.trade = true
})
if (soundSettings['swap-success']) {
successSound.play()
}
notify({
type: 'success',
title: 'Transaction successful',
txid: tx.signature,
2023-06-21 17:48:15 -07:00
})
} else if (selectedMarket instanceof PerpMarket) {
const perpOrderType =
orderType === 'market'
? PerpOrderType.market
: ioc
? PerpOrderType.immediateOrCancel
: postOnly
? PerpOrderType.postOnly
: PerpOrderType.limit
console.log('perpOrderType', perpOrderType)
const tx = await client.perpPlaceOrder(
group,
mangoAccount,
selectedMarket.perpMarketIndex,
orderSide === 'buy' ? PerpOrderSide.bid : PerpOrderSide.ask,
price,
Math.abs(baseSize),
undefined, // maxQuoteQuantity
Date.now(),
perpOrderType,
selectedMarket.reduceOnly || reduceOnly,
undefined,
2023-07-21 11:47:53 -07:00
undefined,
2023-06-21 17:48:15 -07:00
)
actions.fetchOpenOrders(true)
set((s) => {
s.successAnimation.trade = true
})
if (soundSettings['swap-success']) {
successSound.play()
}
notify({
type: 'success',
title: 'Transaction successful',
txid: tx.signature,
2023-06-21 17:48:15 -07:00
})
}
} catch (e) {
console.error('Place trade error:', e)
if (!isMangoError(e)) return
notify({
title: 'There was an issue.',
description: e.message,
txid: e?.txid,
type: 'error',
})
}
},
2023-07-21 11:47:53 -07:00
[serumOrPerpMarket],
2023-06-21 17:48:15 -07:00
)
const onKeyDown = useCallback(
(keyName: string) => {
const orderDetails = hotKeys.find(
2023-07-21 11:47:53 -07:00
(hk: HotKey) => hk.keySequence === keyName,
2023-06-21 17:48:15 -07:00
)
if (orderDetails) {
handlePlaceOrder(orderDetails)
}
},
2023-07-21 11:47:53 -07:00
[handlePlaceOrder, hotKeys],
2023-06-21 17:48:15 -07:00
)
2023-06-22 20:37:34 -07:00
const showHotKeys =
hotKeys.length &&
asPath.includes('/trade') &&
mangoAccountAddress &&
!isUnownedAccount
return showHotKeys ? (
2023-06-21 17:48:15 -07:00
<Hotkeys
keyName={hotKeys.map((k: HotKey) => k.keySequence).toString()}
onKeyDown={onKeyDown}
>
{children}
</Hotkeys>
) : (
<>{children}</>
)
}
export default TradeHotKeys