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',
|
2023-08-12 11:40:09 -07:00
|
|
|
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',
|
2023-08-12 11:40:09 -07:00
|
|
|
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
|