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 ? (
requestSort('block_datetime')}
>
{t('date')}
|
requestSort('asset_amount')}
>
{t('asset-liquidated')}
|
requestSort('liab_amount')}
>
{t('asset-returned')}
|
{t('liquidation-fee')}
|
|
{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 (
|
{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 ? (
requestSort('block_datetime')}
>
{t('date')}
|
requestSort('symbol')}
>
{t('asset')}
|
requestSort('quantity')}
>
{t('quantity')}
|
requestSort('usd_equivalent')}
>
{t('value')}
|
|
{items.map((activity_details: any) => {
const {
signature,
block_datetime,
symbol,
quantity,
usd_equivalent,
} = activity_details
return (
|
}.svg`})
{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')}
)}
>
)
}