Merge pull request #76 from blockworks-foundation/30-day-interest-chart
30 day interest and funding charts
This commit is contained in:
commit
485e26c6fe
|
@ -7,9 +7,11 @@ import { numberCompactFormatter } from '../utils'
|
|||
|
||||
interface ChartProps {
|
||||
data: any
|
||||
hideRangeFilters?: boolean
|
||||
title?: string
|
||||
xAxis: string
|
||||
yAxis: string
|
||||
yAxisWidth?: number
|
||||
type: string
|
||||
labelFormat: (x) => ReactNode
|
||||
tickFormat?: (x) => any
|
||||
|
@ -23,6 +25,8 @@ const Chart: FunctionComponent<ChartProps> = ({
|
|||
labelFormat,
|
||||
tickFormat,
|
||||
type,
|
||||
hideRangeFilters,
|
||||
yAxisWidth,
|
||||
}) => {
|
||||
const [mouseData, setMouseData] = useState<string | null>(null)
|
||||
const [daysToShow, setDaysToShow] = useState(30)
|
||||
|
@ -87,32 +91,34 @@ const Chart: FunctionComponent<ChartProps> = ({
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex h-5">
|
||||
<button
|
||||
className={`default-transition font-bold mx-3 text-th-fgd-1 text-xs hover:text-th-primary focus:outline-none ${
|
||||
daysToShow === 1 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(1)}
|
||||
>
|
||||
24H
|
||||
</button>
|
||||
<button
|
||||
className={`default-transition font-bold mx-3 text-th-fgd-1 text-xs hover:text-th-primary focus:outline-none ${
|
||||
daysToShow === 7 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(7)}
|
||||
>
|
||||
7D
|
||||
</button>
|
||||
<button
|
||||
className={`default-transition font-bold ml-3 text-th-fgd-1 text-xs hover:text-th-primary focus:outline-none ${
|
||||
daysToShow === 30 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(30)}
|
||||
>
|
||||
30D
|
||||
</button>
|
||||
</div>
|
||||
{!hideRangeFilters ? (
|
||||
<div className="flex h-5">
|
||||
<button
|
||||
className={`default-transition font-bold mx-3 text-th-fgd-1 text-xs hover:text-th-primary focus:outline-none ${
|
||||
daysToShow === 1 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(1)}
|
||||
>
|
||||
24H
|
||||
</button>
|
||||
<button
|
||||
className={`default-transition font-bold mx-3 text-th-fgd-1 text-xs hover:text-th-primary focus:outline-none ${
|
||||
daysToShow === 7 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(7)}
|
||||
>
|
||||
7D
|
||||
</button>
|
||||
<button
|
||||
className={`default-transition font-bold ml-3 text-th-fgd-1 text-xs hover:text-th-primary focus:outline-none ${
|
||||
daysToShow === 30 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(30)}
|
||||
>
|
||||
30D
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{width > 0 && type === 'area' ? (
|
||||
<AreaChart
|
||||
|
@ -172,7 +178,7 @@ const Chart: FunctionComponent<ChartProps> = ({
|
|||
: (v) => numberCompactFormatter.format(v)
|
||||
}
|
||||
type="number"
|
||||
width={50}
|
||||
width={yAxisWidth || 50}
|
||||
/>
|
||||
</AreaChart>
|
||||
) : null}
|
||||
|
@ -219,6 +225,7 @@ const Chart: FunctionComponent<ChartProps> = ({
|
|||
/>
|
||||
<YAxis
|
||||
dataKey={yAxis}
|
||||
interval="preserveStartEnd"
|
||||
axisLine={false}
|
||||
hide={data.length > 0 ? false : true}
|
||||
dx={-10}
|
||||
|
@ -234,7 +241,7 @@ const Chart: FunctionComponent<ChartProps> = ({
|
|||
: (v) => numberCompactFormatter.format(v)
|
||||
}
|
||||
type="number"
|
||||
width={50}
|
||||
width={yAxisWidth || 50}
|
||||
/>
|
||||
</BarChart>
|
||||
) : null}
|
||||
|
|
|
@ -19,22 +19,22 @@ export default function Pagination({
|
|||
<button
|
||||
onClick={firstPage}
|
||||
disabled={page === 1}
|
||||
className={`border border-th-bkg-4 px-1 py-1 ${
|
||||
className={`bg-th-bkg-4 px-1 py-1 ${
|
||||
page !== 1
|
||||
? 'hover:text-th-primary hover:cursor-pointer'
|
||||
: 'hover:cursor-not-allowed'
|
||||
}`}
|
||||
} disabled:text-th-fgd-4`}
|
||||
>
|
||||
<ChevronDoubleLeftIcon className={`w-5 h-5`} />
|
||||
</button>
|
||||
<button
|
||||
onClick={previousPage}
|
||||
disabled={page === 1}
|
||||
className={`border border-th-bkg-4 px-1 py-1 ml-2 ${
|
||||
className={`bg-th-bkg-4 px-1 py-1 ml-2 ${
|
||||
page !== 1
|
||||
? 'hover:text-th-primary hover:cursor-pointer'
|
||||
: 'hover:cursor-not-allowed'
|
||||
}`}
|
||||
} disabled:text-th-fgd-4`}
|
||||
>
|
||||
<ChevronLeftIcon className={`w-5 h-5`} />
|
||||
</button>
|
||||
|
@ -46,22 +46,22 @@ export default function Pagination({
|
|||
<button
|
||||
onClick={nextPage}
|
||||
disabled={page === totalPages}
|
||||
className={`px-1 py-1 border border-th-bkg-4 ml-2 ${
|
||||
className={`px-1 py-1 bg-th-bkg-4 ml-2 ${
|
||||
page !== totalPages
|
||||
? 'hover:text-th-primary hover:cursor-pointer'
|
||||
: 'hover:cursor-not-allowed'
|
||||
}`}
|
||||
} disabled:text-th-fgd-4`}
|
||||
>
|
||||
<ChevronRightIcon className={`w-5 h-5`} />
|
||||
</button>
|
||||
<button
|
||||
onClick={lastPage}
|
||||
disabled={page === totalPages}
|
||||
className={`px-1 py-1 border border-th-bkg-4 ml-2 ${
|
||||
className={`px-1 py-1 bg-th-bkg-4 ml-2 ${
|
||||
page !== totalPages
|
||||
? 'hover:text-th-primary hover:cursor-pointer'
|
||||
: 'hover:cursor-not-allowed'
|
||||
}`}
|
||||
} disabled:text-th-fgd-4`}
|
||||
>
|
||||
<ChevronDoubleRightIcon className={`w-5 h-5`} />
|
||||
</button>
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
import { useEffect, useMemo, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import useMangoStore from '../../stores/useMangoStore'
|
||||
import { Table, Td, Th, TrBody, TrHead } from '../TableElements'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import Select from '../Select'
|
||||
import Loading from '../Loading'
|
||||
import Pagination from '../Pagination'
|
||||
import usePagination from '../../hooks/usePagination'
|
||||
import { roundToDecimal } from '../../utils'
|
||||
import Chart from '../Chart'
|
||||
import Switch from '../Switch'
|
||||
import useLocalStorageState from '../../hooks/useLocalStorageState'
|
||||
import { handleDustTicks } from './AccountInterest'
|
||||
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
dayjs.extend(utc)
|
||||
import { exportDataToCSV } from '../../utils/export'
|
||||
import Button from '../Button'
|
||||
import { SaveIcon } from '@heroicons/react/outline'
|
||||
|
@ -31,6 +38,11 @@ const AccountFunding = () => {
|
|||
firstPage,
|
||||
lastPage,
|
||||
} = usePagination(hourlyFunding[selectedAsset])
|
||||
const [hideFundingDust, setHideFundingDust] = useLocalStorageState(
|
||||
'hideFundingDust',
|
||||
false
|
||||
)
|
||||
const [chartData, setChartData] = useState([])
|
||||
|
||||
const mangoAccountPk = useMemo(() => {
|
||||
return mangoAccount.publicKey.toString()
|
||||
|
@ -69,13 +81,24 @@ const AccountFunding = () => {
|
|||
}, [selectedAsset, hourlyFunding])
|
||||
|
||||
useEffect(() => {
|
||||
const hideDust = []
|
||||
const fetchFundingStats = async () => {
|
||||
const response = await fetch(
|
||||
`https://mango-transaction-log.herokuapp.com/v3/stats/total-funding?mango-account=${mangoAccountPk}`
|
||||
)
|
||||
const parsedResponse = await response.json()
|
||||
|
||||
setFundingStats(Object.entries(parsedResponse))
|
||||
if (hideFundingDust) {
|
||||
Object.entries(parsedResponse).forEach((r: any) => {
|
||||
const funding = r[1].total_funding
|
||||
if (Math.abs(funding) > 1) {
|
||||
hideDust.push(r)
|
||||
}
|
||||
})
|
||||
setFundingStats(hideDust)
|
||||
} else {
|
||||
setFundingStats(Object.entries(parsedResponse))
|
||||
}
|
||||
}
|
||||
|
||||
const fetchHourlyFundingStats = async () => {
|
||||
|
@ -84,7 +107,17 @@ const AccountFunding = () => {
|
|||
`https://mango-transaction-log.herokuapp.com/v3/stats/hourly-funding?mango-account=${mangoAccountPk}`
|
||||
)
|
||||
const parsedResponse = await response.json()
|
||||
const assets = Object.keys(parsedResponse)
|
||||
|
||||
let assets
|
||||
if (hideFundingDust) {
|
||||
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) {
|
||||
|
@ -110,23 +143,75 @@ const AccountFunding = () => {
|
|||
setHourlyFunding(stats)
|
||||
}
|
||||
|
||||
fetchFundingStats()
|
||||
fetchHourlyFundingStats()
|
||||
}, [mangoAccountPk])
|
||||
const getStats = async () => {
|
||||
await fetchFundingStats()
|
||||
fetchHourlyFundingStats()
|
||||
}
|
||||
getStats()
|
||||
}, [mangoAccountPk, hideFundingDust])
|
||||
|
||||
useEffect(() => {
|
||||
if (hourlyFunding[selectedAsset]) {
|
||||
const start = new Date(
|
||||
// @ts-ignore
|
||||
dayjs().utc().hour(0).minute(0).subtract(29, 'day')
|
||||
).getTime()
|
||||
|
||||
const filtered = hourlyFunding[selectedAsset].filter(
|
||||
(d) => new Date(d.time).getTime() > start
|
||||
)
|
||||
|
||||
const dailyFunding = []
|
||||
|
||||
filtered.forEach((d) => {
|
||||
const found = dailyFunding.find(
|
||||
(x) =>
|
||||
dayjs(x.time).format('DD-MMM') === dayjs(d.time).format('DD-MMM')
|
||||
)
|
||||
if (found) {
|
||||
const newFunding = d.total_funding
|
||||
found.funding = found.funding + newFunding
|
||||
} else {
|
||||
dailyFunding.push({
|
||||
time: new Date(d.time).getTime(),
|
||||
funding: d.total_funding,
|
||||
})
|
||||
}
|
||||
})
|
||||
setChartData(dailyFunding.reverse())
|
||||
}
|
||||
}, [hourlyFunding, selectedAsset])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedAsset && Object.keys(hourlyFunding).length > 0) {
|
||||
setSelectedAsset(Object.keys(hourlyFunding)[0])
|
||||
}
|
||||
}, [hourlyFunding])
|
||||
|
||||
const increaseYAxisWidth = !!chartData.find((data) => data.value < 0.001)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pb-4 text-th-fgd-1 text-lg">
|
||||
{t('total-funding-stats')}
|
||||
<Button
|
||||
className={`float-right text-xs h-8 pt-0 pb-0 pl-3 pr-3`}
|
||||
onClick={exportFundingDataToCSV}
|
||||
>
|
||||
<div className={`flex items-center`}>
|
||||
<SaveIcon className={`h-4 w-4 mr-1.5`} />
|
||||
{t('export-data')}
|
||||
</div>
|
||||
</Button>
|
||||
<div className="flex items-center justify-between pb-4">
|
||||
<div className="text-th-fgd-1 text-lg">{t('total-funding')}</div>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className={`float-right text-xs h-8 pt-0 pb-0 pl-3 pr-3`}
|
||||
onClick={exportFundingDataToCSV}
|
||||
>
|
||||
<div className={`flex items-center whitespace-nowrap`}>
|
||||
<SaveIcon className={`h-4 w-4 mr-1.5`} />
|
||||
{t('export-data')}
|
||||
</div>
|
||||
</Button>
|
||||
<Switch
|
||||
checked={hideFundingDust}
|
||||
className="ml-2 text-xs"
|
||||
onChange={() => setHideFundingDust(!hideFundingDust)}
|
||||
>
|
||||
{t('hide-dust')}
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
{mangoAccount ? (
|
||||
<div>
|
||||
|
@ -134,7 +219,7 @@ const AccountFunding = () => {
|
|||
<thead>
|
||||
<TrHead>
|
||||
<Th>{t('token')}</Th>
|
||||
<Th>{t('total-funding')}</Th>
|
||||
<Th>{t('total-funding')} (USDC)</Th>
|
||||
</TrHead>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -142,7 +227,9 @@ const AccountFunding = () => {
|
|||
<TrBody index={0}>
|
||||
<td colSpan={4}>
|
||||
<div className="flex">
|
||||
<div className="mx-auto py-4">{t('no-funding')}</div>
|
||||
<div className="mx-auto py-4 text-th-fgd-3">
|
||||
{t('no-funding')}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</TrBody>
|
||||
|
@ -173,7 +260,7 @@ const AccountFunding = () => {
|
|||
}`}
|
||||
>
|
||||
{stats.total_funding
|
||||
? `$${stats.total_funding?.toFixed(6)}`
|
||||
? `${stats.total_funding?.toFixed(6)}`
|
||||
: '-'}
|
||||
</div>
|
||||
</Td>
|
||||
|
@ -226,6 +313,33 @@ const AccountFunding = () => {
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
{selectedAsset && chartData.length > 0 ? (
|
||||
<div className="flex flex-col sm:flex-row space-x-0 sm:space-x-4 w-full">
|
||||
<div
|
||||
className="border border-th-bkg-4 relative mb-6 p-4 rounded-md w-full"
|
||||
style={{ height: '330px' }}
|
||||
>
|
||||
<Chart
|
||||
hideRangeFilters
|
||||
title={t('funding-chart-title', {
|
||||
symbol: selectedAsset,
|
||||
})}
|
||||
xAxis="time"
|
||||
yAxis="funding"
|
||||
data={chartData}
|
||||
labelFormat={(x) =>
|
||||
x &&
|
||||
`${x.toLocaleString(undefined, {
|
||||
maximumFractionDigits: 6,
|
||||
})} USDC`
|
||||
}
|
||||
tickFormat={handleDustTicks}
|
||||
type="bar"
|
||||
yAxisWidth={increaseYAxisWidth ? 70 : 50}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<div>
|
||||
{paginatedData.length ? (
|
||||
|
@ -233,24 +347,21 @@ const AccountFunding = () => {
|
|||
<thead>
|
||||
<TrHead>
|
||||
<Th>{t('time')}</Th>
|
||||
<Th>{t('funding')}</Th>
|
||||
<Th>{t('funding')} (USDC)</Th>
|
||||
</TrHead>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginatedData.map((stat, index) => {
|
||||
const date = new Date(stat.time)
|
||||
// @ts-ignore
|
||||
const utc = dayjs.utc(stat.time).format()
|
||||
|
||||
return (
|
||||
<TrBody index={index} key={stat.time}>
|
||||
<Td>
|
||||
{date.toLocaleDateString()}{' '}
|
||||
{date.toLocaleTimeString()}
|
||||
</Td>
|
||||
<Td>{dayjs(utc).format('DD/MM/YY, h:mma')}</Td>
|
||||
<Td>
|
||||
{stat.total_funding.toFixed(
|
||||
QUOTE_DECIMALS + 1
|
||||
)}{' '}
|
||||
USDC
|
||||
)}
|
||||
</Td>
|
||||
</TrBody>
|
||||
)
|
||||
|
@ -258,7 +369,7 @@ const AccountFunding = () => {
|
|||
</tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="flex justify-center w-full bg-th-bkg-3 py-4">
|
||||
<div className="flex justify-center w-full bg-th-bkg-3 py-4 text-th-fgd-3">
|
||||
{t('no-funding')}
|
||||
</div>
|
||||
)}
|
||||
|
@ -274,10 +385,10 @@ const AccountFunding = () => {
|
|||
</div>
|
||||
</>
|
||||
) : loading ? (
|
||||
<div className="flex justify-center my-8">
|
||||
<div>
|
||||
<Loading />
|
||||
</div>
|
||||
<div className="pt-8 space-y-2">
|
||||
<div className="animate-pulse bg-th-bkg-3 h-12 rounded-md w-full" />
|
||||
<div className="animate-pulse bg-th-bkg-3 h-12 rounded-md w-full" />
|
||||
<div className="animate-pulse bg-th-bkg-3 h-12 rounded-md w-full" />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
|
|
@ -1,17 +1,29 @@
|
|||
import { getTokenBySymbol } from '@blockworks-foundation/mango-client'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
// import { CurrencyDollarIcon } from '@heroicons/react/outline'
|
||||
import useMangoStore from '../../stores/useMangoStore'
|
||||
import Select from '../Select'
|
||||
import { Table, Td, Th, TrBody, TrHead } from '../TableElements'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { isEmpty } from 'lodash'
|
||||
import usePagination from '../../hooks/usePagination'
|
||||
import { roundToDecimal } from '../../utils/'
|
||||
import {
|
||||
// formatUsdValue,
|
||||
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/outline'
|
||||
import Button from '../Button'
|
||||
|
@ -23,14 +35,32 @@ interface InterestStats {
|
|||
}
|
||||
}
|
||||
|
||||
export const handleDustTicks = (v) =>
|
||||
v < 0.000001
|
||||
? v === 0
|
||||
? 0
|
||||
: v.toExponential()
|
||||
: numberCompactFormatter.format(v)
|
||||
|
||||
const handleUsdDustTicks = (v) =>
|
||||
v < 0.000001
|
||||
? 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<any>([])
|
||||
const [hourlyInterestStats, setHourlyInterestStats] = useState<any>({})
|
||||
// const [totalInterestValue, setTotalInterestValue] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedAsset, setSelectedAsset] = useState<string>('')
|
||||
const [chartData, setChartData] = useState([])
|
||||
const {
|
||||
paginatedData,
|
||||
setData,
|
||||
|
@ -43,6 +73,10 @@ const AccountInterest = () => {
|
|||
} = usePagination(hourlyInterestStats[selectedAsset])
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.md : false
|
||||
const [hideInterestDust, sethideInterestDust] = useLocalStorageState(
|
||||
'hideInterestDust',
|
||||
false
|
||||
)
|
||||
|
||||
const mangoAccountPk = useMemo(() => {
|
||||
return mangoAccount.publicKey.toString()
|
||||
|
@ -99,22 +133,58 @@ const AccountInterest = () => {
|
|||
}, [hourlyInterestStats])
|
||||
|
||||
useEffect(() => {
|
||||
const hideDust = []
|
||||
const fetchInterestStats = async () => {
|
||||
const response = await fetch(
|
||||
`https://mango-transaction-log.herokuapp.com/v3/stats/total-interest-earned?mango-account=${mangoAccountPk}`
|
||||
)
|
||||
const parsedResponse: InterestStats = await response.json()
|
||||
|
||||
setInterestStats(Object.entries(parsedResponse))
|
||||
if (hideInterestDust) {
|
||||
Object.entries(parsedResponse).forEach((r) => {
|
||||
const tokens = groupConfig.tokens
|
||||
const token = tokens.find((t) => t.symbol === r[0])
|
||||
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)
|
||||
}
|
||||
})
|
||||
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
|
||||
)
|
||||
})
|
||||
setInterestStats(filterMicroBalances)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchHourlyInterestStats = async () => {
|
||||
setLoading(true)
|
||||
const response = await fetch(
|
||||
`https://mango-transaction-log.herokuapp.com/v3/stats/hourly-interest?mango-account=${mangoAccountPk}`
|
||||
`https://mango-transaction-log.herokuapp.com/v3/stats/hourly-interest-prices?mango-account=${mangoAccountPk}`
|
||||
)
|
||||
const parsedResponse = await response.json()
|
||||
const assets = Object.keys(parsedResponse)
|
||||
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) {
|
||||
|
@ -122,7 +192,7 @@ const AccountInterest = () => {
|
|||
const token = getTokenBySymbol(groupConfig, asset)
|
||||
|
||||
stats[asset] = x
|
||||
.map(([key, value]) => {
|
||||
.map(([key, value, price]) => {
|
||||
const borrows = roundToDecimal(
|
||||
value.borrow_interest,
|
||||
token.decimals + 1
|
||||
|
@ -132,36 +202,115 @@ const AccountInterest = () => {
|
|||
token.decimals + 1
|
||||
)
|
||||
if (borrows > 0 || deposits > 0) {
|
||||
return { ...value, time: key }
|
||||
return { ...value, time: key, ...price }
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter((x) => x)
|
||||
.reverse()
|
||||
if (stats[asset].length === 0) {
|
||||
delete stats[asset]
|
||||
}
|
||||
}
|
||||
setLoading(false)
|
||||
setHourlyInterestStats(stats)
|
||||
}
|
||||
|
||||
fetchHourlyInterestStats()
|
||||
fetchInterestStats()
|
||||
}, [mangoAccountPk])
|
||||
const getStats = async () => {
|
||||
await fetchInterestStats()
|
||||
fetchHourlyInterestStats()
|
||||
}
|
||||
getStats()
|
||||
}, [mangoAccountPk, hideInterestDust])
|
||||
|
||||
// For net interest value to be useful we would need to filter on the dates of the user's financial year and convert the USD value below to the user's home currency.
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log(Object.entries(hourlyInterestStats).flat(Infinity))
|
||||
// const totalInterestValue = Object.entries(hourlyInterestStats)
|
||||
// .flat(Infinity)
|
||||
// .reduce((a: number, c: any) => {
|
||||
// if (c.time) {
|
||||
// return (
|
||||
// a + (c.deposit_interest * c.price - c.borrow_interest * c.price)
|
||||
// )
|
||||
// } else return a
|
||||
// }, 0)
|
||||
// setTotalInterestValue(totalInterestValue)
|
||||
// }, [hourlyInterestStats])
|
||||
|
||||
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 = []
|
||||
|
||||
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
|
||||
} else {
|
||||
dailyInterest.push({
|
||||
// @ts-ignore
|
||||
time: new Date(d.time).getTime(),
|
||||
interest:
|
||||
d.borrow_interest > 0
|
||||
? d.borrow_interest * -1
|
||||
: d.deposit_interest,
|
||||
value:
|
||||
d.borrow_interest > 0
|
||||
? d.borrow_interest * d.price * -1
|
||||
: d.deposit_interest * d.price,
|
||||
})
|
||||
}
|
||||
})
|
||||
setChartData(dailyInterest.reverse())
|
||||
}
|
||||
}, [hourlyInterestStats, selectedAsset])
|
||||
|
||||
const increaseYAxisWidth = !!chartData.find((data) => data.value < 0.001)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pb-4 text-th-fgd-1 text-lg">
|
||||
{t('interest-earned')}
|
||||
<Button
|
||||
className={`float-right text-xs h-8 pt-0 pb-0 pl-3 pr-3`}
|
||||
onClick={exportInterestDataToCSV}
|
||||
>
|
||||
<div className={`flex items-center`}>
|
||||
<SaveIcon className={`h-4 w-4 mr-1.5`} />
|
||||
{t('export-data')}
|
||||
</div>
|
||||
</Button>
|
||||
</div>{' '}
|
||||
<div className="flex items-center justify-between pb-4">
|
||||
<div className="text-th-fgd-1 text-lg">{t('interest-earned')}</div>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className={`float-right text-xs h-8 pt-0 pb-0 pl-3 pr-3`}
|
||||
onClick={exportInterestDataToCSV}
|
||||
>
|
||||
<div className={`flex items-center whitespace-nowrap`}>
|
||||
<SaveIcon className={`h-4 w-4 mr-1.5`} />
|
||||
{t('export-data')}
|
||||
</div>
|
||||
</Button>
|
||||
<Switch
|
||||
checked={hideInterestDust}
|
||||
className="ml-2 text-xs"
|
||||
onChange={() => sethideInterestDust(!hideInterestDust)}
|
||||
>
|
||||
{t('hide-dust')}
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
{mangoAccount ? (
|
||||
<div>
|
||||
{!isMobile ? (
|
||||
|
@ -288,10 +437,26 @@ const AccountInterest = () => {
|
|||
})}
|
||||
</>
|
||||
)}
|
||||
{/* {totalInterestValue > 0 ? (
|
||||
<div className="border border-th-bkg-4 mt-8 p-3 sm:p-4 rounded-md sm:rounded-lg">
|
||||
<div className="font-bold pb-0.5 text-th-fgd-1 text-xs sm:text-sm">
|
||||
{t('net-interest-value')}
|
||||
</div>
|
||||
<div className="pb-0.5 sm:pb-2 text-th-fgd-3 text-xs">
|
||||
{t('net-interest-value-desc')}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<CurrencyDollarIcon className="flex-shrink-0 h-5 w-5 sm:h-7 sm:w-7 mr-1.5 text-th-primary" />
|
||||
<div className="font-bold text-th-fgd-1 text-xl sm:text-2xl">
|
||||
{formatUsdValue(totalInterestValue)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null} */}
|
||||
<>
|
||||
{!isEmpty(hourlyInterestStats) && !loading ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between pb-4 pt-6 w-full">
|
||||
<div className="flex items-center justify-between pb-4 pt-8 w-full">
|
||||
<div className="text-th-fgd-1 text-lg">{t('history')}</div>
|
||||
<Select
|
||||
value={selectedAsset}
|
||||
|
@ -330,6 +495,50 @@ const AccountInterest = () => {
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
{selectedAsset && chartData.length > 0 ? (
|
||||
<div className="flex flex-col sm:flex-row space-x-0 sm:space-x-4 w-full">
|
||||
<div
|
||||
className="border border-th-bkg-4 relative mb-6 p-4 rounded-md w-full sm:w-1/2"
|
||||
style={{ height: '330px' }}
|
||||
>
|
||||
<Chart
|
||||
hideRangeFilters
|
||||
title={t('interest-chart-title', {
|
||||
symbol: selectedAsset,
|
||||
})}
|
||||
xAxis="time"
|
||||
yAxis="interest"
|
||||
data={chartData}
|
||||
labelFormat={(x) => x && x.toFixed(token.decimals + 1)}
|
||||
tickFormat={handleDustTicks}
|
||||
type="bar"
|
||||
yAxisWidth={increaseYAxisWidth ? 70 : 50}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="border border-th-bkg-4 relative mb-6 p-4 rounded-md w-full sm:w-1/2"
|
||||
style={{ height: '330px' }}
|
||||
>
|
||||
<Chart
|
||||
hideRangeFilters
|
||||
title={t('interest-chart-value-title', {
|
||||
symbol: selectedAsset,
|
||||
})}
|
||||
xAxis="time"
|
||||
yAxis="value"
|
||||
data={chartData}
|
||||
labelFormat={(x) =>
|
||||
x && x < 0
|
||||
? `-$${Math.abs(x).toFixed(token.decimals + 1)}`
|
||||
: `$${x.toFixed(token.decimals + 1)}`
|
||||
}
|
||||
tickFormat={handleUsdDustTicks}
|
||||
type="bar"
|
||||
yAxisWidth={increaseYAxisWidth ? 70 : 50}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<div>
|
||||
{paginatedData.length ? (
|
||||
|
@ -338,20 +547,16 @@ const AccountInterest = () => {
|
|||
<TrHead>
|
||||
<Th>{t('time')}</Th>
|
||||
<Th>{t('interest')}</Th>
|
||||
<Th>{t('value')}</Th>
|
||||
</TrHead>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginatedData.map((stat, index) => {
|
||||
const date = new Date(stat.time)
|
||||
|
||||
// @ts-ignore
|
||||
const utc = dayjs.utc(stat.time).format()
|
||||
return (
|
||||
<TrBody index={index} key={stat.time}>
|
||||
<Td>
|
||||
<div>{date.toLocaleDateString()}</div>
|
||||
<div className="text-xs text-th-fgd-3">
|
||||
{date.toLocaleTimeString()}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>{dayjs(utc).format('DD/MM/YY, h:mma')}</Td>
|
||||
<Td>
|
||||
{stat.borrow_interest > 0
|
||||
? `-${stat.borrow_interest.toFixed(
|
||||
|
@ -362,13 +567,22 @@ const AccountInterest = () => {
|
|||
)}{' '}
|
||||
{selectedAsset}
|
||||
</Td>
|
||||
<Td>
|
||||
{stat.borrow_interest > 0
|
||||
? `-$${(
|
||||
stat.borrow_interest * stat.price
|
||||
).toFixed(token.decimals + 1)}`
|
||||
: `$${(
|
||||
stat.deposit_interest * stat.price
|
||||
).toFixed(token.decimals + 1)}`}
|
||||
</Td>
|
||||
</TrBody>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="flex justify-center w-full bg-th-bkg-3 py-4">
|
||||
<div className="flex justify-center w-full bg-th-bkg-3 py-4 text-th-fgd-3">
|
||||
{t('no-interest')}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { useMemo } from 'react'
|
||||
import {
|
||||
// ChartBarIcon,
|
||||
ScaleIcon,
|
||||
CurrencyDollarIcon,
|
||||
GiftIcon,
|
||||
|
|
|
@ -118,11 +118,13 @@
|
|||
"fee": "Fee",
|
||||
"fee-discount": "Fee Discount",
|
||||
"funding": "Funding",
|
||||
"funding-chart-title": "Funding (Last 30 days)",
|
||||
"get-started": "Get Started",
|
||||
"health": "Health",
|
||||
"health-check": "Account Health Check",
|
||||
"health-ratio": "Health Ratio",
|
||||
"hide-all": "Hide all from Nav",
|
||||
"hide-dust": "Hide dust",
|
||||
"high": "High",
|
||||
"history": "History",
|
||||
"history-empty": "History empty.",
|
||||
|
@ -138,7 +140,9 @@
|
|||
"insufficient-balance-withdraw": "Insufficient balance. Borrow funds to withdraw",
|
||||
"insufficient-sol": "You need 0.035 SOL to create a Mango Account.",
|
||||
"interest": "Interest",
|
||||
"interest-earned": "Total Interest Earned/Paid",
|
||||
"interest-chart-title": "{{symbol}} Interest (Last 30 days)",
|
||||
"interest-chart-value-title": "{{symbol}} Interest Value (Last 30 days)",
|
||||
"interest-earned": "Total Interest",
|
||||
"interest-info": "Interest is earned continuously on all deposits.",
|
||||
"intro-feature-1": "Cross‑collateralized leverage trading",
|
||||
"intro-feature-2": "All assets count as collateral to trade or borrow",
|
||||
|
@ -198,16 +202,18 @@
|
|||
"name-your-account": "Name Your Account",
|
||||
"net": "Net",
|
||||
"net-balance": "Net Balance",
|
||||
"net-interest-value": "Net Interest Value",
|
||||
"net-interest-value-desc": "Calculated at the time it was earned/paid. This might be useful at tax time.",
|
||||
"new-account": "New",
|
||||
"next": "Next",
|
||||
"no-account-found": "No Account Found",
|
||||
"no-address": "No {{tokenSymbol}} wallet address found",
|
||||
"no-balances": "No balances",
|
||||
"no-borrows": "No borrows found.",
|
||||
"no-funding": "No funding earned/paid",
|
||||
"no-funding": "No funding earned or paid",
|
||||
"no-history": "No trade history",
|
||||
"no-interest": "No interest earned/paid",
|
||||
"no-margin": "No Margin Accounts Found",
|
||||
"no-interest": "No interest earned or paid",
|
||||
"no-margin": "No margin accounts found",
|
||||
"no-orders": "No open orders",
|
||||
"no-perp": "No perp positions",
|
||||
"no-unsettled": "There are no unsettled funds",
|
||||
|
|
|
@ -116,12 +116,14 @@
|
|||
"export-data-success": "CSV exported successfully",
|
||||
"fee": "Tarifa",
|
||||
"fee-discount": "comisiones",
|
||||
"funding": "Funding",
|
||||
"funding": "Fondos",
|
||||
"funding-chart-title": "Fondos (últimos 30 días)",
|
||||
"get-started": "Comenzar",
|
||||
"health": "Salud",
|
||||
"health-check": "Verificación del estado de la cuenta",
|
||||
"health-ratio": "Relación de salud",
|
||||
"hide-all": "Ocultar todo de Nav",
|
||||
"hide-dust": "Ocultar saldos pequeños",
|
||||
"high": "Alta",
|
||||
"history": "La Historia",
|
||||
"history-empty": "History empty.",
|
||||
|
@ -137,6 +139,8 @@
|
|||
"insufficient-balance-withdraw": "Saldo insuficiente. Pedir prestados fondos para retirar",
|
||||
"insufficient-sol": "Necesita 0.035 SOL para crear una cuenta de mango.",
|
||||
"interest": "Interés",
|
||||
"interest-chart-title": "{{symbol}} Interés (últimos 30 días)",
|
||||
"interest-chart-value-title": "{{symbol}} Valor de interés (últimos 30 días)",
|
||||
"interest-earned": "Interés total devengado / pagado",
|
||||
"interest-info": "El interés se gana continuamente en todos los depósitos.",
|
||||
"intro-feature-1": "Operaciones de apalancamiento con garantía cruzada",
|
||||
|
@ -196,6 +200,8 @@
|
|||
"name-your-account": "Nombra tu cuenta",
|
||||
"net": "Neto",
|
||||
"net-balance": "Balance neto",
|
||||
"net-interest-value": "Valor de interés neto",
|
||||
"net-interest-value-desc": "Calculado en el momento en que se ganó / pagó. Esto podría ser útil al momento de impuestos.",
|
||||
"new-account": "Nuevo",
|
||||
"next": "Próximo",
|
||||
"no-account-found": "No Account Found",
|
||||
|
|
|
@ -117,11 +117,13 @@
|
|||
"fee": "费率",
|
||||
"fee-discount": "费率折扣",
|
||||
"funding": "资金费",
|
||||
"funding-chart-title": "资金费",
|
||||
"get-started": "开始",
|
||||
"health": "健康度",
|
||||
"health-check": "帐户健康检查",
|
||||
"health-ratio": "健康比率",
|
||||
"hide-all": "在导航栏中隐藏全部",
|
||||
"hide-dust": "Hide dust",
|
||||
"high": "高",
|
||||
"history": "历史",
|
||||
"history-empty": "没有历史",
|
||||
|
@ -137,6 +139,8 @@
|
|||
"insufficient-balance-withdraw": "帐户余额不够。您得以借贷而前往",
|
||||
"insufficient-sol": "创建Mango帐户最少需要0.035 SOL。",
|
||||
"interest": "利息",
|
||||
"interest-chart-title": "{{symbol}} 利息",
|
||||
"interest-chart-value-title": "{{symbol}} 利息价值",
|
||||
"interest-earned": "存借利息",
|
||||
"interest-info": "您的存款会持续赚取利息。",
|
||||
"intro-feature-1": "交叉质押的杠杆交易",
|
||||
|
@ -196,6 +200,8 @@
|
|||
"name-your-account": "给帐户标签",
|
||||
"net": "净",
|
||||
"net-balance": "净余额",
|
||||
"net-interest-value": "Net Interest Value",
|
||||
"net-interest-value-desc": "Calculated at the time it was earned/paid. This might be useful at tax time.",
|
||||
"new-account": "新子帐户",
|
||||
"next": "前往",
|
||||
"no-account-found": "您没有帐户",
|
||||
|
|
|
@ -117,11 +117,13 @@
|
|||
"fee": "費率",
|
||||
"fee-discount": "費率折扣",
|
||||
"funding": "資金費",
|
||||
"funding-chart-title": "資金費",
|
||||
"get-started": "開始",
|
||||
"health": "健康度",
|
||||
"health-check": "帳戶健康檢查",
|
||||
"health-ratio": "健康比率",
|
||||
"hide-all": "在導航欄中隱藏全部",
|
||||
"hide-dust": "Hide dust",
|
||||
"high": "高",
|
||||
"history": "歷史",
|
||||
"history-empty": "沒有歷史",
|
||||
|
@ -137,6 +139,8 @@
|
|||
"insufficient-balance-withdraw": "帳戶餘額不夠。您得以借貸而前往",
|
||||
"insufficient-sol": "創建Mango帳戶最少需要0.035 SOL。",
|
||||
"interest": "利息",
|
||||
"interest-chart-title": "{{symbol}} 利息",
|
||||
"interest-chart-value-title": "{{symbol}} 利息價值",
|
||||
"interest-earned": "存借利息",
|
||||
"interest-info": "您的存款會持續賺取利息。",
|
||||
"intro-feature-1": "交叉質押的槓桿交易",
|
||||
|
@ -196,6 +200,8 @@
|
|||
"name-your-account": "給帳戶標籤",
|
||||
"net": "淨",
|
||||
"net-balance": "淨餘額",
|
||||
"net-interest-value": "Net Interest Value",
|
||||
"net-interest-value-desc": "Calculated at the time it was earned/paid. This might be useful at tax time.",
|
||||
"new-account": "新子帳戶",
|
||||
"next": "前往",
|
||||
"no-account-found": "您沒有帳戶",
|
||||
|
|
Loading…
Reference in New Issue