mango-v4-ui/hooks/useTradeHistory.ts

264 lines
7.5 KiB
TypeScript
Raw Normal View History

2023-04-13 04:25:58 -07:00
import {
Group,
PerpMarket,
Serum3Market,
} from '@blockworks-foundation/mango-v4'
import { PublicKey } from '@solana/web3.js'
import mangoStore from '@store/mangoStore'
2023-03-25 14:16:42 -07:00
import { useInfiniteQuery } from '@tanstack/react-query'
2023-04-13 04:25:58 -07:00
import { useMemo } from 'react'
2023-03-25 14:16:42 -07:00
import {
2023-04-13 04:25:58 -07:00
CombinedTradeHistoryTypes,
2023-03-25 14:16:42 -07:00
EmptyObject,
2023-04-13 04:25:58 -07:00
isApiSpotTradeHistory,
isPerpFillEvent,
isSerumFillEvent,
PerpFillEvent,
2023-03-25 14:16:42 -07:00
PerpTradeHistory,
2023-04-13 04:25:58 -07:00
SerumEvent,
2023-03-25 14:16:42 -07:00
SpotTradeHistory,
TradeHistoryApiResponseType,
} from 'types'
import { MANGO_DATA_API_URL, PAGINATION_PAGE_LENGTH } from 'utils/constants'
import useMangoAccount from './useMangoAccount'
2023-04-13 04:25:58 -07:00
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,
}
}
2023-04-20 03:55:52 -07:00
export const parseApiTradeHistory = (
2023-04-13 04:25:58 -07:00
mangoAccountAddress: string,
2023-07-21 11:47:53 -07:00
trade: SpotTradeHistory | PerpTradeHistory,
2023-04-13 04:25:58 -07:00
) => {
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,
}
}
2023-04-17 16:45:27 -07:00
export const formatTradeHistory = (
2023-04-13 04:25:58 -07:00
group: Group,
selectedMarket: Serum3Market | PerpMarket,
mangoAccountAddress: string,
2023-07-21 11:47:53 -07:00
tradeHistory: Array<CombinedTradeHistoryTypes>,
2023-04-13 04:25:58 -07:00
) => {
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(
2023-07-21 11:47:53 -07:00
new PublicKey(trade.market),
2023-04-13 04:25:58 -07:00
)
} else if ('perp_market' in trade) {
market = group.getPerpMarketByMarketIndex(trade.market_index)
}
}
return {
...trade,
market,
time,
}
})
}
2023-03-25 14:16:42 -07:00
2023-04-16 19:13:50 -07:00
const filterNewFills = (
eventQueueFills: (SerumEvent | PerpFillEvent)[],
2023-07-21 11:47:53 -07:00
apiTradeHistory: (SpotTradeHistory | PerpTradeHistory)[],
2023-04-16 19:13:50 -07:00
): (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(
2023-07-21 11:47:53 -07:00
fill.timestamp.toNumber() * 1000,
2023-04-16 19:13:50 -07:00
).getTime()
const lastApiTradeTimestamp = new Date(
2023-07-21 11:47:53 -07:00
apiTradeHistory[apiTradeHistory.length - 1].block_datetime,
2023-04-16 19:13:50 -07:00
).getTime()
if (fillTimestamp < lastApiTradeTimestamp) return true
return trade.seq_num === fill.seqNum.toNumber()
}
})
})
}
2023-03-25 14:16:42 -07:00
const isTradeHistory = (
2023-07-21 11:47:53 -07:00
response: null | EmptyObject | TradeHistoryApiResponseType[],
2023-03-25 14:16:42 -07:00
): response is TradeHistoryApiResponseType[] => {
if (
response &&
Array.isArray(response) &&
'activity_details' in response[0]
) {
return true
}
return false
}
2023-04-20 03:55:52 -07:00
export const fetchTradeHistory = async (
2023-03-25 14:16:42 -07:00
mangoAccountAddress: string,
2023-04-20 03:55:52 -07:00
offset = 0,
2023-07-21 11:47:53 -07:00
limit?: number,
2023-03-25 14:16:42 -07:00
): Promise<Array<PerpTradeHistory | SpotTradeHistory>> => {
2023-04-20 03:55:52 -07:00
const tradesLimit = limit ? limit : PAGINATION_PAGE_LENGTH
2023-03-25 14:16:42 -07:00
const response = await fetch(
2023-07-21 11:47:53 -07:00
`${MANGO_DATA_API_URL}/stats/trade-history?mango-account=${mangoAccountAddress}&limit=${tradesLimit}&offset=${offset}`,
2023-03-25 14:16:42 -07:00
)
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() {
2023-04-13 04:25:58 -07:00
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',
2023-07-21 11:47:53 -07:00
selectedMarket.marketIndex,
2023-04-13 04:25:58 -07:00
)
}
}
}, [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])
2023-03-25 14:16:42 -07:00
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,
2023-07-21 11:47:53 -07:00
},
2023-03-25 14:16:42 -07:00
)
2023-04-13 04:25:58 -07:00
const combinedTradeHistory = useMemo(() => {
const group = mangoStore.getState().group
2023-04-16 19:13:50 -07:00
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,
2023-07-21 11:47:53 -07:00
combinedHistory,
2023-04-16 19:13:50 -07:00
)
2023-04-13 04:25:58 -07:00
}, [eventQueueFillsForOwner, mangoAccountAddress, response, selectedMarket])
return { ...response, data: combinedTradeHistory }
2023-03-25 14:16:42 -07:00
}