From 19a51b729fdba00e779181eff933ef77092cdb56 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Tue, 31 Dec 2024 13:15:53 +0800 Subject: [PATCH 1/7] chore: history performance --- app/history/components/HistoryTable.tsx | 70 ++++---- src/graphql/queries.ts | 48 ++---- src/hooks/usePositionReport.ts | 9 +- src/hooks/useUserPositions.ts | 202 +++++++++++++++--------- src/utils/types.ts | 36 ----- 5 files changed, 188 insertions(+), 177 deletions(-) diff --git a/app/history/components/HistoryTable.tsx b/app/history/components/HistoryTable.tsx index 115dbb0d..a0135b15 100644 --- a/app/history/components/HistoryTable.tsx +++ b/app/history/components/HistoryTable.tsx @@ -14,7 +14,8 @@ import { getExplorerTxURL } from '@/utils/external'; import { actionTypeToText } from '@/utils/morpho'; import { getNetworkImg, getNetworkName } from '@/utils/networks'; import { findToken } from '@/utils/tokens'; -import { UserTransaction, UserTxTypes, UserRebalancerInfo } from '@/utils/types'; +import { UserTransaction, UserTxTypes, UserRebalancerInfo, Market } from '@/utils/types'; +import { useMarkets } from '@/contexts/MarketsContext'; type HistoryTableProps = { history: UserTransaction[]; @@ -34,26 +35,30 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { const [isOpen, setIsOpen] = useState(false); const [query, setQuery] = useState(''); const dropdownRef = useRef(null); + const { markets } = useMarkets(); // Get unique assets with their chain IDs const uniqueAssets = useMemo(() => { const assetMap = new Map(); history.forEach((tx) => { - const key = `${tx.data.market.loanAsset.symbol}-${tx.data.market.morphoBlue.chain.id}`; + const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey); + if (!market) return; + + const key = `${market.loanAsset.symbol}-${market.morphoBlue.chain.id}`; if (!assetMap.has(key)) { const token = findToken( - tx.data.market.loanAsset.address, - tx.data.market.morphoBlue.chain.id, + market.loanAsset.address, + market.morphoBlue.chain.id, ); assetMap.set(key, { - symbol: tx.data.market.loanAsset.symbol, - chainId: tx.data.market.morphoBlue.chain.id, + symbol: market.loanAsset.symbol, + chainId: market.morphoBlue.chain.id, img: token?.img, }); } }); return Array.from(assetMap.values()); - }, [history]); + }, [history, markets]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -83,24 +88,33 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { // Filter and sort transactions const items = useMemo(() => { const filtered = history.filter((tx) => { + const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey); + if (!market) return false; + + if (!selectedAsset) return true; + return ( - tx.data.market.loanAsset.symbol === selectedAsset.symbol && - tx.data.market.morphoBlue.chain.id === selectedAsset.chainId + market.loanAsset.symbol === selectedAsset.symbol && + market.morphoBlue.chain.id === selectedAsset.chainId ); }); const sorted = [...filtered].sort((a, b) => b.timestamp - a.timestamp); const start = (page - 1) * rowsPerPage; const end = start + rowsPerPage; return sorted.slice(start, end); - }, [history, selectedAsset, page]); + }, [history, selectedAsset, page, rowsPerPage, markets]); const pages = Math.ceil( history.filter((tx) => { if (!selectedAsset) return true; + + const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey); + if (!market) return false; + return ( - tx.data.market.loanAsset.symbol === selectedAsset.symbol && - tx.data.market.morphoBlue.chain.id === selectedAsset.chainId + market.loanAsset.symbol === selectedAsset.symbol && + market.morphoBlue.chain.id === selectedAsset.chainId ); }).length / rowsPerPage, ); @@ -271,18 +285,20 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { {items.map((tx, index) => { + const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey) as Market; + const loanToken = findToken( - tx.data.market.loanAsset.address, - tx.data.market.morphoBlue.chain.id, + market.loanAsset.address, + market.morphoBlue.chain.id, ); const collateralToken = findToken( - tx.data.market.collateralAsset.address, - tx.data.market.morphoBlue.chain.id, + market.collateralAsset.address, + market.morphoBlue.chain.id, ); - const networkImg = getNetworkImg(tx.data.market.morphoBlue.chain.id); - const networkName = getNetworkName(tx.data.market.morphoBlue.chain.id); + const networkImg = getNetworkImg(market.morphoBlue.chain.id); + const networkName = getNetworkName(market.morphoBlue.chain.id); const sign = tx.type === UserTxTypes.MarketSupply ? '+' : '-'; - const lltv = Number(formatUnits(BigInt(tx.data.market.lltv), 18)) * 100; + const lltv = Number(formatUnits(BigInt(market.lltv), 18)) * 100; const isAgent = rebalancerInfo?.transactions.some( (agentTx) => agentTx.transactionHash === tx.hash, @@ -297,13 +313,13 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { {loanToken?.img && ( {tx.data.market.loanAsset.symbol} )} - {tx.data.market.loanAsset.symbol} + {market.loanAsset.symbol}
{networkImg && ( @@ -326,10 +342,10 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) {
- {tx.data.market.uniqueKey.slice(2, 8)} + {market.uniqueKey.slice(2, 8)}
{collateralToken?.img && ( @@ -342,7 +358,7 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { /> )} - {tx.data.market.collateralAsset.symbol} + {market.collateralAsset.symbol}
@@ -363,10 +379,10 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { {sign} {formatReadable( Number( - formatUnits(BigInt(tx.data.assets), tx.data.market.loanAsset.decimals), + formatUnits(BigInt(tx.data.assets), market.loanAsset.decimals), ), )}{' '} - {tx.data.market.loanAsset.symbol} + {market.loanAsset.symbol} {isAgent && ( @@ -387,7 +403,7 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) {
diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index 875ece71..da89e054 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -165,6 +165,15 @@ export const userPositionsQuery = ` ...MarketFields } } + + } + } + ${marketFragment} +`; + +export const useHistoryQuery = ` + query getUserHistory($address: String!, $chainId: Int) { + userByAddress(address: $address, chainId: $chainId) { transactions { hash timestamp @@ -176,51 +185,14 @@ export const userPositionsQuery = ` shares assets market { - id uniqueKey - morphoBlue { - chain { - id - } - } - lltv - collateralAsset { - id - address - decimals - } - loanAsset { - id - address - decimals - symbol - } - oracle { - data { - ... on MorphoChainlinkOracleData { - baseFeedOne { - vendor - } - baseFeedTwo { - vendor - } - quoteFeedOne { - vendor - } - quoteFeedTwo { - vendor - } - } - } - } } } } } } } - ${marketFragment} -`; +` export const marketDetailQuery = ` query getMarketDetail($uniqueKey: String!, $chainId: Int) { diff --git a/src/hooks/usePositionReport.ts b/src/hooks/usePositionReport.ts index 52bfc0e9..85a37168 100644 --- a/src/hooks/usePositionReport.ts +++ b/src/hooks/usePositionReport.ts @@ -7,6 +7,7 @@ import { import { estimatedBlockNumber } from '@/utils/rpc'; import { Market, MarketPosition, UserTransaction } from '@/utils/types'; import { usePositionSnapshot } from './usePositionSnapshot'; +import { useMarkets } from './useMarkets'; export type PositionReport = { market: Market; @@ -39,6 +40,7 @@ export const usePositionReport = ( endDate?: Date, ) => { const { fetchPositionSnapshot } = usePositionSnapshot(); + const { markets } = useMarkets(); const generateReport = async (): Promise => { if (!startDate || !endDate || !selectedAsset) return null; @@ -69,8 +71,11 @@ export const usePositionReport = ( ); const relevantTxs = history.filter( - (tx) => - tx.data?.market?.loanAsset.address.toLowerCase() === selectedAsset.address.toLowerCase(), + (tx) => { + const market = markets.find((m) => m.uniqueKey === tx.data?.market?.uniqueKey); + if (!market) return false; + return market.loanAsset.address.toLowerCase() === selectedAsset.address.toLowerCase() + } ); const marketReports = ( diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts index 65b6ceea..4b5f868f 100644 --- a/src/hooks/useUserPositions.ts +++ b/src/hooks/useUserPositions.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { useState, useEffect, useCallback } from 'react'; -import { userPositionsQuery } from '@/graphql/queries'; +import { userPositionsQuery, useHistoryQuery } from '@/graphql/queries'; import { SupportedNetworks } from '@/utils/networks'; import { MarketPosition, UserTransaction } from '@/utils/types'; import { URLS } from '@/utils/urls'; @@ -12,7 +12,8 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { const [isRefetching, setIsRefetching] = useState(false); const [data, setData] = useState([]); const [history, setHistory] = useState([]); - const [error, setError] = useState(null); + const [positionsError, setPositionsError] = useState(null); + const [historyError, setHistoryError] = useState(null); const fetchData = useCallback( async (isRefetch = false) => { @@ -30,80 +31,132 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { setLoading(true); } + // Reset errors at the start of a new fetch + setPositionsError(null); + setHistoryError(null); + // Fetch position data from both networks - const [responseMainnet, responseBase] = await Promise.all([ - fetch(URLS.MORPHO_BLUE_API, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query: userPositionsQuery, - variables: { - address: user.toLowerCase(), - chainId: SupportedNetworks.Mainnet, + let marketPositions: MarketPosition[] = []; + try { + const [responseMainnet, responseBase] = await Promise.all([ + fetch(URLS.MORPHO_BLUE_API, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', }, + body: JSON.stringify({ + query: userPositionsQuery, + variables: { + address: user.toLowerCase(), + chainId: SupportedNetworks.Mainnet, + }, + }), }), - }), - fetch(URLS.MORPHO_BLUE_API, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query: userPositionsQuery, - variables: { - address: user.toLowerCase(), - chainId: SupportedNetworks.Base, + fetch(URLS.MORPHO_BLUE_API, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', }, + body: JSON.stringify({ + query: userPositionsQuery, + variables: { + address: user.toLowerCase(), + chainId: SupportedNetworks.Base, + }, + }), }), - }), - ]); - - const result1 = await responseMainnet.json(); - const result2 = await responseBase.json(); - - const marketPositions: MarketPosition[] = []; - const transactions: UserTransaction[] = []; - - // Collect positions and transactions - for (const result of [result1, result2]) { - if (result.data?.userByAddress) { - marketPositions.push( - ...(result.data.userByAddress.marketPositions as MarketPosition[]), - ); - const parsableTxs = ( - result.data.userByAddress.transactions as UserTransaction[] - ).filter((t) => t.data?.market); - transactions.push(...parsableTxs); + ]); + + const result1 = await responseMainnet.json(); + const result2 = await responseBase.json(); + + // Collect positions + for (const result of [result1, result2]) { + if (result.data?.userByAddress) { + marketPositions.push( + ...(result.data.userByAddress.marketPositions as MarketPosition[]), + ); + } } + + // Process positions and calculate earnings + const enhancedPositions = await Promise.all( + marketPositions + .filter( + (position: MarketPosition) => showEmpty || position.supplyShares.toString() !== '0', + ) + .map(async (position: MarketPosition) => { + return { + ...position, + market: { + ...position.market, + warningsWithDetail: getMarketWarningsWithDetail(position.market), + }, + }; + }), + ); + + setData(enhancedPositions); + } catch (err) { + console.error('Error fetching positions:', err); + setPositionsError(err); } - // Sort transactions by timestamp (newest first) - transactions.sort((a, b) => Number(b.timestamp) - Number(a.timestamp)); - - // Process positions and calculate earnings - const enhancedPositions = await Promise.all( - marketPositions - .filter( - (position: MarketPosition) => showEmpty || position.supplyShares.toString() !== '0', - ) - .map(async (position: MarketPosition) => { - return { - ...position, - market: { - ...position.market, - warningsWithDetail: getMarketWarningsWithDetail(position.market), + // Fetch history data from both networks + try { + const [historyMainnet, historyBase] = await Promise.all([ + fetch(URLS.MORPHO_BLUE_API, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: useHistoryQuery, + variables: { + address: user.toLowerCase(), + chainId: SupportedNetworks.Mainnet, + }, + }), + }), + fetch(URLS.MORPHO_BLUE_API, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: useHistoryQuery, + variables: { + address: user.toLowerCase(), + chainId: SupportedNetworks.Base, }, - }; + }), }), - ); + ]); + + const historyResult1 = await historyMainnet.json(); + const historyResult2 = await historyBase.json(); + + console.log('historyResult2', historyResult2) + + const transactions: UserTransaction[] = []; + + // Collect transactions + for (const result of [historyResult1, historyResult2]) { + if (result.data?.userByAddress) { + const parsableTxs = ( + result.data.userByAddress.transactions as UserTransaction[] + ).filter((t) => t.data?.market); + transactions.push(...parsableTxs); + } + } - setHistory(transactions); - setData(enhancedPositions); - } catch (_error) { - console.error('Error fetching positions:', _error); - setError(_error); + // Sort transactions by timestamp (newest first) + transactions.sort((a, b) => Number(b.timestamp) - Number(a.timestamp)); + setHistory(transactions); + } catch (err) { + console.error('Error fetching history:', err); + setHistoryError(err); + } } finally { setLoading(false); setIsRefetching(false); @@ -113,17 +166,18 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { ); useEffect(() => { - fetchData().catch(console.error); + void fetchData(); }, [fetchData]); - const refetch = useCallback( - (onSuccess?: () => void) => { - fetchData(true).then(onSuccess).catch(console.error); - }, - [fetchData], - ); - - return { loading, isRefetching, data, history, error, refetch }; + return { + data, + history, + loading, + isRefetching, + positionsError, + historyError, + refetch: () => void fetchData(true), + }; }; export default useUserPositions; diff --git a/src/utils/types.ts b/src/utils/types.ts index c644d267..441c2f6f 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -30,46 +30,10 @@ export type UserTransaction = { type: UserTxTypes; data: { __typename: UserTxTypes; - assetsUsd: number; shares: string; assets: string; market: { - id: string; uniqueKey: string; - lltv: string; - oracle: { - data: { - baseFeedOne: { - vendor: string | null; - } | null; - baseFeedTwo: { - vendor: string | null; - } | null; - quoteFeedOne: { - vendor: string | null; - } | null; - quoteFeedTwo: { - vendor: string | null; - } | null; - }; - }; - morphoBlue: { - chain: { - id: number; - }; - }; - loanAsset: { - id: string; - address: string; - decimals: number; - symbol: string; - }; - collateralAsset: { - id: string; - address: string; - decimals: number; - symbol: string; - }; }; }; }; From c48674971fadc077278c778bce5ef44b31204474 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Tue, 31 Dec 2024 15:59:23 +0800 Subject: [PATCH 2/7] chore: temp fix on history table --- app/history/components/HistoryContent.tsx | 54 ++++- app/history/components/HistoryTable.tsx | 70 +++---- src/graphql/queries.ts | 27 +++ src/hooks/usePositionReport.ts | 237 +++++++++------------- src/hooks/useUserPositions.ts | 171 +++++----------- src/hooks/useUserTransactions.ts | 88 ++++++++ 6 files changed, 348 insertions(+), 299 deletions(-) create mode 100644 src/hooks/useUserTransactions.ts diff --git a/app/history/components/HistoryContent.tsx b/app/history/components/HistoryContent.tsx index 2fbdc066..3f50c453 100644 --- a/app/history/components/HistoryContent.tsx +++ b/app/history/components/HistoryContent.tsx @@ -1,18 +1,58 @@ 'use client'; +import { useEffect, useState } from 'react'; import Link from 'next/link'; import { Button } from '@/components/common/Button'; import Header from '@/components/layout/header/Header'; import LoadingScreen from '@/components/Status/LoadingScreen'; -import useUserPositions from '@/hooks/useUserPositions'; +import useUserTransactions from '@/hooks/useUserTransactions'; import { useUserRebalancerInfo } from '@/hooks/useUserRebalancerInfo'; +import { UserTransaction } from '@/utils/types'; import { HistoryTable } from './HistoryTable'; export default function HistoryContent({ account }: { account: string }) { - const { loading, history } = useUserPositions(account); + const [transactions, setTransactions] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + const pageSize = 10; + const { loading, error, fetchTransactions } = useUserTransactions(); const { rebalancerInfo } = useUserRebalancerInfo(account); + useEffect(() => { + const loadTransactions = async () => { + const result = await fetchTransactions({ + userAddress: [account], + page: currentPage, + pageSize, + }); + + if (result) { + setTransactions(result.items); + setTotalCount(result.pageInfo.countTotal); + } + }; + + void loadTransactions(); + }, [account, currentPage, fetchTransactions]); + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + if (error) { + return ( +
+
+
+
+ Error loading transaction history. Please try again later. +
+
+
+ ); + } + return (
@@ -21,13 +61,19 @@ export default function HistoryContent({ account }: { account: string }) { {loading ? ( - ) : history.length === 0 ? ( + ) : transactions.length === 0 ? (
No transaction history available.
) : (
- +
)} diff --git a/app/history/components/HistoryTable.tsx b/app/history/components/HistoryTable.tsx index a0135b15..bbf08ebc 100644 --- a/app/history/components/HistoryTable.tsx +++ b/app/history/components/HistoryTable.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useMemo, useState, useRef, useEffect, KeyboardEvent } from 'react'; +import { useMemo, useState, useRef, useEffect } from 'react'; import { Chip, Link, Pagination } from '@nextui-org/react'; import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@nextui-org/table'; import { ExternalLinkIcon, ChevronDownIcon, TrashIcon } from '@radix-ui/react-icons'; @@ -20,6 +20,9 @@ import { useMarkets } from '@/contexts/MarketsContext'; type HistoryTableProps = { history: UserTransaction[]; rebalancerInfo?: UserRebalancerInfo; + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; }; type AssetKey = { @@ -28,9 +31,13 @@ type AssetKey = { img?: string; }; -export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { - const [page, setPage] = useState(1); - const rowsPerPage = 6; +export function HistoryTable({ + history, + rebalancerInfo, + currentPage, + totalPages, + onPageChange +}: HistoryTableProps) { const [selectedAsset, setSelectedAsset] = useState(null); const [isOpen, setIsOpen] = useState(false); const [query, setQuery] = useState(''); @@ -73,25 +80,18 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { }; }, []); - const toggleDropdown = () => setIsOpen(!isOpen); - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Enter' || event.key === ' ') { - toggleDropdown(); + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + setIsOpen(false); } }; - const filteredAssets = uniqueAssets.filter((asset) => - asset.symbol.toLowerCase().includes(query.toLowerCase()), - ); - - // Filter and sort transactions + // Filter transactions based on selected asset const items = useMemo(() => { - const filtered = history.filter((tx) => { + return history.filter((tx) => { const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey); if (!market) return false; - if (!selectedAsset) return true; return ( @@ -99,25 +99,17 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { market.morphoBlue.chain.id === selectedAsset.chainId ); }); - const sorted = [...filtered].sort((a, b) => b.timestamp - a.timestamp); - const start = (page - 1) * rowsPerPage; - const end = start + rowsPerPage; - return sorted.slice(start, end); - }, [history, selectedAsset, page, rowsPerPage, markets]); + }, [history, selectedAsset, markets]); - const pages = Math.ceil( - history.filter((tx) => { - if (!selectedAsset) return true; + const filteredAssets = uniqueAssets.filter((asset) => + asset.symbol.toLowerCase().includes(query.toLowerCase()), + ); - const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey); - if (!market) return false; + const start = (currentPage - 1) * 6; + const end = start + 6; + const paginatedItems = items.slice(start, end); - return ( - market.loanAsset.symbol === selectedAsset.symbol && - market.morphoBlue.chain.id === selectedAsset.chainId - ); - }).length / rowsPerPage, - ); + const toggleDropdown = () => setIsOpen(!isOpen); return (
@@ -192,7 +184,7 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { setSelectedAsset(asset); setIsOpen(false); setQuery(''); - setPage(1); + onPageChange(1); }} role="option" aria-selected={ @@ -206,7 +198,7 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { setSelectedAsset(asset); setIsOpen(false); setQuery(''); - setPage(1); + onPageChange(1); } }} > @@ -244,7 +236,7 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { setSelectedAsset(null); setQuery(''); setIsOpen(false); - setPage(1); + onPageChange(1); }} type="button" > @@ -269,9 +261,9 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { isCompact showControls color="default" - page={page} - total={pages} - onChange={(_page: number) => setPage(_page)} + page={currentPage} + total={totalPages} + onChange={onPageChange} />
} @@ -284,7 +276,7 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { Transaction - {items.map((tx, index) => { + {paginatedItems.map((tx, index) => { const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey) as Market; const loanToken = findToken( diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index da89e054..4fad77ec 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -266,3 +266,30 @@ export const userRebalancerInfoQuery = ` } } `; + +export const userTransactionsQuery = ` + query getUserTransactions($where: TransactionFilters) { + transactions(where: $where) { + items { + id + hash + timestamp + type + data { + __typename + ... on MarketTransferTransactionData { + shares + assets + market { + uniqueKey + } + } + } + } + pageInfo { + count + countTotal + } + } + } +`; diff --git a/src/hooks/usePositionReport.ts b/src/hooks/usePositionReport.ts index 85a37168..b4afa3a6 100644 --- a/src/hooks/usePositionReport.ts +++ b/src/hooks/usePositionReport.ts @@ -1,172 +1,131 @@ -import { Address } from 'viem'; -import { - calculateEarningsFromSnapshot, - EarningsCalculation, - filterTransactionsInPeriod, -} from '@/utils/interest'; +import { useCallback } from 'react'; +import { Token } from '@/utils/tokens'; import { estimatedBlockNumber } from '@/utils/rpc'; -import { Market, MarketPosition, UserTransaction } from '@/utils/types'; +import { Market, MarketPosition } from '@/utils/types'; import { usePositionSnapshot } from './usePositionSnapshot'; import { useMarkets } from './useMarkets'; +import useUserTransactions from './useUserTransactions'; export type PositionReport = { market: Market; - interestEarned: bigint; - totalDeposits: bigint; - totalWithdraws: bigint; - startBalance: bigint; - endBalance: bigint; - avgCapital: bigint; - apy: number; - effectiveTime: number; - transactions: UserTransaction[]; + position: MarketPosition; + earnings: { + total: number; + startTimestamp: number; + endTimestamp: number; + startPosition: MarketPosition; + endPosition: MarketPosition; + }; }; export type ReportSummary = { - totalInterestEarned: bigint; - totalDeposits: bigint; - totalWithdraws: bigint; - period: number; - marketReports: PositionReport[]; - groupedEarnings: EarningsCalculation; + positions: PositionReport[]; + totalEarnings: number; + startTimestamp: number; + endTimestamp: number; }; export const usePositionReport = ( - positions: MarketPosition[], - history: UserTransaction[], - account: Address, - selectedAsset: { address: string; chainId: number } | null, + account: string | undefined, + selectedAsset: Token | undefined, startDate?: Date, endDate?: Date, ) => { const { fetchPositionSnapshot } = usePositionSnapshot(); const { markets } = useMarkets(); + const { fetchTransactions } = useUserTransactions(); const generateReport = async (): Promise => { if (!startDate || !endDate || !selectedAsset) return null; - if (endDate.getTime() > Date.now()) { - console.log('setting end date to now'); - endDate = new Date(); + const startTimestamp = Math.floor(startDate.getTime() / 1000); + const endTimestamp = Math.floor(endDate.getTime() / 1000); + + // Get block numbers for the timestamps + const [startBlock, endBlock] = await Promise.all([ + estimatedBlockNumber(startTimestamp), + estimatedBlockNumber(endTimestamp), + ]); + + // Get position snapshots + const [startSnapshot, endSnapshot] = await Promise.all([ + fetchPositionSnapshot(account || '', startBlock), + fetchPositionSnapshot(account || '', endBlock), + ]); + + if (!startSnapshot || !endSnapshot) { + throw new Error('Failed to fetch position snapshots'); + } + + // Get all transactions within the time range + const result = await fetchTransactions({ + userAddress: [account || ''], + timestampGte: startTimestamp, + timestampLte: endTimestamp, + }); + + if (!result) { + throw new Error('Failed to fetch transactions'); } - // fetch block number at start and end date - const startBlockNumber = await estimatedBlockNumber( - selectedAsset.chainId, - startDate.getTime() / 1000, - ); - const endBlockNumber = await estimatedBlockNumber( - selectedAsset.chainId, - endDate.getTime() / 1000, - ); - - let startTimestamp = Math.floor(startDate.getTime() / 1000); - let endTimestamp = Math.floor(endDate.getTime() / 1000); - const period = endTimestamp - startTimestamp; - - const relevantPositions = positions.filter( - (position) => - position.market.loanAsset.address.toLowerCase() === selectedAsset.address.toLowerCase() && - position.market.morphoBlue.chain.id === selectedAsset.chainId, - ); - - const relevantTxs = history.filter( - (tx) => { - const market = markets.find((m) => m.uniqueKey === tx.data?.market?.uniqueKey); - if (!market) return false; - return market.loanAsset.address.toLowerCase() === selectedAsset.address.toLowerCase() - } - ); + const relevantTxs = result.items.filter((tx) => { + const market = markets.find((m) => m.uniqueKey === tx.data?.market?.uniqueKey); + if (!market) return false; + return market.loanAsset.address.toLowerCase() === selectedAsset.address.toLowerCase(); + }); const marketReports = ( await Promise.all( - relevantPositions.map(async (position) => { - const startSnapshot = await fetchPositionSnapshot( - position.market.uniqueKey, - account, - position.market.morphoBlue.chain.id, - startBlockNumber, - ); - const endSnapshot = await fetchPositionSnapshot( - position.market.uniqueKey, - account, - position.market.morphoBlue.chain.id, - endBlockNumber, - ); - - if (!startSnapshot || !endSnapshot) { - return; - } - - const marketTransactions = filterTransactionsInPeriod( - history.filter((tx) => tx.data?.market?.uniqueKey === position.market.uniqueKey), - startTimestamp, - endTimestamp, - ); - - const earnings = calculateEarningsFromSnapshot( - BigInt(endSnapshot.supplyAssets), - BigInt(startSnapshot.supplyAssets), - marketTransactions, - startTimestamp, - endTimestamp, - ); - - return { - market: position.market, - interestEarned: earnings.earned, - totalDeposits: earnings.totalDeposits, - totalWithdraws: earnings.totalWithdraws, - apy: earnings.apy, - avgCapital: earnings.avgCapital, - effectiveTime: earnings.effectiveTime, - startBalance: BigInt(startSnapshot.supplyAssets), - endBalance: BigInt(endSnapshot.supplyAssets), - transactions: marketTransactions, - }; - }), + markets + .filter((market) => market.loanAsset.address === selectedAsset.address) + .map(async (market) => { + const startPosition = startSnapshot.find( + (pos) => pos.market.uniqueKey === market.uniqueKey, + ); + const endPosition = endSnapshot.find((pos) => pos.market.uniqueKey === market.uniqueKey); + + if (!startPosition || !endPosition) return null; + + const earnings = calculateEarnings(startPosition, endPosition); + + return { + market, + position: endPosition, + earnings: { + total: earnings, + startTimestamp, + endTimestamp, + startPosition, + endPosition, + }, + }; + }), ) - ).filter((report) => report !== null && report !== undefined) as PositionReport[]; - - const totalInterestEarned = marketReports.reduce( - (sum, report) => sum + BigInt(report.interestEarned), - 0n, - ); - - const totalDeposits = marketReports.reduce( - (sum, report) => sum + BigInt(report.totalDeposits), - 0n, - ); - - const totalWithdraws = marketReports.reduce( - (sum, report) => sum + BigInt(report.totalWithdraws), - 0n, - ); - - const startBalance = marketReports.reduce( - (sum, report) => sum + BigInt(report.startBalance), - 0n, - ); - - const endBalance = marketReports.reduce((sum, report) => sum + BigInt(report.endBalance), 0n); - - const groupedEarnings = calculateEarningsFromSnapshot( - endBalance, - startBalance, - relevantTxs, - startTimestamp, - endTimestamp, - ); + ).filter((report): report is PositionReport => report !== null); + + const totalEarnings = marketReports.reduce((sum, report) => sum + report.earnings.total, 0); return { - totalInterestEarned, - totalDeposits, - totalWithdraws, - period, - marketReports, - groupedEarnings, + positions: marketReports, + totalEarnings, + startTimestamp, + endTimestamp, }; }; - return { generateReport }; + return { + generateReport: useCallback(generateReport, [ + account, + selectedAsset, + startDate, + endDate, + fetchPositionSnapshot, + fetchTransactions, + markets, + ]), + }; }; + +function calculateEarnings(startPosition: MarketPosition, endPosition: MarketPosition): number { + return 0; // TODO: Implement earnings calculation +} diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts index 4b5f868f..2e7849ef 100644 --- a/src/hooks/useUserPositions.ts +++ b/src/hooks/useUserPositions.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { useState, useEffect, useCallback } from 'react'; -import { userPositionsQuery, useHistoryQuery } from '@/graphql/queries'; +import { userPositionsQuery } from '@/graphql/queries'; import { SupportedNetworks } from '@/utils/networks'; -import { MarketPosition, UserTransaction } from '@/utils/types'; +import { MarketPosition } from '@/utils/types'; import { URLS } from '@/utils/urls'; import { getMarketWarningsWithDetail } from '@/utils/warnings'; @@ -11,9 +11,7 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { const [loading, setLoading] = useState(true); const [isRefetching, setIsRefetching] = useState(false); const [data, setData] = useState([]); - const [history, setHistory] = useState([]); const [positionsError, setPositionsError] = useState(null); - const [historyError, setHistoryError] = useState(null); const fetchData = useCallback( async (isRefetch = false) => { @@ -31,132 +29,73 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { setLoading(true); } - // Reset errors at the start of a new fetch setPositionsError(null); - setHistoryError(null); // Fetch position data from both networks - let marketPositions: MarketPosition[] = []; - try { - const [responseMainnet, responseBase] = await Promise.all([ - fetch(URLS.MORPHO_BLUE_API, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + const [responseMainnet, responseBase] = await Promise.all([ + fetch(URLS.MORPHO_BLUE_API, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: userPositionsQuery, + variables: { + address: user.toLowerCase(), + chainId: SupportedNetworks.Mainnet, }, - body: JSON.stringify({ - query: userPositionsQuery, - variables: { - address: user.toLowerCase(), - chainId: SupportedNetworks.Mainnet, - }, - }), }), - fetch(URLS.MORPHO_BLUE_API, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + }), + fetch(URLS.MORPHO_BLUE_API, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: userPositionsQuery, + variables: { + address: user.toLowerCase(), + chainId: SupportedNetworks.Base, }, - body: JSON.stringify({ - query: userPositionsQuery, - variables: { - address: user.toLowerCase(), - chainId: SupportedNetworks.Base, - }, - }), }), - ]); + }), + ]); - const result1 = await responseMainnet.json(); - const result2 = await responseBase.json(); - - // Collect positions - for (const result of [result1, result2]) { - if (result.data?.userByAddress) { - marketPositions.push( - ...(result.data.userByAddress.marketPositions as MarketPosition[]), - ); - } - } + const result1 = await responseMainnet.json(); + const result2 = await responseBase.json(); - // Process positions and calculate earnings - const enhancedPositions = await Promise.all( - marketPositions - .filter( - (position: MarketPosition) => showEmpty || position.supplyShares.toString() !== '0', - ) - .map(async (position: MarketPosition) => { - return { - ...position, - market: { - ...position.market, - warningsWithDetail: getMarketWarningsWithDetail(position.market), - }, - }; - }), - ); + const marketPositions: MarketPosition[] = []; - setData(enhancedPositions); - } catch (err) { - console.error('Error fetching positions:', err); - setPositionsError(err); + // Collect positions + for (const result of [result1, result2]) { + if (result.data?.userByAddress) { + marketPositions.push( + ...(result.data.userByAddress.marketPositions as MarketPosition[]), + ); + } } - // Fetch history data from both networks - try { - const [historyMainnet, historyBase] = await Promise.all([ - fetch(URLS.MORPHO_BLUE_API, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query: useHistoryQuery, - variables: { - address: user.toLowerCase(), - chainId: SupportedNetworks.Mainnet, + // Process positions and calculate earnings + const enhancedPositions = await Promise.all( + marketPositions + .filter( + (position: MarketPosition) => showEmpty || position.supplyShares.toString() !== '0', + ) + .map(async (position: MarketPosition) => { + return { + ...position, + market: { + ...position.market, + warningsWithDetail: getMarketWarningsWithDetail(position.market), }, - }), + }; }), - fetch(URLS.MORPHO_BLUE_API, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query: useHistoryQuery, - variables: { - address: user.toLowerCase(), - chainId: SupportedNetworks.Base, - }, - }), - }), - ]); - - const historyResult1 = await historyMainnet.json(); - const historyResult2 = await historyBase.json(); - - console.log('historyResult2', historyResult2) - - const transactions: UserTransaction[] = []; + ); - // Collect transactions - for (const result of [historyResult1, historyResult2]) { - if (result.data?.userByAddress) { - const parsableTxs = ( - result.data.userByAddress.transactions as UserTransaction[] - ).filter((t) => t.data?.market); - transactions.push(...parsableTxs); - } - } - - // Sort transactions by timestamp (newest first) - transactions.sort((a, b) => Number(b.timestamp) - Number(a.timestamp)); - setHistory(transactions); - } catch (err) { - console.error('Error fetching history:', err); - setHistoryError(err); - } + setData(enhancedPositions); + } catch (err) { + console.error('Error fetching positions:', err); + setPositionsError(err); } finally { setLoading(false); setIsRefetching(false); @@ -171,11 +110,9 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { return { data, - history, loading, isRefetching, positionsError, - historyError, refetch: () => void fetchData(true), }; }; diff --git a/src/hooks/useUserTransactions.ts b/src/hooks/useUserTransactions.ts new file mode 100644 index 00000000..ad7ce263 --- /dev/null +++ b/src/hooks/useUserTransactions.ts @@ -0,0 +1,88 @@ +import { useState, useCallback } from 'react'; +import { userTransactionsQuery } from '@/graphql/queries'; +import { UserTransaction } from '@/utils/types'; +import { URLS } from '@/utils/urls'; +import { SupportedNetworks } from '@/utils/networks'; + +export type TransactionFilters = { + userAddress?: string[]; + marketUniqueKeys?: string[]; + chainIds?: number[]; + timestampGte?: number; + timestampLte?: number; + page?: number; + pageSize?: number; +}; + +export type TransactionResponse = { + items: UserTransaction[]; + pageInfo: { + count: number; + countTotal: number; + }; +}; + +const useUserTransactions = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchTransactions = useCallback( + async (filters: TransactionFilters): Promise => { + if (!filters.userAddress?.length) { + console.error('Missing user address'); + return null; + } + + try { + setLoading(true); + setError(null); + + const response = await fetch(URLS.MORPHO_BLUE_API, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: userTransactionsQuery, + variables: { + where: { + userAddress_in: filters.userAddress, + marketUniqueKey_in: filters.marketUniqueKeys ?? null, + chainId_in: filters.chainIds ?? [SupportedNetworks.Base, SupportedNetworks.Mainnet], + timestamp_gte: filters.timestampGte ?? null, + timestamp_lte: filters.timestampLte ?? null, + }, + pagination: { + page: filters.page || 1, + pageSize: filters.pageSize || 10, + }, + }, + }), + }); + + const result = await response.json(); + + if (result.errors) { + throw new Error(result.errors[0].message); + } + + return result.data.transactions as TransactionResponse; + } catch (err) { + console.error('Error fetching transactions:', err); + setError(err); + return null; + } finally { + setLoading(false); + } + }, + [], + ); + + return { + loading, + error, + fetchTransactions, + }; +}; + +export default useUserTransactions; From aaaec53ce5fcb89490b02bfd2d044d038f59d404 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Tue, 31 Dec 2024 16:54:37 +0800 Subject: [PATCH 3/7] chore: proper paging --- app/history/components/HistoryContent.tsx | 19 +- app/history/components/HistoryTable.tsx | 28 +- .../report/components/ReportContent.tsx | 3 +- src/graphql/queries.ts | 26 +- src/hooks/usePositionReport.ts | 261 +++++++++++------- src/hooks/useUserTransactions.ts | 10 +- 6 files changed, 193 insertions(+), 154 deletions(-) diff --git a/app/history/components/HistoryContent.tsx b/app/history/components/HistoryContent.tsx index 3f50c453..4ac4ccbe 100644 --- a/app/history/components/HistoryContent.tsx +++ b/app/history/components/HistoryContent.tsx @@ -1,14 +1,13 @@ 'use client'; import { useEffect, useState } from 'react'; -import Link from 'next/link'; -import { Button } from '@/components/common/Button'; import Header from '@/components/layout/header/Header'; import LoadingScreen from '@/components/Status/LoadingScreen'; import useUserTransactions from '@/hooks/useUserTransactions'; import { useUserRebalancerInfo } from '@/hooks/useUserRebalancerInfo'; import { UserTransaction } from '@/utils/types'; import { HistoryTable } from './HistoryTable'; +import { useMarkets } from '@/contexts/MarketsContext'; export default function HistoryContent({ account }: { account: string }) { const [transactions, setTransactions] = useState([]); @@ -18,13 +17,15 @@ export default function HistoryContent({ account }: { account: string }) { const { loading, error, fetchTransactions } = useUserTransactions(); const { rebalancerInfo } = useUserRebalancerInfo(account); + const { markets } = useMarkets() useEffect(() => { const loadTransactions = async () => { const result = await fetchTransactions({ userAddress: [account], - page: currentPage, - pageSize, + first: pageSize, + skip: (currentPage - 1) * pageSize, + marketUniqueKeys: markets.map((market) => market.uniqueKey), }); if (result) { @@ -34,7 +35,7 @@ export default function HistoryContent({ account }: { account: string }) { }; void loadTransactions(); - }, [account, currentPage, fetchTransactions]); + }, [markets, account, currentPage, fetchTransactions]); const handlePageChange = (page: number) => { setCurrentPage(page); @@ -76,14 +77,6 @@ export default function HistoryContent({ account }: { account: string }) { />
)} - -
- - - -
); diff --git a/app/history/components/HistoryTable.tsx b/app/history/components/HistoryTable.tsx index bbf08ebc..5e92a698 100644 --- a/app/history/components/HistoryTable.tsx +++ b/app/history/components/HistoryTable.tsx @@ -255,17 +255,19 @@ export function HistoryTable({ wrapper: 'rounded-none shadow-none bg-surface p-6', }} bottomContent={ -
- -
+ totalPages > 1 ? ( +
+ +
+ ) : null } > @@ -276,7 +278,9 @@ export function HistoryTable({ Transaction - {paginatedItems.map((tx, index) => { + {history.map((tx, index) => { + + // safely cast here because we only fetch txs for unique id in "markets" const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey) as Market; const loanToken = findToken( diff --git a/app/positions/report/components/ReportContent.tsx b/app/positions/report/components/ReportContent.tsx index 2173c59a..da094d55 100644 --- a/app/positions/report/components/ReportContent.tsx +++ b/app/positions/report/components/ReportContent.tsx @@ -33,7 +33,7 @@ type ReportState = { }; export default function ReportContent({ account }: { account: Address }) { - const { loading, data: positions, history } = useUserPositions(account, true); + const { loading, data: positions } = useUserPositions(account, true); const [selectedAsset, setSelectedAsset] = useState(null); // Get today's date and 2 months ago @@ -114,7 +114,6 @@ export default function ReportContent({ account }: { account: Address }) { const { generateReport } = usePositionReport( positions || [], - history || [], account, selectedAsset, startDate.toDate(), diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index 4fad77ec..d91b17b1 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -171,28 +171,6 @@ export const userPositionsQuery = ` ${marketFragment} `; -export const useHistoryQuery = ` - query getUserHistory($address: String!, $chainId: Int) { - userByAddress(address: $address, chainId: $chainId) { - transactions { - hash - timestamp - type - data { - __typename - ... on MarketTransferTransactionData { - assetsUsd - shares - assets - market { - uniqueKey - } - } - } - } - } - } -` export const marketDetailQuery = ` query getMarketDetail($uniqueKey: String!, $chainId: Int) { @@ -268,8 +246,8 @@ export const userRebalancerInfoQuery = ` `; export const userTransactionsQuery = ` - query getUserTransactions($where: TransactionFilters) { - transactions(where: $where) { + query getUserTransactions($where: TransactionFilters, $first: Int, $skip: Int) { + transactions(where: $where, first: $first, skip: $skip) { items { id hash diff --git a/src/hooks/usePositionReport.ts b/src/hooks/usePositionReport.ts index b4afa3a6..689894ef 100644 --- a/src/hooks/usePositionReport.ts +++ b/src/hooks/usePositionReport.ts @@ -1,131 +1,198 @@ -import { useCallback } from 'react'; -import { Token } from '@/utils/tokens'; +import { Address } from 'viem'; +import { + calculateEarningsFromSnapshot, + EarningsCalculation, + filterTransactionsInPeriod, +} from '@/utils/interest'; import { estimatedBlockNumber } from '@/utils/rpc'; -import { Market, MarketPosition } from '@/utils/types'; +import { Market, MarketPosition, UserTransaction } from '@/utils/types'; import { usePositionSnapshot } from './usePositionSnapshot'; -import { useMarkets } from './useMarkets'; import useUserTransactions from './useUserTransactions'; +import { useMarkets } from './useMarkets'; export type PositionReport = { market: Market; - position: MarketPosition; - earnings: { - total: number; - startTimestamp: number; - endTimestamp: number; - startPosition: MarketPosition; - endPosition: MarketPosition; - }; + interestEarned: bigint; + totalDeposits: bigint; + totalWithdraws: bigint; + startBalance: bigint; + endBalance: bigint; + avgCapital: bigint; + apy: number; + effectiveTime: number; + transactions: UserTransaction[]; }; export type ReportSummary = { - positions: PositionReport[]; - totalEarnings: number; - startTimestamp: number; - endTimestamp: number; + totalInterestEarned: bigint; + totalDeposits: bigint; + totalWithdraws: bigint; + period: number; + marketReports: PositionReport[]; + groupedEarnings: EarningsCalculation; }; export const usePositionReport = ( - account: string | undefined, - selectedAsset: Token | undefined, + positions: MarketPosition[], + account: Address, + selectedAsset: { address: string; chainId: number } | null, startDate?: Date, endDate?: Date, ) => { const { fetchPositionSnapshot } = usePositionSnapshot(); - const { markets } = useMarkets(); const { fetchTransactions } = useUserTransactions(); const generateReport = async (): Promise => { if (!startDate || !endDate || !selectedAsset) return null; - const startTimestamp = Math.floor(startDate.getTime() / 1000); - const endTimestamp = Math.floor(endDate.getTime() / 1000); - - // Get block numbers for the timestamps - const [startBlock, endBlock] = await Promise.all([ - estimatedBlockNumber(startTimestamp), - estimatedBlockNumber(endTimestamp), - ]); - - // Get position snapshots - const [startSnapshot, endSnapshot] = await Promise.all([ - fetchPositionSnapshot(account || '', startBlock), - fetchPositionSnapshot(account || '', endBlock), - ]); - - if (!startSnapshot || !endSnapshot) { - throw new Error('Failed to fetch position snapshots'); + if (endDate.getTime() > Date.now()) { + console.log('setting end date to now'); + endDate = new Date(); } - // Get all transactions within the time range - const result = await fetchTransactions({ - userAddress: [account || ''], - timestampGte: startTimestamp, - timestampLte: endTimestamp, - }); + // fetch block number at start and end date + const startBlockNumber = await estimatedBlockNumber( + selectedAsset.chainId, + startDate.getTime() / 1000, + ); + const endBlockNumber = await estimatedBlockNumber( + selectedAsset.chainId, + endDate.getTime() / 1000, + ); - if (!result) { - throw new Error('Failed to fetch transactions'); + const startTimestamp = Math.floor(startDate.getTime() / 1000); + const endTimestamp = Math.floor(endDate.getTime() / 1000); + const period = endTimestamp - startTimestamp; + + const relevantPositions = positions.filter( + (position) => + position.market.loanAsset.address.toLowerCase() === selectedAsset.address.toLowerCase() && + position.market.morphoBlue.chain.id === selectedAsset.chainId, + ); + + // Fetch all transactions with pagination + const PAGE_SIZE = 100; + let allTransactions: UserTransaction[] = []; + let hasMore = true; + let skip = 0; + + while (hasMore) { + const transactionResult = await fetchTransactions({ + userAddress: [account], + chainIds: [selectedAsset.chainId], + timestampGte: startTimestamp, + timestampLte: endTimestamp, + marketUniqueKeys: relevantPositions.map((position) => position.market.uniqueKey), + first: PAGE_SIZE, + skip, + }); + + if (!transactionResult) { + throw new Error('Failed to fetch transactions'); + } + + allTransactions = [...allTransactions, ...transactionResult.items]; + + // Check if we've fetched all transactions + hasMore = transactionResult.items.length === PAGE_SIZE; + skip += PAGE_SIZE; + + // Safety check to prevent infinite loops + if (skip > PAGE_SIZE * 100) { + console.warn('Reached maximum skip limit, some transactions might be missing'); + break; + } } - const relevantTxs = result.items.filter((tx) => { - const market = markets.find((m) => m.uniqueKey === tx.data?.market?.uniqueKey); - if (!market) return false; - return market.loanAsset.address.toLowerCase() === selectedAsset.address.toLowerCase(); - }); - const marketReports = ( await Promise.all( - markets - .filter((market) => market.loanAsset.address === selectedAsset.address) - .map(async (market) => { - const startPosition = startSnapshot.find( - (pos) => pos.market.uniqueKey === market.uniqueKey, - ); - const endPosition = endSnapshot.find((pos) => pos.market.uniqueKey === market.uniqueKey); - - if (!startPosition || !endPosition) return null; - - const earnings = calculateEarnings(startPosition, endPosition); - - return { - market, - position: endPosition, - earnings: { - total: earnings, - startTimestamp, - endTimestamp, - startPosition, - endPosition, - }, - }; - }), + relevantPositions.map(async (position) => { + const startSnapshot = await fetchPositionSnapshot( + position.market.uniqueKey, + account, + position.market.morphoBlue.chain.id, + startBlockNumber, + ); + const endSnapshot = await fetchPositionSnapshot( + position.market.uniqueKey, + account, + position.market.morphoBlue.chain.id, + endBlockNumber, + ); + + if (!startSnapshot || !endSnapshot) { + return; + } + + const marketTransactions = filterTransactionsInPeriod( + allTransactions.filter((tx) => tx.data?.market?.uniqueKey === position.market.uniqueKey), + startTimestamp, + endTimestamp, + ); + + const earnings = calculateEarningsFromSnapshot( + BigInt(endSnapshot.supplyAssets), + BigInt(startSnapshot.supplyAssets), + marketTransactions, + startTimestamp, + endTimestamp, + ); + + return { + market: position.market, + interestEarned: earnings.earned, + totalDeposits: earnings.totalDeposits, + totalWithdraws: earnings.totalWithdraws, + apy: earnings.apy, + avgCapital: earnings.avgCapital, + effectiveTime: earnings.effectiveTime, + startBalance: BigInt(startSnapshot.supplyAssets), + endBalance: BigInt(endSnapshot.supplyAssets), + transactions: marketTransactions, + }; + }), ) - ).filter((report): report is PositionReport => report !== null); - - const totalEarnings = marketReports.reduce((sum, report) => sum + report.earnings.total, 0); - - return { - positions: marketReports, - totalEarnings, + ).filter((report) => report !== null && report !== undefined) as PositionReport[]; + + const totalInterestEarned = marketReports.reduce( + (sum, report) => sum + BigInt(report.interestEarned), + 0n, + ); + + const totalDeposits = marketReports.reduce( + (sum, report) => sum + BigInt(report.totalDeposits), + 0n, + ); + + const totalWithdraws = marketReports.reduce( + (sum, report) => sum + BigInt(report.totalWithdraws), + 0n, + ); + + const startBalance = marketReports.reduce( + (sum, report) => sum + BigInt(report.startBalance), + 0n, + ); + + const endBalance = marketReports.reduce((sum, report) => sum + BigInt(report.endBalance), 0n); + + const groupedEarnings = calculateEarningsFromSnapshot( + endBalance, + startBalance, + allTransactions, startTimestamp, endTimestamp, - }; - }; + ); - return { - generateReport: useCallback(generateReport, [ - account, - selectedAsset, - startDate, - endDate, - fetchPositionSnapshot, - fetchTransactions, - markets, - ]), + return { + totalInterestEarned, + totalDeposits, + totalWithdraws, + period, + marketReports, + groupedEarnings, + }; }; -}; -function calculateEarnings(startPosition: MarketPosition, endPosition: MarketPosition): number { - return 0; // TODO: Implement earnings calculation -} + return { generateReport }; +}; \ No newline at end of file diff --git a/src/hooks/useUserTransactions.ts b/src/hooks/useUserTransactions.ts index ad7ce263..39538cd0 100644 --- a/src/hooks/useUserTransactions.ts +++ b/src/hooks/useUserTransactions.ts @@ -10,8 +10,8 @@ export type TransactionFilters = { chainIds?: number[]; timestampGte?: number; timestampLte?: number; - page?: number; - pageSize?: number; + skip?: number; + first?: number; }; export type TransactionResponse = { @@ -52,10 +52,8 @@ const useUserTransactions = () => { timestamp_gte: filters.timestampGte ?? null, timestamp_lte: filters.timestampLte ?? null, }, - pagination: { - page: filters.page || 1, - pageSize: filters.pageSize || 10, - }, + first: filters.first ?? 1000, + skip: filters.skip ?? 0, }, }), }); From f0e5bef0305a7949a31f983048dfc69af5aadb13 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Tue, 31 Dec 2024 17:30:34 +0800 Subject: [PATCH 4/7] chore: temp fixes --- app/history/components/HistoryContent.tsx | 12 ++--- app/history/components/HistoryTable.tsx | 50 ++++--------------- app/positions/components/agent/Main.tsx | 39 +++++++++++---- .../components/agent/SetupAgentModal.tsx | 3 +- src/graphql/queries.ts | 1 - src/hooks/usePositionReport.ts | 7 +-- src/hooks/useUserPositionsSummaryData.ts | 14 ++++-- src/hooks/useUserTransactions.ts | 20 ++++---- 8 files changed, 72 insertions(+), 74 deletions(-) diff --git a/app/history/components/HistoryContent.tsx b/app/history/components/HistoryContent.tsx index 4ac4ccbe..26e47765 100644 --- a/app/history/components/HistoryContent.tsx +++ b/app/history/components/HistoryContent.tsx @@ -3,11 +3,11 @@ import { useEffect, useState } from 'react'; import Header from '@/components/layout/header/Header'; import LoadingScreen from '@/components/Status/LoadingScreen'; -import useUserTransactions from '@/hooks/useUserTransactions'; +import { useMarkets } from '@/contexts/MarketsContext'; import { useUserRebalancerInfo } from '@/hooks/useUserRebalancerInfo'; +import useUserTransactions from '@/hooks/useUserTransactions'; import { UserTransaction } from '@/utils/types'; import { HistoryTable } from './HistoryTable'; -import { useMarkets } from '@/contexts/MarketsContext'; export default function HistoryContent({ account }: { account: string }) { const [transactions, setTransactions] = useState([]); @@ -17,7 +17,7 @@ export default function HistoryContent({ account }: { account: string }) { const { loading, error, fetchTransactions } = useUserTransactions(); const { rebalancerInfo } = useUserRebalancerInfo(account); - const { markets } = useMarkets() + const { markets } = useMarkets(); useEffect(() => { const loadTransactions = async () => { @@ -68,9 +68,9 @@ export default function HistoryContent({ account }: { account: string }) {
) : (
- (null); const [isOpen, setIsOpen] = useState(false); @@ -48,15 +48,12 @@ export function HistoryTable({ const uniqueAssets = useMemo(() => { const assetMap = new Map(); history.forEach((tx) => { - const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey); + const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey); if (!market) return; const key = `${market.loanAsset.symbol}-${market.morphoBlue.chain.id}`; if (!assetMap.has(key)) { - const token = findToken( - market.loanAsset.address, - market.morphoBlue.chain.id, - ); + const token = findToken(market.loanAsset.address, market.morphoBlue.chain.id); assetMap.set(key, { symbol: market.loanAsset.symbol, chainId: market.morphoBlue.chain.id, @@ -86,29 +83,10 @@ export function HistoryTable({ } }; - // Filter transactions based on selected asset - const items = useMemo(() => { - return history.filter((tx) => { - const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey); - if (!market) return false; - - if (!selectedAsset) return true; - - return ( - market.loanAsset.symbol === selectedAsset.symbol && - market.morphoBlue.chain.id === selectedAsset.chainId - ); - }); - }, [history, selectedAsset, markets]); - const filteredAssets = uniqueAssets.filter((asset) => asset.symbol.toLowerCase().includes(query.toLowerCase()), ); - const start = (currentPage - 1) * 6; - const end = start + 6; - const paginatedItems = items.slice(start, end); - const toggleDropdown = () => setIsOpen(!isOpen); return ( @@ -279,14 +257,10 @@ export function HistoryTable({ {history.map((tx, index) => { - // safely cast here because we only fetch txs for unique id in "markets" const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey) as Market; - const loanToken = findToken( - market.loanAsset.address, - market.morphoBlue.chain.id, - ); + const loanToken = findToken(market.loanAsset.address, market.morphoBlue.chain.id); const collateralToken = findToken( market.collateralAsset.address, market.morphoBlue.chain.id, @@ -374,11 +348,9 @@ export function HistoryTable({ > {sign} {formatReadable( - Number( - formatUnits(BigInt(tx.data.assets), market.loanAsset.decimals), - ), + Number(formatUnits(BigInt(tx.data.assets), market.loanAsset.decimals)), )}{' '} - {market.loanAsset.symbol} + {market.loanAsset.symbol} {isAgent && ( diff --git a/app/positions/components/agent/Main.tsx b/app/positions/components/agent/Main.tsx index 260511d3..f77ee9c5 100644 --- a/app/positions/components/agent/Main.tsx +++ b/app/positions/components/agent/Main.tsx @@ -10,6 +10,8 @@ import { useMarkets } from '@/contexts/MarketsContext'; import { findAgent } from '@/utils/monarch-agent'; import { findToken } from '@/utils/tokens'; import { UserRebalancerInfo, UserTransaction } from '@/utils/types'; +import useUserTransactions from '@/hooks/useUserTransactions'; +import { useMemo, useState, useEffect } from 'react'; const img = require('../../../../src/imgs/agent/agent-detailed.png') as string; @@ -17,12 +19,36 @@ type MainProps = { account?: string; onNext: () => void; userRebalancerInfo: UserRebalancerInfo; - history: UserTransaction[]; }; -export function Main({ account, onNext, userRebalancerInfo, history }: MainProps) { +export function Main({ account, onNext, userRebalancerInfo }: MainProps) { const agent = findAgent(userRebalancerInfo.rebalancer); const { markets } = useMarkets(); + const { fetchTransactions } = useUserTransactions(); + const [lastTx, setLastTx] = useState(null); + + useEffect(() => { + const fetchLastTransaction = async () => { + if (!account) return; + + // Get the most recent bot transaction hash + const lastBotTxHash = userRebalancerInfo.transactions.at(0)?.transactionHash; + + if (lastBotTxHash) { + const result = await fetchTransactions({ + userAddress: [account], + hash: lastBotTxHash, + first: 1, + }); + + if (result?.items.length > 0) { + setLastTx(result.items[0]); + } + } + }; + + void fetchLastTransaction(); + }, [userRebalancerInfo.transactions, fetchTransactions, account]); if (!agent) { return null; @@ -52,15 +78,6 @@ export function Main({ account, onNext, userRebalancerInfo, history }: MainProps {} as Record, ); - // sort from newest to oldest - const lastTx = history - .sort((a, b) => b.timestamp - a.timestamp) - .find((tx) => - userRebalancerInfo.transactions.find( - (t) => t.transactionHash.toLowerCase() === tx.hash.toLowerCase(), - ), - ); - return (
(SetupStep.Main); const [pendingCaps, setPendingCaps] = useState([]); - const { data: positions, history } = useUserPositions(account, true); + const { data: positions } = useUserPositions(account, true); const { markets: allMarkets } = useMarkets(); @@ -191,7 +191,6 @@ export function SetupAgentModal({ onNext={handleNext} account={account} userRebalancerInfo={userRebalancerInfo} - history={history} /> )} {currentStep === SetupStep.Setup && ( diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index d91b17b1..75d563ad 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -171,7 +171,6 @@ export const userPositionsQuery = ` ${marketFragment} `; - export const marketDetailQuery = ` query getMarketDetail($uniqueKey: String!, $chainId: Int) { marketByUniqueKey(uniqueKey: $uniqueKey, chainId: $chainId) { diff --git a/src/hooks/usePositionReport.ts b/src/hooks/usePositionReport.ts index 689894ef..039a04dd 100644 --- a/src/hooks/usePositionReport.ts +++ b/src/hooks/usePositionReport.ts @@ -8,7 +8,6 @@ import { estimatedBlockNumber } from '@/utils/rpc'; import { Market, MarketPosition, UserTransaction } from '@/utils/types'; import { usePositionSnapshot } from './usePositionSnapshot'; import useUserTransactions from './useUserTransactions'; -import { useMarkets } from './useMarkets'; export type PositionReport = { market: Market; @@ -125,7 +124,9 @@ export const usePositionReport = ( } const marketTransactions = filterTransactionsInPeriod( - allTransactions.filter((tx) => tx.data?.market?.uniqueKey === position.market.uniqueKey), + allTransactions.filter( + (tx) => tx.data?.market?.uniqueKey === position.market.uniqueKey, + ), startTimestamp, endTimestamp, ); @@ -195,4 +196,4 @@ export const usePositionReport = ( }; return { generateReport }; -}; \ No newline at end of file +}; diff --git a/src/hooks/useUserPositionsSummaryData.ts b/src/hooks/useUserPositionsSummaryData.ts index dc12fa0b..4a4549ae 100644 --- a/src/hooks/useUserPositionsSummaryData.ts +++ b/src/hooks/useUserPositionsSummaryData.ts @@ -11,6 +11,7 @@ import { } from '@/utils/types'; import { usePositionSnapshot } from './usePositionSnapshot'; import useUserPositions from './useUserPositions'; +import useUserTransactions from './useUserTransactions'; type BlockNumbers = { day: number; @@ -27,12 +28,13 @@ const useUserPositionsSummaryData = (user: string | undefined) => { loading: positionsLoading, isRefetching, data: positions, - history, - error: positionsError, + positionsError, refetch, } = useUserPositions(user, true); const { fetchPositionSnapshot } = usePositionSnapshot(); + const { fetchTransactions } = useUserTransactions(); + const [positionsWithEarnings, setPositionsWithEarnings] = useState( [], ); @@ -167,9 +169,15 @@ const useUserPositionsSummaryData = (user: string | undefined) => { const positionsWithEarningsData = await Promise.all( positions.map(async (position) => { + + const history = await fetchTransactions({ + userAddress: [user], + marketUniqueKeys: [position.market.uniqueKey], + }); + const earned = await calculateEarningsFromPeriod( position, - history, + history.items, user as Address, position.market.morphoBlue.chain.id as SupportedNetworks, ); diff --git a/src/hooks/useUserTransactions.ts b/src/hooks/useUserTransactions.ts index 39538cd0..2d036a80 100644 --- a/src/hooks/useUserTransactions.ts +++ b/src/hooks/useUserTransactions.ts @@ -1,17 +1,18 @@ import { useState, useCallback } from 'react'; import { userTransactionsQuery } from '@/graphql/queries'; +import { SupportedNetworks } from '@/utils/networks'; import { UserTransaction } from '@/utils/types'; import { URLS } from '@/utils/urls'; -import { SupportedNetworks } from '@/utils/networks'; export type TransactionFilters = { - userAddress?: string[]; + userAddress: string[]; marketUniqueKeys?: string[]; chainIds?: number[]; timestampGte?: number; timestampLte?: number; skip?: number; first?: number; + hash?: string; }; export type TransactionResponse = { @@ -20,6 +21,7 @@ export type TransactionResponse = { count: number; countTotal: number; }; + error: string | null; }; const useUserTransactions = () => { @@ -27,12 +29,7 @@ const useUserTransactions = () => { const [error, setError] = useState(null); const fetchTransactions = useCallback( - async (filters: TransactionFilters): Promise => { - if (!filters.userAddress?.length) { - console.error('Missing user address'); - return null; - } - + async (filters: TransactionFilters): Promise => { try { setLoading(true); setError(null); @@ -51,6 +48,7 @@ const useUserTransactions = () => { chainId_in: filters.chainIds ?? [SupportedNetworks.Base, SupportedNetworks.Mainnet], timestamp_gte: filters.timestampGte ?? null, timestamp_lte: filters.timestampLte ?? null, + hash: filters.hash ?? null, }, first: filters.first ?? 1000, skip: filters.skip ?? 0, @@ -68,7 +66,11 @@ const useUserTransactions = () => { } catch (err) { console.error('Error fetching transactions:', err); setError(err); - return null; + return { + items: [], + pageInfo: { count: 0, countTotal: 0 }, + error: err instanceof Error ? err.message : 'Unknown error occurred', + }; } finally { setLoading(false); } From de31ffbda40d86541364f3fe120a89bc4139462c Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Tue, 31 Dec 2024 17:50:51 +0800 Subject: [PATCH 5/7] chore: all build errors --- app/history/components/HistoryContent.tsx | 4 +++- app/history/components/HistoryTable.tsx | 19 +++++++++++++++++-- app/positions/components/agent/Main.tsx | 4 ++-- src/hooks/useUserPositionsSummaryData.ts | 3 +-- src/hooks/useUserTransactions.ts | 7 +++++-- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/app/history/components/HistoryContent.tsx b/app/history/components/HistoryContent.tsx index 26e47765..7b45abab 100644 --- a/app/history/components/HistoryContent.tsx +++ b/app/history/components/HistoryContent.tsx @@ -13,6 +13,7 @@ export default function HistoryContent({ account }: { account: string }) { const [transactions, setTransactions] = useState([]); const [totalCount, setTotalCount] = useState(0); const [currentPage, setCurrentPage] = useState(1); + const [isInitialized, setIsInitialized] = useState(false); const pageSize = 10; const { loading, error, fetchTransactions } = useUserTransactions(); @@ -32,6 +33,7 @@ export default function HistoryContent({ account }: { account: string }) { setTransactions(result.items); setTotalCount(result.pageInfo.countTotal); } + setIsInitialized(true); }; void loadTransactions(); @@ -60,7 +62,7 @@ export default function HistoryContent({ account }: { account: string }) {

Transaction History

- {loading ? ( + {loading || !isInitialized ? ( ) : transactions.length === 0 ? (
diff --git a/app/history/components/HistoryTable.tsx b/app/history/components/HistoryTable.tsx index ead10320..5749edb7 100644 --- a/app/history/components/HistoryTable.tsx +++ b/app/history/components/HistoryTable.tsx @@ -89,6 +89,21 @@ export function HistoryTable({ const toggleDropdown = () => setIsOpen(!isOpen); + // Filter transactions based on selected asset + const filteredHistory = useMemo(() => { + if (!selectedAsset) return history; + + return history.filter((tx) => { + const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey); + if (!market) return false; + + return ( + market.loanAsset.symbol === selectedAsset.symbol && + market.morphoBlue.chain.id === selectedAsset.chainId + ); + }); + }, [history, selectedAsset, markets]); + return (
@@ -255,8 +270,8 @@ export function HistoryTable({ Time Transaction - - {history.map((tx, index) => { + + {filteredHistory.map((tx, index) => { // safely cast here because we only fetch txs for unique id in "markets" const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey) as Market; diff --git a/app/positions/components/agent/Main.tsx b/app/positions/components/agent/Main.tsx index f77ee9c5..fcc535dc 100644 --- a/app/positions/components/agent/Main.tsx +++ b/app/positions/components/agent/Main.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from 'react'; import { Tooltip } from '@nextui-org/react'; import { motion } from 'framer-motion'; import moment from 'moment'; @@ -7,11 +8,10 @@ import { GrStatusGood } from 'react-icons/gr'; import { Button } from '@/components/common'; import { TooltipContent } from '@/components/TooltipContent'; import { useMarkets } from '@/contexts/MarketsContext'; +import useUserTransactions from '@/hooks/useUserTransactions'; import { findAgent } from '@/utils/monarch-agent'; import { findToken } from '@/utils/tokens'; import { UserRebalancerInfo, UserTransaction } from '@/utils/types'; -import useUserTransactions from '@/hooks/useUserTransactions'; -import { useMemo, useState, useEffect } from 'react'; const img = require('../../../../src/imgs/agent/agent-detailed.png') as string; diff --git a/src/hooks/useUserPositionsSummaryData.ts b/src/hooks/useUserPositionsSummaryData.ts index 4a4549ae..d28a991e 100644 --- a/src/hooks/useUserPositionsSummaryData.ts +++ b/src/hooks/useUserPositionsSummaryData.ts @@ -169,7 +169,6 @@ const useUserPositionsSummaryData = (user: string | undefined) => { const positionsWithEarningsData = await Promise.all( positions.map(async (position) => { - const history = await fetchTransactions({ userAddress: [user], marketUniqueKeys: [position.market.uniqueKey], @@ -197,7 +196,7 @@ const useUserPositionsSummaryData = (user: string | undefined) => { }; void updatePositionsWithEarnings(); - }, [positions, user, blockNums, history, calculateEarningsFromPeriod]); + }, [positions, user, blockNums, calculateEarningsFromPeriod]); return { positions: positionsWithEarnings, diff --git a/src/hooks/useUserTransactions.ts b/src/hooks/useUserTransactions.ts index 2d036a80..af6114ee 100644 --- a/src/hooks/useUserTransactions.ts +++ b/src/hooks/useUserTransactions.ts @@ -56,13 +56,16 @@ const useUserTransactions = () => { }), }); - const result = await response.json(); + const result = (await response.json()) as { + data?: { transactions?: TransactionResponse }; + errors?: { message: string }[]; + }; if (result.errors) { throw new Error(result.errors[0].message); } - return result.data.transactions as TransactionResponse; + return result.data?.transactions as TransactionResponse; } catch (err) { console.error('Error fetching transactions:', err); setError(err); From bd23390a9c8c9d078cb1938b86ce8c89d734e809 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Wed, 1 Jan 2025 22:34:26 +0800 Subject: [PATCH 6/7] chore: styling --- app/history/components/HistoryContent.tsx | 70 +--- app/history/components/HistoryTable.tsx | 395 ++++++++++++---------- app/positions/components/agent/Main.tsx | 2 +- src/hooks/useUserTransactions.ts | 2 + 4 files changed, 221 insertions(+), 248 deletions(-) diff --git a/app/history/components/HistoryContent.tsx b/app/history/components/HistoryContent.tsx index 7b45abab..da2c499f 100644 --- a/app/history/components/HistoryContent.tsx +++ b/app/history/components/HistoryContent.tsx @@ -1,60 +1,14 @@ 'use client'; -import { useEffect, useState } from 'react'; import Header from '@/components/layout/header/Header'; -import LoadingScreen from '@/components/Status/LoadingScreen'; -import { useMarkets } from '@/contexts/MarketsContext'; +import useUserPositions from '@/hooks/useUserPositions'; import { useUserRebalancerInfo } from '@/hooks/useUserRebalancerInfo'; -import useUserTransactions from '@/hooks/useUserTransactions'; -import { UserTransaction } from '@/utils/types'; import { HistoryTable } from './HistoryTable'; export default function HistoryContent({ account }: { account: string }) { - const [transactions, setTransactions] = useState([]); - const [totalCount, setTotalCount] = useState(0); - const [currentPage, setCurrentPage] = useState(1); - const [isInitialized, setIsInitialized] = useState(false); - const pageSize = 10; + const { data: positions } = useUserPositions(account, true); - const { loading, error, fetchTransactions } = useUserTransactions(); const { rebalancerInfo } = useUserRebalancerInfo(account); - const { markets } = useMarkets(); - - useEffect(() => { - const loadTransactions = async () => { - const result = await fetchTransactions({ - userAddress: [account], - first: pageSize, - skip: (currentPage - 1) * pageSize, - marketUniqueKeys: markets.map((market) => market.uniqueKey), - }); - - if (result) { - setTransactions(result.items); - setTotalCount(result.pageInfo.countTotal); - } - setIsInitialized(true); - }; - - void loadTransactions(); - }, [markets, account, currentPage, fetchTransactions]); - - const handlePageChange = (page: number) => { - setCurrentPage(page); - }; - - if (error) { - return ( -
-
-
-
- Error loading transaction history. Please try again later. -
-
-
- ); - } return (
@@ -62,23 +16,9 @@ export default function HistoryContent({ account }: { account: string }) {

Transaction History

- {loading || !isInitialized ? ( - - ) : transactions.length === 0 ? ( -
- No transaction history available. -
- ) : ( -
- -
- )} +
+ +
); diff --git a/app/history/components/HistoryTable.tsx b/app/history/components/HistoryTable.tsx index 5749edb7..1453477c 100644 --- a/app/history/components/HistoryTable.tsx +++ b/app/history/components/HistoryTable.tsx @@ -7,22 +7,27 @@ import moment from 'moment'; import Image from 'next/image'; import { RiRobot2Line } from 'react-icons/ri'; import { formatUnits } from 'viem'; - import { Badge } from '@/components/common/Badge'; +import LoadingScreen from '@/components/Status/LoadingScreen'; import { useMarkets } from '@/contexts/MarketsContext'; +import useUserTransactions from '@/hooks/useUserTransactions'; import { formatReadable } from '@/utils/balance'; import { getExplorerTxURL } from '@/utils/external'; import { actionTypeToText } from '@/utils/morpho'; import { getNetworkImg, getNetworkName } from '@/utils/networks'; import { findToken } from '@/utils/tokens'; -import { UserTransaction, UserTxTypes, UserRebalancerInfo, Market } from '@/utils/types'; +import { + UserTxTypes, + UserRebalancerInfo, + Market, + MarketPosition, + UserTransaction, +} from '@/utils/types'; type HistoryTableProps = { - history: UserTransaction[]; + account: string | undefined; + positions: MarketPosition[]; rebalancerInfo?: UserRebalancerInfo; - currentPage: number; - totalPages: number; - onPageChange: (page: number) => void; }; type AssetKey = { @@ -31,24 +36,25 @@ type AssetKey = { img?: string; }; -export function HistoryTable({ - history, - rebalancerInfo, - currentPage, - totalPages, - onPageChange, -}: HistoryTableProps) { +export function HistoryTable({ account, positions, rebalancerInfo }: HistoryTableProps) { const [selectedAsset, setSelectedAsset] = useState(null); const [isOpen, setIsOpen] = useState(false); const [query, setQuery] = useState(''); const dropdownRef = useRef(null); const { markets } = useMarkets(); + const { loading, fetchTransactions } = useUserTransactions(); + const [currentPage, setCurrentPage] = useState(1); + const [history, setHistory] = useState([]); + const [isInitialized, setIsInitialized] = useState(false); + const [totalPages, setTotalPages] = useState(0); + const pageSize = 10; + // Get unique assets with their chain IDs const uniqueAssets = useMemo(() => { const assetMap = new Map(); - history.forEach((tx) => { - const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey); + positions.forEach((pos) => { + const market = markets.find((m) => m.uniqueKey === pos.market.uniqueKey); if (!market) return; const key = `${market.loanAsset.symbol}-${market.morphoBlue.chain.id}`; @@ -62,7 +68,41 @@ export function HistoryTable({ } }); return Array.from(assetMap.values()); - }, [history, markets]); + }, [positions, markets]); + + // Get filtered market IDs based on selected asset + const filteredMarketIds = useMemo(() => { + if (!selectedAsset) return markets.map((m) => m.uniqueKey); + + return markets + .filter( + (m) => + m.loanAsset.symbol === selectedAsset.symbol && + m.morphoBlue.chain.id === selectedAsset.chainId, + ) + .map((m) => m.uniqueKey); + }, [selectedAsset, markets]); + + useEffect(() => { + const loadTransactions = async () => { + if (!account || !fetchTransactions) return; + + const result = await fetchTransactions({ + userAddress: [account], + first: pageSize, + skip: (currentPage - 1) * pageSize, + marketUniqueKeys: filteredMarketIds, + }); + + if (result) { + setHistory(result.items); + setTotalPages(Math.ceil(result.pageInfo.countTotal / pageSize)); + } + setIsInitialized(true); + }; + + void loadTransactions(); + }, [markets, account, currentPage, fetchTransactions, filteredMarketIds]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -89,21 +129,6 @@ export function HistoryTable({ const toggleDropdown = () => setIsOpen(!isOpen); - // Filter transactions based on selected asset - const filteredHistory = useMemo(() => { - if (!selectedAsset) return history; - - return history.filter((tx) => { - const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey); - if (!market) return false; - - return ( - market.loanAsset.symbol === selectedAsset.symbol && - market.morphoBlue.chain.id === selectedAsset.chainId - ); - }); - }, [history, selectedAsset, markets]); - return (
@@ -177,7 +202,7 @@ export function HistoryTable({ setSelectedAsset(asset); setIsOpen(false); setQuery(''); - onPageChange(1); + setCurrentPage(1); }} role="option" aria-selected={ @@ -191,7 +216,7 @@ export function HistoryTable({ setSelectedAsset(asset); setIsOpen(false); setQuery(''); - onPageChange(1); + setCurrentPage(1); } }} > @@ -229,7 +254,7 @@ export function HistoryTable({ setSelectedAsset(null); setQuery(''); setIsOpen(false); - onPageChange(1); + setCurrentPage(1); }} type="button" > @@ -242,164 +267,170 @@ export function HistoryTable({ )}
- 1 ? ( -
- -
- ) : null - } - > - - Asset & Network - Market Details - Action & Amount - Time - Transaction - - - {filteredHistory.map((tx, index) => { - // safely cast here because we only fetch txs for unique id in "markets" - const market = markets.find((m) => m.uniqueKey === tx.data.market.uniqueKey) as Market; + {!isInitialized || loading ? ( + + ) : ( +
1 ? ( +
+ +
+ ) : null + } + > + + Asset & Network + Market Details + Action & Amount + Time + Transaction + + + {history.map((tx, index) => { + // safely cast here because we only fetch txs for unique id in "markets" + const market = markets.find( + (m) => m.uniqueKey === tx.data.market.uniqueKey, + ) as Market; - const loanToken = findToken(market.loanAsset.address, market.morphoBlue.chain.id); - const collateralToken = findToken( - market.collateralAsset.address, - market.morphoBlue.chain.id, - ); - const networkImg = getNetworkImg(market.morphoBlue.chain.id); - const networkName = getNetworkName(market.morphoBlue.chain.id); - const sign = tx.type === UserTxTypes.MarketSupply ? '+' : '-'; - const lltv = Number(formatUnits(BigInt(market.lltv), 18)) * 100; + const loanToken = findToken(market.loanAsset.address, market.morphoBlue.chain.id); + const collateralToken = findToken( + market.collateralAsset.address, + market.morphoBlue.chain.id, + ); + const networkImg = getNetworkImg(market.morphoBlue.chain.id); + const networkName = getNetworkName(market.morphoBlue.chain.id); + const sign = tx.type === UserTxTypes.MarketSupply ? '+' : '-'; + const lltv = Number(formatUnits(BigInt(market.lltv), 18)) * 100; - const isAgent = rebalancerInfo?.transactions.some( - (agentTx) => agentTx.transactionHash === tx.hash, - ); + const isAgent = rebalancerInfo?.transactions.some( + (agentTx) => agentTx.transactionHash === tx.hash, + ); - return ( - - {/* Network & Asset */} - -
-
- {loanToken?.img && ( - - )} - {market.loanAsset.symbol} + return ( + + {/* Network & Asset */} + +
+
+ {loanToken?.img && ( + + )} + {market.loanAsset.symbol} +
+
+ {networkImg && ( + + )} + + {networkName} + +
-
- {networkImg && ( - - )} - - {networkName} - + + + {/* Market Details */} + +
+ + {market.uniqueKey.slice(2, 8)} + +
+ {collateralToken?.img && ( + + )} + + {market.collateralAsset.symbol} + +
+ + {formatReadable(lltv)}% +
-
-
+ - {/* Market Details */} - -
- - {market.uniqueKey.slice(2, 8)} - -
- {collateralToken?.img && ( - - )} - - {market.collateralAsset.symbol} + {/* Action & Amount */} + +
+ {actionTypeToText(tx.type)} + + {sign} + {formatReadable( + Number(formatUnits(BigInt(tx.data.assets), market.loanAsset.decimals)), + )}{' '} + {market.loanAsset.symbol} + {isAgent && ( + + + + )}
- - {formatReadable(lltv)}% - -
- - - {/* Action & Amount */} - -
- {actionTypeToText(tx.type)} - - {sign} - {formatReadable( - Number(formatUnits(BigInt(tx.data.assets), market.loanAsset.decimals)), - )}{' '} - {market.loanAsset.symbol} - - {isAgent && ( - - - - )} -
-
+ - {/* Time */} - -
- {moment.unix(tx.timestamp).fromNow()} -
-
+ {/* Time */} + +
+ {moment.unix(tx.timestamp).fromNow()} +
+
- {/* Transaction */} - -
- - {tx.hash.slice(0, 6)}...{tx.hash.slice(-4)} - - -
-
- - ); - })} - -
+ {/* Transaction */} + +
+ + {tx.hash.slice(0, 6)}...{tx.hash.slice(-4)} + + +
+
+ + ); + })} + + + )}
); } diff --git a/app/positions/components/agent/Main.tsx b/app/positions/components/agent/Main.tsx index fcc535dc..09bd033c 100644 --- a/app/positions/components/agent/Main.tsx +++ b/app/positions/components/agent/Main.tsx @@ -32,7 +32,7 @@ export function Main({ account, onNext, userRebalancerInfo }: MainProps) { if (!account) return; // Get the most recent bot transaction hash - const lastBotTxHash = userRebalancerInfo.transactions.at(0)?.transactionHash; + const lastBotTxHash = userRebalancerInfo.transactions.at(-1)?.transactionHash; if (lastBotTxHash) { const result = await fetchTransactions({ diff --git a/src/hooks/useUserTransactions.ts b/src/hooks/useUserTransactions.ts index af6114ee..e38003c9 100644 --- a/src/hooks/useUserTransactions.ts +++ b/src/hooks/useUserTransactions.ts @@ -13,6 +13,7 @@ export type TransactionFilters = { skip?: number; first?: number; hash?: string; + assetIds?: string[]; }; export type TransactionResponse = { @@ -49,6 +50,7 @@ const useUserTransactions = () => { timestamp_gte: filters.timestampGte ?? null, timestamp_lte: filters.timestampLte ?? null, hash: filters.hash ?? null, + assetId_in: filters.assetIds ?? null, }, first: filters.first ?? 1000, skip: filters.skip ?? 0, From be2a9138d4bab1838de3211343359a2cd63a9724 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Wed, 1 Jan 2025 22:50:53 +0800 Subject: [PATCH 7/7] chore: fix build --- src/hooks/useUserPositions.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts index 2e7849ef..e6111e0f 100644 --- a/src/hooks/useUserPositions.ts +++ b/src/hooks/useUserPositions.ts @@ -14,7 +14,7 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { const [positionsError, setPositionsError] = useState(null); const fetchData = useCallback( - async (isRefetch = false) => { + async (isRefetch = false, onSuccess?: () => void) => { if (!user) { console.error('Missing user address'); setLoading(false); @@ -93,6 +93,7 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { ); setData(enhancedPositions); + onSuccess?.(); } catch (err) { console.error('Error fetching positions:', err); setPositionsError(err); @@ -113,7 +114,7 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { loading, isRefetching, positionsError, - refetch: () => void fetchData(true), + refetch: (onSuccess?: () => void) => void fetchData(true, onSuccess), }; };