import { getTokenBySymbol } from '@blockworks-foundation/mango-client' import { useEffect, useMemo, useState } from 'react' import dayjs from 'dayjs' import useMangoStore from '../../stores/useMangoStore' import { Table, TableDateDisplay, Td, Th, TrBody, TrHead, } from '../TableElements' import { useTranslation } from 'next-i18next' import isEmpty from 'lodash/isEmpty' import usePagination from '../../hooks/usePagination' import { numberCompactFormatter, roundToDecimal } from '../../utils/' import Pagination from '../Pagination' import { useViewport } from '../../hooks/useViewport' import { breakpoints } from '../TradePageGrid' import { ExpandableRow } from '../TableElements' import MobileTableHeader from '../mobile/MobileTableHeader' import Chart from '../Chart' import Switch from '../Switch' import useLocalStorageState from '../../hooks/useLocalStorageState' const utc = require('dayjs/plugin/utc') dayjs.extend(utc) import { exportDataToCSV } from '../../utils/export' import { SaveIcon } from '@heroicons/react/solid' import Button from '../Button' import TabButtons from 'components/TabButtons' interface InterestStats { [key: string]: { total_borrow_interest: number total_deposit_interest: number } } export const handleDustTicks = (v) => Math.abs(v) < 0.0000099 ? v === 0 ? 0 : v.toExponential() : numberCompactFormatter.format(v) const handleUsdDustTicks = (v) => Math.abs(v) < 0.0000099 ? v === 0 ? '$0' : `$${v.toExponential()}` : `$${numberCompactFormatter.format(v)}` const AccountInterest = () => { const { t } = useTranslation('common') const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current) const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config) const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current) const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache) const [interestStats, setInterestStats] = useState([]) const [hourlyInterestStats, setHourlyInterestStats] = useState({}) const [loadHourlyStats, setLoadHourlyStats] = useState(false) const [loadTotalStats, setLoadTotalStats] = useState(false) const [selectedAsset, setSelectedAsset] = useState('') const [chartData, setChartData] = useState([]) const { paginatedData, setData, totalPages, nextPage, previousPage, page, firstPage, lastPage, } = usePagination(hourlyInterestStats[selectedAsset] || []) const { width } = useViewport() const isMobile = width ? width < breakpoints.md : false const [hideInterestDust, sethideInterestDust] = useLocalStorageState( 'hideInterestDust', false ) const mangoAccountPk = useMemo(() => { if (mangoAccount) { return mangoAccount.publicKey.toString() } }, [mangoAccount]) const token = useMemo(() => { if (selectedAsset) { return getTokenBySymbol(groupConfig, selectedAsset) } }, [selectedAsset]) const exportInterestDataToCSV = () => { const assets = Object.keys(hourlyInterestStats) let dataToExport: any[] = [] for (const asset of assets) { dataToExport = [ ...dataToExport, ...hourlyInterestStats[asset].map((interest) => { const timestamp = new Date(interest.time) return { timestamp: `${timestamp.toLocaleDateString()} ${timestamp.toLocaleTimeString()}`, asset: asset, deposit_interest: interest.deposit_interest, borrow_interest: interest.borrow_interest, } }), ] } const title = `${ mangoAccount?.name || mangoAccount?.publicKey }-Interest-${new Date().toLocaleDateString()}` const headers = [ 'Timestamp', 'Asset', 'Deposit Interest', 'Borrow Interest', ] exportDataToCSV(dataToExport, title, headers, t) } useEffect(() => { if (!isEmpty(hourlyInterestStats)) { setData(hourlyInterestStats[selectedAsset] || []) } }, [selectedAsset, hourlyInterestStats]) useEffect(() => { if (!selectedAsset && Object.keys(hourlyInterestStats).length > 0) { setSelectedAsset(Object.keys(hourlyInterestStats)[0]) } }, [hourlyInterestStats]) useEffect(() => { const hideDust: any[] = [] const fetchInterestStats = async () => { setLoadTotalStats(true) const response = await fetch( `https://mango-transaction-log.herokuapp.com/v3/stats/total-interest-earned?mango-account=${mangoAccountPk}` ) const parsedResponse: InterestStats = await response.json() if (hideInterestDust) { Object.entries(parsedResponse).forEach((r) => { const tokens = groupConfig.tokens const token = tokens.find((t) => t.symbol === r[0]) if (!token || !mangoGroup || !mangoCache) { return } const tokenIndex = mangoGroup.getTokenIndex(token.mintKey) const price = mangoGroup.getPrice(tokenIndex, mangoCache).toNumber() const interest = r[1].total_deposit_interest > 0 ? r[1].total_deposit_interest : r[1].total_borrow_interest if (price * interest > 1) { hideDust.push(r) } }) setLoadTotalStats(false) setInterestStats(hideDust) } else { const stats = Object.entries(parsedResponse) const filterMicroBalances = stats.filter(([symbol, stats]) => { const decimals = getTokenBySymbol(groupConfig, symbol).decimals const smallestValue = Math.pow(10, (decimals + 1) * -1) return ( stats.total_borrow_interest > smallestValue || stats.total_deposit_interest > smallestValue ) }) setLoadTotalStats(false) setInterestStats(filterMicroBalances) } } const fetchHourlyInterestStats = async () => { setLoadHourlyStats(true) const response = await fetch( `https://mango-transaction-log.herokuapp.com/v3/stats/hourly-interest-prices?mango-account=${mangoAccountPk}` ) const parsedResponse = await response.json() let assets if (hideInterestDust) { const assetsToShow = hideDust.map((a) => a[0]) assets = Object.keys(parsedResponse).filter((a) => assetsToShow.includes(a) ) setSelectedAsset(assetsToShow[0]) } else { assets = Object.keys(parsedResponse) } const stats = {} for (const asset of assets) { const x: any = Object.entries(parsedResponse[asset]) const decimals = getTokenBySymbol(groupConfig, asset).decimals stats[asset] = x .map(([key, value, price]) => { const borrows = roundToDecimal(value.borrow_interest, decimals + 1) const deposits = roundToDecimal( value.deposit_interest, decimals + 1 ) if (borrows > 0 || deposits > 0) { return { ...value, time: key, ...price } } else { return null } }) .filter((x) => x) .reverse() if (stats[asset].length === 0) { delete stats[asset] } } setLoadHourlyStats(false) setHourlyInterestStats(stats) } const getStats = async () => { fetchInterestStats() fetchHourlyInterestStats() } getStats() }, [mangoAccountPk, hideInterestDust]) useEffect(() => { if (hourlyInterestStats[selectedAsset]) { const start = new Date( // @ts-ignore dayjs().utc().hour(0).minute(0).subtract(29, 'day') ).getTime() const filtered = hourlyInterestStats[selectedAsset].filter( (d) => new Date(d.time).getTime() > start ) const dailyInterest: any[] = [] for (let i = 0; i < 30; i++) { dailyInterest.push({ interest: 0, value: 0, time: new Date( // @ts-ignore dayjs().utc().hour(0).minute(0).subtract(i, 'day') ).getTime(), }) } filtered.forEach((d) => { const found = dailyInterest.find( (x) => dayjs(x.time).format('DD-MMM') === dayjs(d.time).format('DD-MMM') ) if (found) { const newInterest = d.borrow_interest > 0 ? d.borrow_interest * -1 : d.deposit_interest const newValue = d.borrow_interest > 0 ? d.borrow_interest * -1 * d.price : d.deposit_interest * d.price found.interest = found.interest + newInterest found.value = found.value + newValue } }) setChartData(dailyInterest.reverse()) } }, [hourlyInterestStats, selectedAsset]) const increaseYAxisWidth = !!chartData.find((data) => data.value < 0.001) return ( <>

{t('interest-earned')}

sethideInterestDust(!hideInterestDust)} > {t('hide-dust')}
{mangoAccount ? (
{loadTotalStats ? (
) : !isMobile ? ( {interestStats.length === 0 ? ( ) : ( interestStats.map(([symbol, stats]) => { const decimals = getTokenBySymbol( groupConfig, symbol ).decimals return ( ) }) )}
{t('token')} {t('total-deposit-interest')} {t('total-borrow-interest')} {t('net')}
{t('no-interest')}
{symbol}
{stats.total_deposit_interest.toFixed(decimals)}{' '} {symbol} {stats.total_borrow_interest.toFixed(decimals)}{' '} {symbol} {( stats.total_deposit_interest - stats.total_borrow_interest ).toFixed(decimals)}{' '} {symbol}
) : interestStats.length === 0 ? (
{t('no-interest')}
) : ( <> {interestStats.map(([symbol, stats], index) => { const decimals = getTokenBySymbol(groupConfig, symbol).decimals return (
{symbol}
{( stats.total_deposit_interest - stats.total_borrow_interest ).toFixed(decimals)}{' '} {symbol}
} key={`${symbol}${index}`} panelTemplate={ <>
{t('total-deposit-interest')}
{stats.total_deposit_interest.toFixed(decimals)}{' '} {symbol}
{t('total-borrow-interest')}
{stats.total_borrow_interest.toFixed(decimals)}{' '} {symbol}
} /> ) })} )} <> {!isEmpty(hourlyInterestStats) && !loadHourlyStats ? ( <>

{t('history')}

({ label: token, key: token }) )} onClick={setSelectedAsset} showSymbolIcon />
{selectedAsset && chartData.length > 0 ? (
{chartData.find((d) => d.interest !== 0) ? (
{ return x === 0 ? 0 : token ? x.toFixed(token.decimals + 1) : null }} tickFormat={handleDustTicks} titleValue={chartData.reduce( (a, c) => a + c.interest, 0 )} type="bar" useMulticoloredBars yAxisWidth={increaseYAxisWidth ? 70 : 50} zeroLine />
) : null} {chartData.find((d) => d.value !== 0) ? (
{token ? ( x === 0 ? 0 : x < 0 ? `-$${Math.abs(x)?.toFixed( token.decimals + 1 )}` : `$${x?.toFixed(token.decimals + 1)}` } tickFormat={handleUsdDustTicks} titleValue={chartData.reduce( (a, c) => a + c.value, 0 )} type="bar" useMulticoloredBars yAxisWidth={increaseYAxisWidth ? 70 : 50} zeroLine /> ) : null}
) : null}
) : null}
{paginatedData.length ? ( {paginatedData.map((stat) => { // @ts-ignore const utc = dayjs.utc(stat.time).format() return ( {token ? ( ) : null} {token ? ( ) : null} ) })}
{t('time')} {t('interest')} {t('value')}
{stat.borrow_interest > 0 ? `-${stat.borrow_interest.toFixed( token.decimals + 1 )}` : stat.deposit_interest.toFixed( token.decimals + 1 )}{' '} {selectedAsset} {stat.borrow_interest > 0 ? `-$${( stat.borrow_interest * stat.price ).toFixed(token.decimals + 1)}` : `$${( stat.deposit_interest * stat.price ).toFixed(token.decimals + 1)}`}
) : (
{t('no-interest')}
)}
) : loadHourlyStats ? (
) : null}
) : (
{t('connect-wallet')}
)} ) } export default AccountInterest