mango-v4-ui/hooks/useTradeHistory.ts

264 lines
7.5 KiB
TypeScript

import {
Group,
PerpMarket,
Serum3Market,
} from '@blockworks-foundation/mango-v4'
import { PublicKey } from '@solana/web3.js'
import mangoStore from '@store/mangoStore'
import { useInfiniteQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import {
CombinedTradeHistoryTypes,
EmptyObject,
isApiSpotTradeHistory,
isPerpFillEvent,
isSerumFillEvent,
PerpFillEvent,
PerpTradeHistory,
SerumEvent,
SpotTradeHistory,
TradeHistoryApiResponseType,
} from 'types'
import { MANGO_DATA_API_URL, PAGINATION_PAGE_LENGTH } from 'utils/constants'
import useMangoAccount from './useMangoAccount'
import useSelectedMarket from './useSelectedMarket'
const parsePerpEvent = (mangoAccountAddress: string, event: PerpFillEvent) => {
const maker = event.maker.toString() === mangoAccountAddress
const orderId = maker ? event.makerOrderId : event.takerOrderId
const value = event.quantity * event.price
const feeRate = maker ? event.makerFee : event.takerFee
const takerSide = event.takerSide === 0 ? 'buy' : 'sell'
const side = maker ? (takerSide === 'buy' ? 'sell' : 'buy') : takerSide
return {
...event,
key: orderId?.toString(),
liquidity: maker ? 'Maker' : 'Taker',
size: event.quantity,
price: event.price,
value,
feeCost: feeRate * value,
side,
}
}
const parseSerumEvent = (event: SerumEvent) => {
let liquidity
if (event.eventFlags) {
liquidity = event?.eventFlags?.maker ? 'Maker' : 'Taker'
}
return {
...event,
liquidity,
key: `${liquidity}-${event.price}`,
value: event.price * event.size,
side: event.side,
}
}
export const parseApiTradeHistory = (
mangoAccountAddress: string,
trade: SpotTradeHistory | PerpTradeHistory
) => {
let side: 'buy' | 'sell'
let size
let feeCost
let liquidity
if (isApiSpotTradeHistory(trade)) {
side = trade.side
size = trade.size
feeCost = trade.fee_cost
liquidity = trade.maker ? 'Maker' : 'Taker'
} else {
liquidity =
trade.taker && trade.taker === mangoAccountAddress ? 'Taker' : 'Maker'
if (liquidity == 'Taker') {
side = trade.taker_side == 'bid' ? 'buy' : 'sell'
} else {
side = trade.taker_side == 'bid' ? 'sell' : 'buy'
}
size = trade.quantity
const feeRate =
trade.maker === mangoAccountAddress ? trade.maker_fee : trade.taker_fee
feeCost = (trade.price * trade.quantity * feeRate).toFixed(5)
}
return {
...trade,
liquidity,
side,
size,
feeCost,
}
}
export const formatTradeHistory = (
group: Group,
selectedMarket: Serum3Market | PerpMarket,
mangoAccountAddress: string,
tradeHistory: Array<CombinedTradeHistoryTypes>
) => {
return tradeHistory.flat().map((event) => {
let trade
let market = selectedMarket
let time: string | number = ''
if (isSerumFillEvent(event)) {
trade = parseSerumEvent(event)
} else if (isPerpFillEvent(event)) {
trade = parsePerpEvent(mangoAccountAddress, event)
market = selectedMarket
time = trade.timestamp.toNumber() * 1000
} else {
trade = parseApiTradeHistory(mangoAccountAddress, event)
time = trade.block_datetime
if ('market' in trade) {
market = group.getSerum3MarketByExternalMarket(
new PublicKey(trade.market)
)
} else if ('perp_market' in trade) {
market = group.getPerpMarketByMarketIndex(trade.market_index)
}
}
return {
...trade,
market,
time,
}
})
}
const filterNewFills = (
eventQueueFills: (SerumEvent | PerpFillEvent)[],
apiTradeHistory: (SpotTradeHistory | PerpTradeHistory)[]
): (SerumEvent | PerpFillEvent)[] => {
return eventQueueFills.filter((fill) => {
return !apiTradeHistory.find((trade) => {
if ('order_id' in trade && isSerumFillEvent(fill)) {
return trade.order_id === fill.orderId.toString()
} else if ('seq_num' in trade && isPerpFillEvent(fill)) {
const fillTimestamp = new Date(
fill.timestamp.toNumber() * 1000
).getTime()
const lastApiTradeTimestamp = new Date(
apiTradeHistory[apiTradeHistory.length - 1].block_datetime
).getTime()
if (fillTimestamp < lastApiTradeTimestamp) return true
return trade.seq_num === fill.seqNum.toNumber()
}
})
})
}
const isTradeHistory = (
response: null | EmptyObject | TradeHistoryApiResponseType[]
): response is TradeHistoryApiResponseType[] => {
if (
response &&
Array.isArray(response) &&
'activity_details' in response[0]
) {
return true
}
return false
}
export const fetchTradeHistory = async (
mangoAccountAddress: string,
offset = 0,
limit?: number
): Promise<Array<PerpTradeHistory | SpotTradeHistory>> => {
const tradesLimit = limit ? limit : PAGINATION_PAGE_LENGTH
const response = await fetch(
`${MANGO_DATA_API_URL}/stats/trade-history?mango-account=${mangoAccountAddress}&limit=${tradesLimit}&offset=${offset}`
)
const jsonResponse: null | EmptyObject | TradeHistoryApiResponseType[] =
await response.json()
let data
if (isTradeHistory(jsonResponse)) {
data = jsonResponse.map((h) => h.activity_details)
}
return data ?? []
}
export default function useTradeHistory() {
const { selectedMarket } = useSelectedMarket()
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const fills = mangoStore((s) => s.selectedMarket.fills)
const openOrderOwner = useMemo(() => {
if (!mangoAccount || !selectedMarket) return
if (selectedMarket instanceof PerpMarket) {
return mangoAccount.publicKey
} else {
try {
return mangoAccount.getSerum3OoAccount(selectedMarket.marketIndex)
.address
} catch {
console.warn(
'Unable to find OO account for mkt index',
selectedMarket.marketIndex
)
}
}
}, [mangoAccount, selectedMarket])
const eventQueueFillsForOwner = useMemo(() => {
if (!selectedMarket || !openOrderOwner) return []
return fills.filter((fill) => {
if (isSerumFillEvent(fill)) {
// handles serum event queue for spot trades
return openOrderOwner ? fill.openOrders.equals(openOrderOwner) : false
} else if (isPerpFillEvent(fill)) {
// handles mango event queue for perp trades
return (
fill.taker.equals(openOrderOwner) || fill.maker.equals(openOrderOwner)
)
}
})
}, [selectedMarket, openOrderOwner, fills])
const response = useInfiniteQuery(
['trade-history', mangoAccountAddress],
({ pageParam }) => fetchTradeHistory(mangoAccountAddress, pageParam),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
enabled: !!mangoAccountAddress,
keepPreviousData: true,
refetchInterval: 1000 * 60 * 5,
getNextPageParam: (_lastPage, pages) =>
pages.length * PAGINATION_PAGE_LENGTH,
}
)
const combinedTradeHistory = useMemo(() => {
const group = mangoStore.getState().group
if (!group || !selectedMarket) return []
const apiTradeHistory = response.data?.pages.flat() ?? []
const newFills: (SerumEvent | PerpFillEvent)[] =
eventQueueFillsForOwner?.length
? filterNewFills(eventQueueFillsForOwner, apiTradeHistory)
: []
const combinedHistory = [...newFills, ...apiTradeHistory]
return formatTradeHistory(
group,
selectedMarket,
mangoAccountAddress,
combinedHistory
)
}, [eventQueueFillsForOwner, mangoAccountAddress, response, selectedMarket])
return { ...response, data: combinedTradeHistory }
}