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

452 lines
15 KiB
TypeScript
Raw Normal View History

2023-01-19 21:25:43 -08:00
import {
Group,
ParsedFillEvent,
2023-01-19 21:25:43 -08:00
PerpMarket,
Serum3Market,
} from '@blockworks-foundation/mango-v4'
2023-01-19 15:44:58 -08:00
import { IconButton, LinkButton } from '@components/shared/Button'
2023-01-19 17:45:08 -08:00
import ConnectEmptyState from '@components/shared/ConnectEmptyState'
2023-01-23 20:44:50 -08:00
import FormatNumericValue from '@components/shared/FormatNumericValue'
2023-01-12 16:16:10 -08:00
import SheenLoader from '@components/shared/SheenLoader'
2022-12-19 09:12:03 -08:00
import SideBadge from '@components/shared/SideBadge'
import {
Table,
TableDateDisplay,
Td,
Th,
TrBody,
TrHead,
} from '@components/shared/TableElements'
2023-01-19 15:44:58 -08:00
import Tooltip from '@components/shared/Tooltip'
import { NoSymbolIcon, UsersIcon } from '@heroicons/react/20/solid'
2023-01-19 17:45:08 -08:00
import { useWallet } from '@solana/wallet-adapter-react'
2023-01-06 16:26:06 -08:00
import { PublicKey } from '@solana/web3.js'
2022-12-19 09:12:03 -08:00
import mangoStore from '@store/mangoStore'
import dayjs from 'dayjs'
2022-12-19 09:12:03 -08:00
import useMangoAccount from 'hooks/useMangoAccount'
import useSelectedMarket from 'hooks/useSelectedMarket'
2022-12-20 20:48:41 -08:00
import { useViewport } from 'hooks/useViewport'
2023-01-12 16:16:10 -08:00
import { useTranslation } from 'next-i18next'
import { useCallback, useMemo, useState } from 'react'
import { SerumEvent, PerpTradeHistory, SpotTradeHistory } from 'types'
2023-01-12 16:16:10 -08:00
import { PAGINATION_PAGE_LENGTH } from 'utils/constants'
2023-02-15 11:31:31 -08:00
import { abbreviateAddress } from 'utils/formatting'
2022-12-20 20:48:41 -08:00
import { breakpoints } from 'utils/theme'
import MarketLogos from './MarketLogos'
import PerpSideBadge from './PerpSideBadge'
2022-12-19 09:12:03 -08:00
import TableMarketName from './TableMarketName'
type PerpFillEvent = ParsedFillEvent
const parsePerpEvent = (mangoAccountAddress: string, event: PerpFillEvent) => {
const maker = event.maker.toString() === mangoAccountAddress
2022-12-19 09:12:03 -08:00
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
2022-12-19 09:12:03 -08:00
return {
...event,
key: orderId?.toString(),
liquidity: maker ? 'Maker' : 'Taker',
size: event.quantity,
2022-12-19 09:12:03 -08:00
price: event.price,
value,
2023-02-20 12:07:16 -08:00
feeCost: feeRate * value,
2022-12-19 09:12:03 -08:00
side,
}
}
const parseSerumEvent = (event: SerumEvent) => {
2022-12-19 09:12:03 -08:00
let liquidity
if (event.eventFlags) {
liquidity = event?.eventFlags?.maker ? 'Maker' : 'Taker'
}
2022-12-19 09:12:03 -08:00
return {
...event,
liquidity,
key: `${liquidity}-${event.price}`,
2022-12-19 09:12:03 -08:00
value: event.price * event.size,
side: event.side,
}
}
const isApiSpotTradeHistory = (
t: SpotTradeHistory | PerpTradeHistory
): t is SpotTradeHistory => {
if ('open_orders' in t) return true
else return false
}
type CombinedTradeHistoryTypes =
| SpotTradeHistory
| PerpTradeHistory
| PerpFillEvent
| SerumEvent
export const isSerumFillEvent = (
t: CombinedTradeHistoryTypes
): t is SerumEvent => {
if ('eventFlags' in t) return true
else return false
}
export const isPerpFillEvent = (
t: CombinedTradeHistoryTypes
): t is PerpFillEvent => {
if ('takerSide' in t) return true
else return false
}
2023-01-19 21:25:43 -08:00
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'
2023-01-19 21:25:43 -08:00
} else {
side = trade.taker_side == 'bid' ? 'sell' : 'buy'
2023-01-19 21:25:43 -08:00
}
size = trade.quantity
2023-02-20 12:07:16 -08:00
const feeRate =
2023-01-19 21:25:43 -08:00
trade.maker === mangoAccountAddress ? trade.maker_fee : trade.taker_fee
2023-02-20 12:07:16 -08:00
feeCost = (trade.price * trade.quantity * feeRate).toFixed(5)
2023-01-19 21:25:43 -08:00
}
return {
...trade,
liquidity,
2023-01-19 21:25:43 -08:00
side,
size,
feeCost,
2023-01-19 21:25:43 -08:00
}
}
const formatTradeHistory = (
2023-01-19 21:25:43 -08:00
group: Group,
selectedMarket: Serum3Market | PerpMarket,
mangoAccountAddress: string,
tradeHistory: Array<CombinedTradeHistoryTypes>
) => {
return tradeHistory.flat().map((event) => {
let trade
let market = selectedMarket
2023-03-04 09:46:21 -08:00
let time: string | number = ''
if (isSerumFillEvent(event)) {
trade = parseSerumEvent(event)
} else if (isPerpFillEvent(event)) {
trade = parsePerpEvent(mangoAccountAddress, event)
market = selectedMarket
2023-03-04 09:46:21 -08:00
time = trade.timestamp.toNumber() * 1000
} else {
trade = parseApiTradeHistory(mangoAccountAddress, event)
time = trade.block_datetime
2023-01-19 21:25:43 -08:00
if ('market' in trade) {
market = group.getSerum3MarketByExternalMarket(
new PublicKey(trade.market)
)
} else if ('perp_market' in trade) {
2023-01-29 02:22:09 -08:00
market = group.getPerpMarketByMarketIndex(trade.market_index)
2023-01-19 21:25:43 -08:00
}
}
2023-01-19 21:25:43 -08:00
return {
...trade,
market,
time,
}
})
2022-12-19 09:12:03 -08:00
}
const TradeHistory = () => {
2023-01-12 16:16:10 -08:00
const { t } = useTranslation(['common', 'trade'])
2023-01-06 16:26:06 -08:00
const group = mangoStore.getState().group
2022-12-19 09:12:03 -08:00
const { selectedMarket } = useSelectedMarket()
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
2023-01-12 16:16:10 -08:00
const actions = mangoStore((s) => s.actions)
2022-12-19 09:12:03 -08:00
const fills = mangoStore((s) => s.selectedMarket.fills)
const tradeHistoryFromApi = mangoStore(
(s) => s.mangoAccount.tradeHistory.data
)
2023-01-12 16:16:10 -08:00
const loadingTradeHistory = mangoStore(
(s) => s.mangoAccount.tradeHistory.loading
)
const [offset, setOffset] = useState(0)
2022-12-20 20:48:41 -08:00
const { width } = useViewport()
2023-01-19 17:45:08 -08:00
const { connected } = useWallet()
2022-12-20 20:48:41 -08:00
const showTableView = width ? width > breakpoints.md : false
2022-12-19 09:12:03 -08:00
const openOrderOwner = useMemo(() => {
if (!mangoAccount || !selectedMarket) return
2023-01-06 16:26:06 -08:00
if (selectedMarket instanceof PerpMarket) {
return mangoAccount.publicKey
} else {
try {
return mangoAccount.getSerum3OoAccount(selectedMarket.marketIndex)
.address
2023-01-06 16:26:06 -08:00
} catch {
console.warn(
'Unable to find OO account for mkt index',
selectedMarket.marketIndex
)
}
2022-12-19 09:12:03 -08:00
}
}, [mangoAccount, selectedMarket])
const eventQueueFillsForOwner = useMemo(() => {
if (!selectedMarket || !openOrderOwner) return []
2022-12-19 09:12:03 -08:00
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)
)
}
})
2023-01-19 21:25:43 -08:00
}, [selectedMarket, openOrderOwner, fills])
2022-12-19 09:12:03 -08:00
2023-01-06 16:26:06 -08:00
const combinedTradeHistory = useMemo(() => {
2023-01-19 21:25:43 -08:00
const group = mangoStore.getState().group
if (!group || !selectedMarket) return []
let newFills: (SerumEvent | PerpFillEvent)[] = []
if (eventQueueFillsForOwner?.length) {
newFills = eventQueueFillsForOwner.filter((fill) => {
return !tradeHistoryFromApi.find((t) => {
if ('order_id' in t && isSerumFillEvent(fill)) {
return t.order_id === fill.orderId.toString()
} else if ('seq_num' in t && isPerpFillEvent(fill)) {
return t.seq_num === fill.seqNum.toNumber()
2023-01-06 16:26:06 -08:00
}
})
})
}
2023-01-19 21:25:43 -08:00
return formatTradeHistory(group, selectedMarket, mangoAccountAddress, [
...newFills,
...tradeHistoryFromApi,
2023-01-19 21:25:43 -08:00
])
}, [
eventQueueFillsForOwner,
2023-01-19 21:25:43 -08:00
mangoAccountAddress,
tradeHistoryFromApi,
2023-01-19 21:25:43 -08:00
selectedMarket,
])
2023-01-06 16:26:06 -08:00
2023-01-12 16:16:10 -08:00
const handleShowMore = useCallback(() => {
const set = mangoStore.getState().set
set((s) => {
s.mangoAccount.tradeHistory.loading = true
})
setOffset(offset + PAGINATION_PAGE_LENGTH)
actions.fetchTradeHistory(offset + PAGINATION_PAGE_LENGTH)
}, [actions, offset])
2023-01-06 16:26:06 -08:00
if (!selectedMarket || !group) return null
2022-12-19 09:12:03 -08:00
2023-01-19 19:10:15 -08:00
return mangoAccountAddress &&
2023-01-12 16:16:10 -08:00
(combinedTradeHistory.length || loadingTradeHistory) ? (
<>
{showTableView ? (
<div className="thin-scroll overflow-x-auto">
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('market')}</Th>
<Th className="text-right">{t('trade:side')}</Th>
<Th className="text-right">{t('trade:size')}</Th>
<Th className="text-right">{t('price')}</Th>
<Th className="text-right">{t('value')}</Th>
<Th className="text-right">{t('fee')}</Th>
<Th className="text-right">{t('date')}</Th>
<Th />
</TrHead>
</thead>
<tbody>
{combinedTradeHistory.map((trade, index: number) => {
const { side, price, market, size, feeCost, liquidity } = trade
return (
<TrBody
key={`${side}${size}${price}${index}`}
className="my-1 p-2"
>
<Td className="">
<TableMarketName market={market} />
</Td>
<Td className="text-right">
{market instanceof PerpMarket ? (
<PerpSideBadge basePosition={side === 'buy' ? 1 : -1} />
) : (
<SideBadge side={side} />
)}
</Td>
<Td className="text-right font-mono">{size}</Td>
<Td className="text-right font-mono">
<FormatNumericValue value={price} />
</Td>
<Td className="text-right font-mono">
<FormatNumericValue
value={price * size}
decimals={2}
isUsd
/>
</Td>
<Td className="text-right">
<span className="font-mono">
<FormatNumericValue roundUp value={feeCost} />
</span>
<p className="font-body text-xs text-th-fgd-4">
{liquidity}
</p>
</Td>
<Td className="whitespace-nowrap text-right">
{trade?.time ? (
<TableDateDisplay date={trade.time} showSeconds />
) : (
'Recent'
)}
</Td>
<Td className="xl:!pl-0">
{'taker' in trade ? (
<div className="flex justify-end">
2023-02-15 11:31:31 -08:00
<Tooltip
content={`View Counterparty ${abbreviateAddress(
trade.liquidity === 'Taker'
? new PublicKey(trade.maker)
: new PublicKey(trade.taker)
)}`}
delay={0}
>
<a
className=""
target="_blank"
rel="noopener noreferrer"
href={`/?address=${
trade.liquidity === 'Taker'
? trade.maker
: trade.taker
}`}
>
<IconButton size="small">
<UsersIcon className="h-4 w-4" />
</IconButton>
</a>
</Tooltip>
</div>
) : null}
</Td>
</TrBody>
)
})}
</tbody>
</Table>
</div>
2023-01-12 16:16:10 -08:00
) : (
<div>
{combinedTradeHistory.map((trade, index: number) => {
const { side, price, market, size, liquidity } = trade
2023-01-12 16:16:10 -08:00
return (
<div
className="flex items-center justify-between border-b border-th-bkg-3 p-4"
key={`${price}${size}${side}${index}`}
2023-01-12 16:16:10 -08:00
>
<div>
<p className="text-sm text-th-fgd-1">
{dayjs(trade?.time ? trade.time : Date.now()).format(
'ddd D MMM'
)}
</p>
<p className="text-xs text-th-fgd-3">
{trade?.time ? dayjs(trade.time).format('h:mma') : 'Recent'}
</p>
2023-01-12 16:16:10 -08:00
</div>
<div className="flex items-center space-x-3">
2023-01-19 15:44:58 -08:00
<div className="flex flex-col items-end">
<div className="flex items-center">
<MarketLogos market={market} size="small" />
<span className="mr-1 whitespace-nowrap">
{market.name}
</span>
{market instanceof PerpMarket ? (
<PerpSideBadge basePosition={side === 'buy' ? 1 : -1} />
2023-01-19 15:44:58 -08:00
) : (
<SideBadge side={side} />
2023-01-19 15:44:58 -08:00
)}
</div>
<div className="mt-0.5 flex space-x-1 leading-none text-th-fgd-2">
<p className="text-th-fgd-4">
<span className="font-mono text-th-fgd-2">{size}</span>
{' at '}
<span className="font-mono text-th-fgd-2">
<FormatNumericValue value={price} />
</span>
</p>
</div>
2023-01-19 15:44:58 -08:00
</div>
{'taker' in trade ? (
2023-01-19 15:44:58 -08:00
<a
className=""
target="_blank"
rel="noopener noreferrer"
href={`/?address=${
liquidity === 'Taker' ? trade.maker : trade.taker
2023-01-19 15:44:58 -08:00
}`}
>
<IconButton size="small">
2023-01-19 15:44:58 -08:00
<UsersIcon className="h-4 w-4" />
</IconButton>
</a>
) : null}
</div>
</div>
2023-01-12 16:16:10 -08:00
)
})}
</div>
)}
{loadingTradeHistory ? (
<div className="mt-4 space-y-1.5">
{[...Array(4)].map((x, i) => (
<SheenLoader className="mx-4 flex flex-1 md:mx-6" key={i}>
<div className="h-16 w-full bg-th-bkg-2" />
</SheenLoader>
))}
</div>
) : null}
{combinedTradeHistory.length &&
combinedTradeHistory.length % PAGINATION_PAGE_LENGTH === 0 ? (
<div className="flex justify-center py-6">
<LinkButton onClick={handleShowMore}>Show More</LinkButton>
</div>
) : null}
</>
2023-01-19 19:10:15 -08:00
) : mangoAccountAddress || connected ? (
2023-01-15 01:53:34 -08:00
<div className="flex flex-col items-center p-8">
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
2023-01-06 16:26:06 -08:00
<p>No trade history</p>
2022-12-19 09:12:03 -08:00
</div>
2023-01-19 17:45:08 -08:00
) : (
<div className="p-8">
<ConnectEmptyState text={t('trade:connect-trade-history')} />
</div>
2022-12-19 09:12:03 -08:00
)
}
export default TradeHistory