import { Group, I80F48, PerpMarket, Serum3Market, } from '@blockworks-foundation/mango-v4' import { IconButton, LinkButton } from '@components/shared/Button' import ConnectEmptyState from '@components/shared/ConnectEmptyState' import FormatNumericValue from '@components/shared/FormatNumericValue' import SheenLoader from '@components/shared/SheenLoader' import SideBadge from '@components/shared/SideBadge' import { Table, TableDateDisplay, Td, Th, TrBody, TrHead, } from '@components/shared/TableElements' import Tooltip from '@components/shared/Tooltip' import { NoSymbolIcon, UsersIcon } from '@heroicons/react/20/solid' import { useWallet } from '@solana/wallet-adapter-react' import { PublicKey } from '@solana/web3.js' import mangoStore from '@store/mangoStore' import useMangoAccount from 'hooks/useMangoAccount' import useSelectedMarket from 'hooks/useSelectedMarket' import { useViewport } from 'hooks/useViewport' import { useTranslation } from 'next-i18next' import { useCallback, useMemo, useState } from 'react' import { PAGINATION_PAGE_LENGTH } from 'utils/constants' import { breakpoints } from 'utils/theme' import TableMarketName from './TableMarketName' const byTimestamp = (a: any, b: any) => { return ( new Date(b.loadTimestamp || b.timestamp * 1000).getTime() - new Date(a.loadTimestamp || a.timestamp * 1000).getTime() ) } const reverseSide = (side: string) => (side === 'long' ? 'short' : 'long') const parsePerpEvent = (mangoAccountAddress: string, event: any) => { const maker = event.maker.toString() === mangoAccountAddress const orderId = maker ? event.makerOrderId : event.takerOrderId const value = event.quantity * event.price const feeRate = maker ? new I80F48(event.makerFee.val) : new I80F48(event.takerFee.val) const takerSide = event.takerSide === 0 ? 'long' : 'short' const side = maker ? reverseSide(takerSide) : takerSide return { ...event, key: orderId?.toString(), liquidity: maker ? 'Maker' : 'Taker', size: event.size, price: event.price, value, feeCost: (feeRate.toNumber() * value).toFixed(4), side, } } const parseSerumEvent = (event: any) => { let liquidity if (event.eventFlags) { liquidity = event?.eventFlags?.maker ? 'Maker' : 'Taker' } else { liquidity = event.maker ? 'Maker' : 'Taker' } return { ...event, liquidity, key: `${event.maker}-${event.price}`, value: event.price * event.size, side: event.side, } } const parseApiTradeHistory = (mangoAccountAddress: string, trade: any) => { let makerTaker if ('maker' in trade) { makerTaker = trade.maker ? 'Maker' : 'Taker' if (trade.taker && trade.taker.toString() === mangoAccountAddress) { makerTaker = 'Taker' } } let side = trade.side if ('perp_market' in trade) { const sideObj: any = {} if (makerTaker == 'Taker') { sideObj[trade.taker_side] = 1 } else { sideObj[trade.taker_side == 'bid' ? 'ask' : 'bid'] = 1 } side = sideObj } const size = trade.size || trade.quantity let fee if (trade.fee_cost) { fee = trade.fee_cost } else { fee = trade.maker === mangoAccountAddress ? trade.maker_fee : trade.taker_fee } return { ...trade, liquidity: trade.liquidity || makerTaker, side, size, feeCost: fee, } } const formatTradeHistory = ( group: Group, selectedMarket: Serum3Market | PerpMarket | undefined, mangoAccountAddress: string, tradeHistory: any[] ) => { return tradeHistory .flat() .map((event) => { let trade if (event.eventFlags || event.nativeQuantityPaid) { trade = parseSerumEvent(event) } else if (event.makerFee) { trade = parsePerpEvent(mangoAccountAddress, event) } else { trade = parseApiTradeHistory(mangoAccountAddress, event) } let market if ('market' in trade) { market = group.getSerum3MarketByExternalMarket( new PublicKey(trade.market) ) } else if ('perp_market' in trade) { market = group.getPerpMarketByMarketIndex(trade.market_index) } else { market = selectedMarket } return { ...trade, market } }) .sort(byTimestamp) } const TradeHistory = () => { const { t } = useTranslation(['common', 'trade']) const group = mangoStore.getState().group const { selectedMarket } = useSelectedMarket() const { mangoAccount, mangoAccountAddress } = useMangoAccount() const actions = mangoStore((s) => s.actions) const fills = mangoStore((s) => s.selectedMarket.fills) const tradeHistory = mangoStore((s) => s.mangoAccount.tradeHistory.data) const loadingTradeHistory = mangoStore( (s) => s.mangoAccount.tradeHistory.loading ) const [offset, setOffset] = useState(0) const { width } = useViewport() const { connected } = useWallet() const showTableView = width ? width > breakpoints.md : false 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 eventQueueFillsForAccount = useMemo(() => { if (!selectedMarket) return [] return fills.filter((fill: any) => { if (fill.openOrders) { // handles serum event queue for spot trades return openOrderOwner ? fill.openOrders.equals(openOrderOwner) : false } else { // handles mango event queue for perp trades return ( fill.taker.equals(openOrderOwner) || fill.maker.equals(openOrderOwner) ) } }) }, [selectedMarket, openOrderOwner, fills]) const combinedTradeHistory = useMemo(() => { const group = mangoStore.getState().group if (!group) return [] let newFills = [] if (eventQueueFillsForAccount?.length) { newFills = eventQueueFillsForAccount.filter((fill) => { return !tradeHistory.find((t) => { if ('order_id' in t) { return t.order_id === fill.orderId?.toString() } else { return t.seq_num === fill.seqNum?.toNumber() } }) }) } return formatTradeHistory(group, selectedMarket, mangoAccountAddress, [ ...newFills, ...tradeHistory, ]) }, [ eventQueueFillsForAccount, mangoAccountAddress, tradeHistory, selectedMarket, ]) 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]) if (!selectedMarket || !group) return null return mangoAccountAddress && (combinedTradeHistory.length || loadingTradeHistory) ? ( <> {showTableView ? ( {combinedTradeHistory.map((trade: any, index: number) => { return ( ) })}
{t('market')} {t('trade:side')} {t('trade:size')} {t('price')} {t('value')} {t('fee')} {t('date')}
{trade.size}

{trade.liquidity}

{trade.block_datetime ? ( ) : ( 'Recent' )} {trade.market.name.includes('PERP') ? (
) : null}
) : (
{combinedTradeHistory.map((trade: any, index: number) => { return (

{trade.size} {' for '}

{trade.block_datetime ? ( ) : ( 'Recent' )}

{trade.market.name.includes('PERP') ? ( ) : null}
) })}
)} {loadingTradeHistory ? (
{[...Array(4)].map((x, i) => (
))}
) : null} {combinedTradeHistory.length && combinedTradeHistory.length % PAGINATION_PAGE_LENGTH === 0 ? (
Show More
) : null} ) : mangoAccountAddress || connected ? (

No trade history

) : (
) } export default TradeHistory