From aea440200d33c7048c19418bf6543b248f28345a Mon Sep 17 00:00:00 2001 From: saml33 <30796577+saml33@users.noreply.github.com> Date: Tue, 16 Apr 2024 20:53:08 +1000 Subject: [PATCH] funding history table (#411) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add desktop table * responsive and more styling * add funding history export --------- Co-authored-by: Adrian Brzeziński --- components/account/AccountStats.tsx | 4 +- components/account/FundingChart.tsx | 57 +--- components/account/FundingTable.tsx | 330 +++++++++++++++++++++++ components/account/HistoryTabs.tsx | 84 +++++- components/borrow/AssetsBorrowsTable.tsx | 2 +- hooks/useAccountHourlyFunding.ts | 56 ++++ public/locales/en/common.json | 1 + public/locales/es/account.json | 2 +- public/locales/es/common.json | 1 + public/locales/pt/account.json | 2 +- public/locales/pt/common.json | 1 + public/locales/ru/account.json | 2 +- public/locales/ru/common.json | 1 + public/locales/zh/account.json | 2 +- public/locales/zh/common.json | 1 + public/locales/zh_tw/account.json | 2 +- public/locales/zh_tw/common.json | 1 + types/index.ts | 54 ++++ 18 files changed, 543 insertions(+), 60 deletions(-) create mode 100644 components/account/FundingTable.tsx create mode 100644 hooks/useAccountHourlyFunding.ts diff --git a/components/account/AccountStats.tsx b/components/account/AccountStats.tsx index 4fd986d88..12407e36d 100644 --- a/components/account/AccountStats.tsx +++ b/components/account/AccountStats.tsx @@ -116,7 +116,7 @@ const AccountStats = ({ hideView }: { hideView: () => void }) => { />
-
+
void }) => {
-
+

{t('account:lifetime-volume')}

diff --git a/components/account/FundingChart.tsx b/components/account/FundingChart.tsx index 3976e609e..587d24204 100644 --- a/components/account/FundingChart.tsx +++ b/components/account/FundingChart.tsx @@ -1,14 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { useQuery } from '@tanstack/react-query' import dayjs from 'dayjs' -import useMangoAccount from 'hooks/useMangoAccount' import { useEffect, useMemo, useState } from 'react' -import { - HourlyFundingChartData, - HourlyFundingData, - HourlyFundingStatsData, -} from 'types' -import { DAILY_MILLISECONDS, MANGO_DATA_API_URL } from 'utils/constants' +import { HourlyFundingChartData } from 'types' +import { DAILY_MILLISECONDS } from 'utils/constants' import { formatCurrencyValue } from 'utils/numbers' import { TooltipProps } from 'recharts/types/component/Tooltip' import { @@ -32,6 +26,7 @@ import ContentBox from '@components/shared/ContentBox' import SheenLoader from '@components/shared/SheenLoader' import useThemeWrapper from 'hooks/useThemeWrapper' import FormatNumericValue from '@components/shared/FormatNumericValue' +import useAccountHourlyFunding from 'hooks/useAccountHourlyFunding' type TempDataType = { [time: string]: { @@ -41,55 +36,15 @@ type TempDataType = { } } -const fetchHourlyFunding = async (mangoAccountPk: string) => { - try { - const data = await fetch( - `${MANGO_DATA_API_URL}/stats/funding-account-hourly?mango-account=${mangoAccountPk}`, - ) - const res = await data.json() - if (res) { - const entries: HourlyFundingData[] = Object.entries(res) - - const stats: HourlyFundingStatsData[] = entries.map(([key, value]) => { - const marketEntries = Object.entries(value) - const marketFunding = marketEntries.map(([key, value]) => { - return { - long_funding: value.long_funding * -1, - short_funding: value.short_funding * -1, - time: key, - } - }) - return { marketFunding, market: key } - }) - - return stats - } - } catch (e) { - console.log('Failed to fetch account funding history', e) - } -} - const FundingChart = () => { const { t } = useTranslation('common') - const { mangoAccountAddress } = useMangoAccount() const [daysToShow, setDaysToShow] = useState('30') const { theme } = useThemeWrapper() const { data: fundingData, - isLoading: loadingFunding, - isFetching: fetchingFunding, + loading: loadingFunding, refetch, - } = useQuery( - ['hourly-funding', mangoAccountAddress], - () => fetchHourlyFunding(mangoAccountAddress), - { - cacheTime: 1000 * 60 * 10, - staleTime: 1000 * 60, - retry: 3, - refetchOnWindowFocus: false, - enabled: !!mangoAccountAddress, - }, - ) + } = useAccountHourlyFunding() useEffect(() => { refetch() @@ -219,7 +174,7 @@ const FundingChart = () => { return ( - {loadingFunding || fetchingFunding ? ( + {loadingFunding ? (
diff --git a/components/account/FundingTable.tsx b/components/account/FundingTable.tsx new file mode 100644 index 000000000..f4ff2f36d --- /dev/null +++ b/components/account/FundingTable.tsx @@ -0,0 +1,330 @@ +import { Bank, PerpMarket } from '@blockworks-foundation/mango-v4' +import { LinkButton } from '@components/shared/Button' +import FormatNumericValue from '@components/shared/FormatNumericValue' +import SheenLoader from '@components/shared/SheenLoader' +import { + SortableColumnHeader, + Table, + TableDateDisplay, + Td, + Th, + TrBody, + TrHead, +} from '@components/shared/TableElements' +import TokenLogo from '@components/shared/TokenLogo' +import MarketLogos from '@components/trade/MarketLogos' +import { Disclosure, Transition } from '@headlessui/react' +import mangoStore from '@store/mangoStore' +import { useInfiniteQuery } from '@tanstack/react-query' +import useMangoAccount from 'hooks/useMangoAccount' +import { useSortableData } from 'hooks/useSortableData' +import { useViewport } from 'hooks/useViewport' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { + MarginFundingFeed, + isCollateralFundingItem, + isPerpFundingItem, +} from 'types' +import { MANGO_DATA_API_URL, PAGINATION_PAGE_LENGTH } from 'utils/constants' +import { breakpoints } from 'utils/theme' + +type FundingItem = { + asset: string + amount: number + marketOrBank: PerpMarket | Bank | undefined + time: string + type: string + value?: number +} + +export const fetchMarginFunding = async ( + mangoAccountPk: string, + offset = 0, + noLimit?: boolean, +) => { + const params = noLimit ? '' : `&limit=${PAGINATION_PAGE_LENGTH}` + try { + const response = await fetch( + `${MANGO_DATA_API_URL}/stats/margin-funding?mango-account=${mangoAccountPk}&offset=${offset}${params}`, + ) + const parsedResponse = await response.json() + + if (Array.isArray(parsedResponse)) { + return parsedResponse + } + return [] + } catch (e) { + console.error('Failed to fetch account margin funding', e) + } +} + +const FundingTable = () => { + const { t } = useTranslation(['common', 'account']) + const { mangoAccountAddress } = useMangoAccount() + const { width } = useViewport() + const showTableView = width ? width > breakpoints.md : false + + const { + data: fundingData, + isLoading, + // isFetching, + isFetchingNextPage, + fetchNextPage, + } = useInfiniteQuery( + ['margin-funding', mangoAccountAddress], + ({ pageParam }) => fetchMarginFunding(mangoAccountAddress, pageParam), + { + cacheTime: 1000 * 60 * 10, + staleTime: 1000 * 60 * 5, + retry: 3, + refetchOnWindowFocus: false, + keepPreviousData: true, + getNextPageParam: (_lastPage, pages) => + pages.length * PAGINATION_PAGE_LENGTH, + }, + ) + + const tableData: FundingItem[] = useMemo(() => { + if (!fundingData || !fundingData?.pages.length) return [] + const group = mangoStore.getState().group + const data: FundingItem[] = [] + fundingData.pages.flat().forEach((item: MarginFundingFeed) => { + const time = item.date_time + if (isPerpFundingItem(item)) { + const asset = item.activity_details.perp_market + const amount = + item.activity_details.long_funding + + item.activity_details.short_funding + const type = 'Perp' + const market = group?.getPerpMarketByName(asset) + const perpFundingItem = { + asset, + amount, + marketOrBank: market, + type, + time, + } + if (Math.abs(amount) > 0) { + data.push(perpFundingItem) + } + } + if (isCollateralFundingItem(item)) { + const asset = item.activity_details.symbol + const amount = item.activity_details.fee_tokens * -1 + const value = item.activity_details.fee_value_usd * -1 + const type = 'Collateral' + const bank = group?.banksMapByName.get(asset)?.[0] + const collateralFundingItem = { + asset, + amount, + marketOrBank: bank, + type, + time, + value, + } + data.push(collateralFundingItem) + } + }) + data.sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime()) + return data + }, [fundingData]) + + const { + items: sortedTableData, + requestSort, + sortConfig, + } = useSortableData(tableData) + + return ( + <> + {showTableView ? ( + + + + + + + + + + + {sortedTableData.map((item, i) => { + const { asset, amount, marketOrBank, time, type, value } = item + return ( + + + + + + + ) + })} + +
+ requestSort('time')} + sortConfig={sortConfig} + title={t('date')} + /> + +
+ requestSort('asset')} + sortConfig={sortConfig} + title={t('asset')} + /> +
+
+
+ requestSort('type')} + sortConfig={sortConfig} + title={t('account:funding-type')} + /> +
+
+
+ requestSort('amount')} + sortConfig={sortConfig} + title={t('amount')} + /> +
+
+ + +
+ {marketOrBank ? ( + marketOrBank instanceof PerpMarket ? ( + + ) : ( + + ) + ) : null} +

{asset}

+
+
+

{type}

+
+

+ {type === 'Perp' ? ( + 0 ? 'text-th-up' : 'text-th-down' + }`} + > + + + ) : ( + + + {' '} + + {asset} + + + {value ? ( + + + + ) : null} + + )} +

+
+ ) : ( + sortedTableData.map((item, i) => { + const { asset, amount, marketOrBank, time, type, value } = item + return ( + + <> + +
+
+ +
+
+
+ {marketOrBank ? ( + marketOrBank instanceof PerpMarket ? ( + + ) : ( + + ) + ) : null} +

{asset}

+
+

+ {type === 'Perp' ? ( + 0 ? 'text-th-up' : 'text-th-down' + }`} + > + + + ) : ( + + + {' '} + + {asset} + + + {value ? ( + + + + ) : null} + + )} +

+
+
+
+ + +
+
+

+ {t('available')} +

+
+
+
+
+ +
+ ) + }) + )} + {isLoading || isFetchingNextPage ? ( +
+ {[...Array(20)].map((x, i) => ( + +
+ + ))} +
+ ) : null} + {tableData.length && !(tableData.length % PAGINATION_PAGE_LENGTH) ? ( + fetchNextPage()}> + {t('show-more')} + + ) : null} + + ) +} + +export default FundingTable diff --git a/components/account/HistoryTabs.tsx b/components/account/HistoryTabs.tsx index a0014b28c..c62920de9 100644 --- a/components/account/HistoryTabs.tsx +++ b/components/account/HistoryTabs.tsx @@ -26,8 +26,28 @@ import useUnownedAccount from 'hooks/useUnownedAccount' import SecondaryTabBar from '@components/shared/SecondaryTabBar' import ActivityFilters from './ActivityFilters' import { useTranslation } from 'react-i18next' +import FundingTable, { fetchMarginFunding } from './FundingTable' +import { + MarginFundingFeed, + isCollateralFundingItem, + isPerpFundingItem, +} from 'types' +import dayjs from 'dayjs' + +type FundingExportItem = { + asset: string + amount: string + time: string + type: string + value?: string +} -const TABS = ['activity:activity-feed', 'activity:swaps', 'activity:trades'] +const TABS = [ + 'activity:activity-feed', + 'activity:swaps', + 'activity:trades', + 'funding', +] const HistoryTabs = () => { const { t } = useTranslation(['common', 'account', 'activity', 'trade']) @@ -165,11 +185,71 @@ const HistoryTabs = () => { setLoadExportData(false) } + const exportFundingDataToCSV = async () => { + setLoadExportData(true) + try { + const fundingHistory = await fetchMarginFunding( + mangoAccountAddress, + 0, + true, + ) + if (fundingHistory && fundingHistory?.length) { + const dataToExport: FundingExportItem[] = [] + fundingHistory.forEach((item: MarginFundingFeed) => { + const time = dayjs(item.date_time).format('DD MMM YYYY, h:mma') + if (isPerpFundingItem(item)) { + const asset = item.activity_details.perp_market + const amount = + item.activity_details.long_funding + + item.activity_details.short_funding + const type = 'Perp' + const perpFundingItem = { + time, + asset, + type, + amount: formatCurrencyValue(amount), + } + if (Math.abs(amount) > 0) { + dataToExport.push(perpFundingItem) + } + } + if (isCollateralFundingItem(item)) { + const asset = item.activity_details.symbol + const amount = item.activity_details.fee_tokens * -1 + const value = item.activity_details.fee_value_usd * -1 + const type = 'Collateral' + const collateralFundingItem = { + time, + asset, + type, + amount: formatNumericValue(amount), + value: formatCurrencyValue(value), + } + dataToExport.push(collateralFundingItem) + } + }) + dataToExport.sort( + (a, b) => new Date(b.time).getTime() - new Date(a.time).getTime(), + ) + + const title = `${mangoAccountAddress}-Funding-${new Date().toLocaleDateString()}` + + handleExport(dataToExport, title) + } + } catch (e) { + console.log('failed to export funding data', e) + } finally { + setLoadExportData(false) + } + } + const handleExportData = (dataType: string) => { if (dataType === 'activity:activity-feed') { exportActivityDataToCSV() } else if (dataType === 'activity:swaps') { exportSwapsDataToCSV() + } else if (dataType === 'funding') { + exportFundingDataToCSV() } else { exportTradesDataToCSV() } @@ -216,6 +296,8 @@ const TabContent = ({ activeTab }: { activeTab: string }) => { return case 'activity:trades': return + case 'funding': + return default: return } diff --git a/components/borrow/AssetsBorrowsTable.tsx b/components/borrow/AssetsBorrowsTable.tsx index d244265f8..ecda1f712 100644 --- a/components/borrow/AssetsBorrowsTable.tsx +++ b/components/borrow/AssetsBorrowsTable.tsx @@ -155,7 +155,7 @@ const AssetsBorrowsTable = () => { enterTo="opacity-100" > -
+

{t('available')} diff --git a/hooks/useAccountHourlyFunding.ts b/hooks/useAccountHourlyFunding.ts new file mode 100644 index 000000000..688a0adb0 --- /dev/null +++ b/hooks/useAccountHourlyFunding.ts @@ -0,0 +1,56 @@ +import { useQuery } from '@tanstack/react-query' +import useMangoAccount from './useMangoAccount' +import { MANGO_DATA_API_URL } from 'utils/constants' +import { HourlyFundingData, HourlyFundingStatsData } from 'types' + +const fetchHourlyFunding = async (mangoAccountPk: string) => { + try { + const data = await fetch( + `${MANGO_DATA_API_URL}/stats/funding-account-hourly?mango-account=${mangoAccountPk}`, + ) + const res = await data.json() + if (res) { + const entries: HourlyFundingData[] = Object.entries(res) + + const stats: HourlyFundingStatsData[] = entries.map(([key, value]) => { + const marketEntries = Object.entries(value) + const marketFunding = marketEntries.map(([key, value]) => { + return { + long_funding: value.long_funding * -1, + short_funding: value.short_funding * -1, + time: key, + } + }) + return { marketFunding, market: key } + }) + + return stats + } + } catch (e) { + console.log('Failed to fetch account funding history', e) + } +} + +export default function useAccountHourlyFunding() { + const { mangoAccountAddress } = useMangoAccount() + + const { data, isLoading, isFetching, refetch } = useQuery( + ['hourly-funding', mangoAccountAddress], + () => fetchHourlyFunding(mangoAccountAddress), + { + cacheTime: 1000 * 60 * 10, + staleTime: 1000 * 60, + retry: 3, + refetchOnWindowFocus: false, + enabled: !!mangoAccountAddress, + }, + ) + + const loading = isLoading || isFetching + + return { + data, + loading, + refetch, + } +} diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 2c5a72e15..801e1c89e 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -21,6 +21,7 @@ "amount-owed": "Amount Owed", "announcements": "Announcements", "asked-sign-transaction": "You'll be asked to sign a transaction", + "asset": "Asset", "asset-liability-weight": "Asset/Liability Weights", "asset-liability-weight-desc": "Asset weight applies a haircut to the value of the collateral in your account health calculation. The lower the asset weight, the less the asset counts towards collateral. Liability weight does the opposite (adds to the value of the liability in your health calculation).", "asset-weight": "Asset Weight", diff --git a/public/locales/es/account.json b/public/locales/es/account.json index bf8d24ff7..b1230a7bb 100644 --- a/public/locales/es/account.json +++ b/public/locales/es/account.json @@ -17,7 +17,7 @@ "follow": "Follow", "followed-accounts": "Followed Accounts", "funding-chart": "Funding Chart", - "funding-rate": "Funding Rate", + "funding-type": "Funding Type", "hide-announcements": "Hide Announcements", "init-health": "Init Health", "maint-health": "Maint Health", diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 2c5a72e15..801e1c89e 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -21,6 +21,7 @@ "amount-owed": "Amount Owed", "announcements": "Announcements", "asked-sign-transaction": "You'll be asked to sign a transaction", + "asset": "Asset", "asset-liability-weight": "Asset/Liability Weights", "asset-liability-weight-desc": "Asset weight applies a haircut to the value of the collateral in your account health calculation. The lower the asset weight, the less the asset counts towards collateral. Liability weight does the opposite (adds to the value of the liability in your health calculation).", "asset-weight": "Asset Weight", diff --git a/public/locales/pt/account.json b/public/locales/pt/account.json index d3cf4c8d9..3cdbe55bb 100644 --- a/public/locales/pt/account.json +++ b/public/locales/pt/account.json @@ -17,7 +17,7 @@ "follow": "Seguir", "followed-accounts": "Contas Seguidas", "funding-chart": "Gráfico de Financiamento", - "funding-rate": "Funding Rate", + "funding-type": "Funding Type", "hide-announcements": "Hide Announcements", "init-health": "Saúde Inicial", "maint-health": "Saúde de Manutenção", diff --git a/public/locales/pt/common.json b/public/locales/pt/common.json index a41688291..55df0fe0b 100644 --- a/public/locales/pt/common.json +++ b/public/locales/pt/common.json @@ -21,6 +21,7 @@ "amount-owed": "Quantia Devida", "announcements": "Announcements", "asked-sign-transaction": "Você será solicitado a assinar uma transação", + "asset": "Asset", "asset-liability-weight": "Pesos de Ativo/Passivo", "asset-liability-weight-desc": "O peso do ativo aplica um desconto ao valor do colateral no cálculo da saúde da sua conta. Quanto menor o peso do ativo, menos o ativo conta como colateral. O peso do passivo faz o oposto (adiciona ao valor do passivo no cálculo da saúde).", "asset-weight": "Peso do Ativo", diff --git a/public/locales/ru/account.json b/public/locales/ru/account.json index bf8d24ff7..b1230a7bb 100644 --- a/public/locales/ru/account.json +++ b/public/locales/ru/account.json @@ -17,7 +17,7 @@ "follow": "Follow", "followed-accounts": "Followed Accounts", "funding-chart": "Funding Chart", - "funding-rate": "Funding Rate", + "funding-type": "Funding Type", "hide-announcements": "Hide Announcements", "init-health": "Init Health", "maint-health": "Maint Health", diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index 2c5a72e15..801e1c89e 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -21,6 +21,7 @@ "amount-owed": "Amount Owed", "announcements": "Announcements", "asked-sign-transaction": "You'll be asked to sign a transaction", + "asset": "Asset", "asset-liability-weight": "Asset/Liability Weights", "asset-liability-weight-desc": "Asset weight applies a haircut to the value of the collateral in your account health calculation. The lower the asset weight, the less the asset counts towards collateral. Liability weight does the opposite (adds to the value of the liability in your health calculation).", "asset-weight": "Asset Weight", diff --git a/public/locales/zh/account.json b/public/locales/zh/account.json index 4ddafa96c..5c2f30715 100644 --- a/public/locales/zh/account.json +++ b/public/locales/zh/account.json @@ -17,7 +17,7 @@ "follow": "关注", "followed-accounts": "你关注的帐户", "funding-chart": "资金费图表", - "funding-rate": "Funding Rate", + "funding-type": "Funding Type", "hide-announcements": "隐藏通知", "health-contributions": "健康度贡献", "init-health": "初始健康度", diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index ff3585708..8b9a57c76 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -21,6 +21,7 @@ "amount-owed": "欠款", "announcements": "通知", "asked-sign-transaction": "你会被要求签署交易", + "asset": "Asset", "asset-liability-weight": "资产/债务权重", "asset-liability-weight-desc": "资产权重在账户健康计算中对质押品价值进行扣减。资产权重越低,资产对质押品的影响越小。债务权重恰恰相反(在健康计算中增加债务价值)。", "asset-weight": "资产权重", diff --git a/public/locales/zh_tw/account.json b/public/locales/zh_tw/account.json index 53d8d4ce5..652648ae0 100644 --- a/public/locales/zh_tw/account.json +++ b/public/locales/zh_tw/account.json @@ -17,7 +17,7 @@ "follow": "關注", "followed-accounts": "你關注的帳戶", "funding-chart": "資金費圖表", - "funding-rate": "Funding Rate", + "funding-type": "Funding Type", "hide-announcements": "隱藏通知", "health-contributions": "健康度貢獻", "init-health": "初始健康度", diff --git a/public/locales/zh_tw/common.json b/public/locales/zh_tw/common.json index ea7c6d1e6..5d4311a02 100644 --- a/public/locales/zh_tw/common.json +++ b/public/locales/zh_tw/common.json @@ -21,6 +21,7 @@ "amount-owed": "欠款", "announcements": "通知", "asked-sign-transaction": "你會被要求簽署交易", + "asset": "Asset", "asset-liability-weight": "資產/債務權重", "asset-liability-weight-desc": "資產權重在賬戶健康計算中對質押品價值進行扣減。資產權重越低,資產對質押品的影響越小。債務權重恰恰相反(在健康計算中增加債務價值)。", "asset-weight": "資產權重", diff --git a/types/index.ts b/types/index.ts index 6772ad013..e460ba8c7 100644 --- a/types/index.ts +++ b/types/index.ts @@ -593,3 +593,57 @@ export interface FilledOrder { } export type SwapTypes = 'swap' | 'trade:trigger-order' + +export type MarginFundingFeed = { + activity_type: string + date_time: string + activity_details: PerpFundingItem | CollateralFundingItem +} + +type PerpFundingFeed = { + activity_type: string + date_time: string + activity_details: PerpFundingItem +} + +type PerpFundingItem = { + date_time: string + long_funding: number + perp_market: string + price: number + short_funding: number +} + +type CollateralFundingFeed = { + activity_type: string + date_time: string + activity_details: CollateralFundingItem +} + +type CollateralFundingItem = { + date_time: string + fee_tokens: number + fee_value_usd: number + signature: string + slot: number + symbol: string + token_price: number +} + +export function isPerpFundingItem( + item: MarginFundingFeed, +): item is PerpFundingFeed { + if (item.activity_type === 'perp_funding') { + return true + } + return false +} + +export function isCollateralFundingItem( + item: MarginFundingFeed, +): item is CollateralFundingFeed { + if (item.activity_type === 'mango_token_collateral_fees') { + return true + } + return false +}