import { useState, useEffect, useMemo } from 'react' import { useTranslation } from 'next-i18next' import { ArrowSmDownIcon, ExternalLinkIcon, InformationCircleIcon, SaveIcon, } from '@heroicons/react/solid' import { getMarketByBaseSymbolAndKind, getTokenBySymbol, PerpMarket, } from '@blockworks-foundation/mango-client' import TradeHistoryTable from '../TradeHistoryTable' import useMangoStore from '../../stores/useMangoStore' import { Table, TrHead, Th, TrBody, Td, TableDateDisplay, Row, ExpandableRow, } from '../TableElements' import { LinkButton } from '../Button' import { useSortableData } from '../../hooks/useSortableData' import { formatUsdValue } from '../../utils' import Tooltip from '../Tooltip' import { exportDataToCSV } from '../../utils/export' import { notify } from '../../utils/notifications' import Button from '../Button' import { useViewport } from '../../hooks/useViewport' import { breakpoints } from '.././TradePageGrid' import MobileTableHeader from 'components/mobile/MobileTableHeader' import AccountInterest from './AccountInterest' import AccountFunding from './AccountFunding' import TabButtons from 'components/TabButtons' import AccountVolume from './AccountVolume' const historyViews = [ { label: 'Trades', key: 'Trades' }, { label: 'Deposits', key: 'Deposit' }, { label: 'Withdrawals', key: 'Withdraw' }, { label: 'Interest', key: 'Interest' }, { label: 'Funding', key: 'Funding' }, { label: 'Liquidations', key: 'Liquidation' }, { label: 'Volume', key: 'Volume' }, ] export default function AccountHistory() { const [view, setView] = useState('Trades') const [history, setHistory] = useState(null) const [volumeTotals, setVolumeTotals] = useState([]) const [loading, setLoading] = useState(false) const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current) const mangoAccountPk = useMemo(() => { if (mangoAccount) { return mangoAccount.publicKey.toString() } }, [mangoAccount]) useEffect(() => { const fetchAccountActivity = async () => { setLoading(true) try { const promises = [ fetch( `https://mango-transaction-log.herokuapp.com/v3/stats/activity-feed?mango-account=${mangoAccountPk}` ), fetch( `https://mango-transaction-log.herokuapp.com/v3/stats/mango-account-lifetime-volumes?mango-account=${mangoAccountPk}` ), ] const data = await Promise.all(promises) const historyData = await data[0].json() const volumeTotalsData = await data[1].json() let volumeEntries: any = Object.entries(volumeTotalsData.volumes) volumeEntries = volumeEntries .map( ([key, value]) => ({ [key]: value }) // Object.entries(value), ) .filter((v) => { const assetVolumes: any = Object.values(v)[0] return assetVolumes.maker > 0 || assetVolumes.taker > 0 }) setHistory(historyData) setVolumeTotals(volumeEntries) setLoading(false) } catch { setLoading(false) notify({ title: 'Failed to fetch history data', description: '', type: 'error', }) } } if (mangoAccountPk) { fetchAccountActivity() } }, [mangoAccountPk]) return ( <>
) } const ViewContent = ({ view, history, loading, volumeTotals }) => { switch (view) { case 'Trades': return case 'Deposit': return case 'Withdraw': return case 'Interest': return case 'Funding': return case 'Liquidation': return case 'Volume': return default: return } } const parseActivityDetails = (activity_details, activity_type, perpMarket) => { const groupConfig = useMangoStore.getState().selectedMangoGroup.config let assetGained, assetLost const assetSymbol = activity_type === 'liquidate_perp_market' ? 'USDC' : activity_details.asset_symbol const assetDecimals = activity_type.includes('perp') ? getMarketByBaseSymbolAndKind( groupConfig, assetSymbol.split('-')[0], 'perp' ).baseDecimals : getTokenBySymbol(groupConfig, assetSymbol.split('-')[0]).decimals const liabSymbol = activity_type === 'liquidate_perp_market' || activity_details.liab_type === 'Perp' ? activity_details.liab_symbol.includes('USDC') ? 'USDC' : `${activity_details.liab_symbol}-PERP` : activity_details.liab_symbol const liabDecimals = activity_type.includes('perp') ? getMarketByBaseSymbolAndKind( groupConfig, liabSymbol.split('-')[0], 'perp' ).baseDecimals : getTokenBySymbol(groupConfig, liabSymbol.split('-')[0]).decimals const liabAmount = perpMarket && liabSymbol !== 'USDC' ? perpMarket.baseLotsToNumber(activity_details.liab_amount) : activity_details.liab_amount const assetAmount = activity_details.asset_amount const asset_amount = { amount: parseFloat(assetAmount), decimals: assetDecimals, symbol: assetSymbol, price: parseFloat(activity_details.asset_price), } const liab_amount = { amount: parseFloat(liabAmount), decimals: liabDecimals, symbol: liabSymbol, price: parseFloat(activity_details.liab_price), } switch (activity_type) { case 'liquidate_token_and_token': return [liab_amount, asset_amount] case 'liquidate_token_and_perp': if (activity_details.asset_type === 'Token') { return [liab_amount, asset_amount] } else { return [asset_amount, liab_amount] } case 'liquidate_perp_market': if (parseFloat(activity_details.asset_amount) > 0) { assetGained = asset_amount assetLost = liab_amount } else { assetGained = liab_amount assetLost = asset_amount } return [assetGained, assetLost] default: return [] } } const LiquidationHistoryTable = ({ history, view }) => { const { t } = useTranslation('common') const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current) const markets = useMangoStore((s) => s.selectedMangoGroup.markets) const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config) const filteredHistory = useMemo(() => { return history?.length ? history.filter((h) => h.activity_type.includes('liquidate')) : [] }, [history, view]) const { items, requestSort, sortConfig } = useSortableData(filteredHistory) const { width } = useViewport() const isMobile = width ? width < breakpoints.md : false const exportHistoryToCSV = () => { const dataToExport = history .filter((val) => val.activity_type == view) .map((row) => { row = row.activity_details const timestamp = new Date(row.block_datetime) return { date: `${timestamp.toLocaleDateString()} ${timestamp.toLocaleTimeString()}`, asset: row.symbol, quantity: row.quantity, value: row.usd_equivalent, } }) const headers = ['Timestamp', 'Asset', 'Quantity', 'Value'] if (dataToExport.length == 0) { notify({ title: t('export-data-empty'), description: '', type: 'info', }) return } const tab = historyViews.filter((v) => v.key == view)[0].label const title = `${ mangoAccount?.name || mangoAccount?.publicKey }-${tab}-${new Date().toLocaleDateString()}` exportDataToCSV(dataToExport, title, headers, t) } return ( <>

{filteredHistory.length === 1 ? t('number-liquidation', { number: filteredHistory.length }) : t('number-liquidations', { number: filteredHistory.length })}

{t('delay-displaying-recent')} {t('use-explorer-one')} {t('use-explorer-two')} {t('use-explorer-three')}
} >
{items.length ? ( !isMobile ? ( {items.map(({ activity_details, activity_type }) => { let perpMarket: PerpMarket | null = null if (activity_type.includes('perp')) { const symbol = activity_details.perp_market.split('-')[0] const marketConfig = getMarketByBaseSymbolAndKind( groupConfig, symbol, 'perp' ) if (marketConfig && marketConfig.publicKey) { perpMarket = markets[ marketConfig.publicKey.toString() ] as PerpMarket } else { perpMarket = markets[ marketConfig.publicKey.toBase58() ] as PerpMarket } } const [assetGained, assetLost] = parseActivityDetails( activity_details, activity_type, perpMarket ) const valueLost = Math.abs(assetLost.amount * assetLost.price) const valueGained = assetGained.amount * assetGained.price const liquidationFee = valueGained - valueLost return ( ) })}
requestSort('block_datetime')} > {t('date')} requestSort('asset_amount')} > {t('asset-liquidated')} requestSort('liab_amount')} > {t('asset-returned')} {t('liquidation-fee')}
{Math.abs(assetLost.amount).toLocaleString(undefined, { maximumFractionDigits: assetLost.decimals, })}{' '} {`${assetLost.symbol} at ${formatUsdValue( assetLost.price )}`}

{formatUsdValue(valueLost)}

{Math.abs(assetGained.amount).toLocaleString( undefined, { maximumFractionDigits: assetGained.decimals, } )}{' '} {`${assetGained.symbol} at ${formatUsdValue( assetGained.price )}`}

{formatUsdValue(valueGained)}

= 0 ? 'text-th-green' : 'text-th-red' } > {formatUsdValue(liquidationFee)} {t('view-transaction')}
) : (
{items.map(({ activity_details, activity_type }) => { let perpMarket: PerpMarket | null = null if (activity_type.includes('perp')) { const symbol = activity_details.perp_market.split('-')[0] const marketConfig = getMarketByBaseSymbolAndKind( groupConfig, symbol, 'perp' ) perpMarket = markets[ marketConfig.publicKey.toString() ] as PerpMarket } const [assetGained, assetLost] = parseActivityDetails( activity_details, activity_type, perpMarket ) const valueLost = Math.abs(assetLost.amount * assetLost.price) const valueGained = assetGained.amount * assetGained.price const liquidationFee = valueGained - valueLost return (
= 0 ? 'text-th-green' : 'text-th-red' } > {formatUsdValue(liquidationFee)}
} key={`${activity_details.signature}`} panelTemplate={
{t('asset-liquidated')}
{Math.abs(assetLost.amount).toLocaleString( undefined, { maximumFractionDigits: assetLost.decimals, } )}{' '} {`${assetLost.symbol} at ${formatUsdValue( assetLost.price )}`}

{formatUsdValue(valueLost)}

{t('asset-returned')}
{Math.abs(assetGained.amount).toLocaleString( undefined, { maximumFractionDigits: assetGained.decimals, } )}{' '} {`${assetGained.symbol} at ${formatUsdValue( assetGained.price )}`}

{formatUsdValue(valueGained)}

} /> ) })} ) ) : (
{t('history-empty')}
)} ) } const HistoryTable = ({ history, view }) => { const { t } = useTranslation('common') const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current) const { width } = useViewport() const isMobile = width ? width < breakpoints.md : false const filteredHistory = useMemo(() => { return history?.length ? history .filter((h) => h.activity_type === view) .map((h) => h.activity_details) : [] }, [history, view]) const { items, requestSort, sortConfig } = useSortableData(filteredHistory) const exportHistoryToCSV = () => { const dataToExport = history .filter((val) => val.activity_type == view) .map((row) => { row = row.activity_details const timestamp = new Date(row.block_datetime) return { date: `${timestamp.toLocaleDateString()} ${timestamp.toLocaleTimeString()}`, asset: row.symbol, quantity: row.quantity, value: row.usd_equivalent, } }) const headers = ['Timestamp', 'Asset', 'Quantity', 'Value'] if (dataToExport.length == 0) { notify({ title: t('export-data-empty'), description: '', type: 'info', }) return } const tab = historyViews.filter((v) => v.key == view)[0].label const title = `${ mangoAccount?.name || mangoAccount?.publicKey }-${tab}-${new Date().toLocaleDateString()}` exportDataToCSV(dataToExport, title, headers, t) } return ( <>

{filteredHistory.length === 1 ? view === 'Withdraw' ? t('number-withdrawal', { number: filteredHistory.length }) : t('number-deposit', { number: filteredHistory.length }) : view === 'Withdraw' ? t('number-withdrawals', { number: filteredHistory.length }) : t('number-deposits', { number: filteredHistory.length })}

{t('delay-displaying-recent')} {t('use-explorer-one')} {t('use-explorer-two')} {t('use-explorer-three')}
} >
{items.length ? ( !isMobile ? ( {items.map((activity_details: any) => { const { signature, block_datetime, symbol, quantity, usd_equivalent, } = activity_details return ( ) })}
requestSort('block_datetime')} > {t('date')} requestSort('symbol')} > {t('asset')} requestSort('quantity')} > {t('quantity')} requestSort('usd_equivalent')} > {t('value')}
{symbol}
{quantity.toLocaleString()} {formatUsdValue(usd_equivalent)} {t('view-transaction')}
) : (
{items.map((activity_details: any) => { const { signature, block_datetime, symbol, quantity, usd_equivalent, } = activity_details return (

{`${quantity.toLocaleString()} ${symbol}`}

{formatUsdValue(usd_equivalent)}

) })}
) ) : (
{t('history-empty')}
)} ) }