diff --git a/app/history/components/HistoryContent.tsx b/app/history/components/HistoryContent.tsx index 2fbdc066..da2c499f 100644 --- a/app/history/components/HistoryContent.tsx +++ b/app/history/components/HistoryContent.tsx @@ -1,15 +1,12 @@ 'use client'; -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 { useUserRebalancerInfo } from '@/hooks/useUserRebalancerInfo'; import { HistoryTable } from './HistoryTable'; export default function HistoryContent({ account }: { account: string }) { - const { loading, history } = useUserPositions(account); + const { data: positions } = useUserPositions(account, true); const { rebalancerInfo } = useUserRebalancerInfo(account); @@ -19,24 +16,8 @@ export default function HistoryContent({ account }: { account: string }) {

Transaction History

- {loading ? ( - - ) : history.length === 0 ? ( -
- No transaction history available. -
- ) : ( -
- -
- )} - -
- - - +
+
diff --git a/app/history/components/HistoryTable.tsx b/app/history/components/HistoryTable.tsx index 115dbb0d..1453477c 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'; @@ -7,17 +7,26 @@ 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 } from '@/utils/types'; +import { + UserTxTypes, + UserRebalancerInfo, + Market, + MarketPosition, + UserTransaction, +} from '@/utils/types'; type HistoryTableProps = { - history: UserTransaction[]; + account: string | undefined; + positions: MarketPosition[]; rebalancerInfo?: UserRebalancerInfo; }; @@ -27,33 +36,73 @@ type AssetKey = { img?: string; }; -export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { - const [page, setPage] = useState(1); - const rowsPerPage = 6; +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 key = `${tx.data.market.loanAsset.symbol}-${tx.data.market.morphoBlue.chain.id}`; + 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}`; if (!assetMap.has(key)) { - const token = findToken( - tx.data.market.loanAsset.address, - tx.data.market.morphoBlue.chain.id, - ); + const token = findToken(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]); + }, [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) => { @@ -68,11 +117,9 @@ 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); } }; @@ -80,30 +127,7 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { asset.symbol.toLowerCase().includes(query.toLowerCase()), ); - // Filter and sort transactions - const items = useMemo(() => { - const filtered = history.filter((tx) => { - if (!selectedAsset) return true; - return ( - tx.data.market.loanAsset.symbol === selectedAsset.symbol && - tx.data.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]); - - const pages = Math.ceil( - history.filter((tx) => { - if (!selectedAsset) return true; - return ( - tx.data.market.loanAsset.symbol === selectedAsset.symbol && - tx.data.market.morphoBlue.chain.id === selectedAsset.chainId - ); - }).length / rowsPerPage, - ); + const toggleDropdown = () => setIsOpen(!isOpen); return (
@@ -178,7 +202,7 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { setSelectedAsset(asset); setIsOpen(false); setQuery(''); - setPage(1); + setCurrentPage(1); }} role="option" aria-selected={ @@ -192,7 +216,7 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { setSelectedAsset(asset); setIsOpen(false); setQuery(''); - setPage(1); + setCurrentPage(1); } }} > @@ -230,7 +254,7 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { setSelectedAsset(null); setQuery(''); setIsOpen(false); - setPage(1); + setCurrentPage(1); }} type="button" > @@ -243,164 +267,170 @@ export function HistoryTable({ history, rebalancerInfo }: HistoryTableProps) { )}
- - setPage(_page)} - /> - - } - > - - Asset & Network - Market Details - Action & Amount - Time - Transaction - - - {items.map((tx, index) => { - const loanToken = findToken( - tx.data.market.loanAsset.address, - tx.data.market.morphoBlue.chain.id, - ); - const collateralToken = findToken( - tx.data.market.collateralAsset.address, - tx.data.market.morphoBlue.chain.id, - ); - const networkImg = getNetworkImg(tx.data.market.morphoBlue.chain.id); - const networkName = getNetworkName(tx.data.market.morphoBlue.chain.id); - const sign = tx.type === UserTxTypes.MarketSupply ? '+' : '-'; - const lltv = Number(formatUnits(BigInt(tx.data.market.lltv), 18)) * 100; + {!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 isAgent = rebalancerInfo?.transactions.some( - (agentTx) => agentTx.transactionHash === tx.hash, - ); + 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; - return ( - - {/* Network & Asset */} - -
-
- {loanToken?.img && ( - - )} - {tx.data.market.loanAsset.symbol} + const isAgent = rebalancerInfo?.transactions.some( + (agentTx) => agentTx.transactionHash === tx.hash, + ); + + 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 */} - -
- - {tx.data.market.uniqueKey.slice(2, 8)} - -
- {collateralToken?.img && ( - - )} - - {tx.data.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), tx.data.market.loanAsset.decimals), - ), - )}{' '} - {tx.data.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 260511d3..09bd033c 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,6 +8,7 @@ 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'; @@ -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(-1)?.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/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 875ece71..75d563ad 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -165,58 +165,7 @@ export const userPositionsQuery = ` ...MarketFields } } - transactions { - hash - timestamp - type - data { - __typename - ... on MarketTransferTransactionData { - assetsUsd - 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} @@ -294,3 +243,30 @@ export const userRebalancerInfoQuery = ` } } `; + +export const userTransactionsQuery = ` + query getUserTransactions($where: TransactionFilters, $first: Int, $skip: Int) { + transactions(where: $where, first: $first, skip: $skip) { + 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 52bfc0e9..039a04dd 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 useUserTransactions from './useUserTransactions'; export type PositionReport = { market: Market; @@ -32,13 +33,13 @@ export type ReportSummary = { export const usePositionReport = ( positions: MarketPosition[], - history: UserTransaction[], account: Address, selectedAsset: { address: string; chainId: number } | null, startDate?: Date, endDate?: Date, ) => { const { fetchPositionSnapshot } = usePositionSnapshot(); + const { fetchTransactions } = useUserTransactions(); const generateReport = async (): Promise => { if (!startDate || !endDate || !selectedAsset) return null; @@ -58,8 +59,8 @@ export const usePositionReport = ( endDate.getTime() / 1000, ); - let startTimestamp = Math.floor(startDate.getTime() / 1000); - let endTimestamp = Math.floor(endDate.getTime() / 1000); + const startTimestamp = Math.floor(startDate.getTime() / 1000); + const endTimestamp = Math.floor(endDate.getTime() / 1000); const period = endTimestamp - startTimestamp; const relevantPositions = positions.filter( @@ -68,10 +69,39 @@ export const usePositionReport = ( position.market.morphoBlue.chain.id === selectedAsset.chainId, ); - const relevantTxs = history.filter( - (tx) => - tx.data?.market?.loanAsset.address.toLowerCase() === selectedAsset.address.toLowerCase(), - ); + // 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 marketReports = ( await Promise.all( @@ -94,7 +124,9 @@ export const usePositionReport = ( } const marketTransactions = filterTransactionsInPeriod( - history.filter((tx) => tx.data?.market?.uniqueKey === position.market.uniqueKey), + allTransactions.filter( + (tx) => tx.data?.market?.uniqueKey === position.market.uniqueKey, + ), startTimestamp, endTimestamp, ); @@ -148,7 +180,7 @@ export const usePositionReport = ( const groupedEarnings = calculateEarningsFromSnapshot( endBalance, startBalance, - relevantTxs, + allTransactions, startTimestamp, endTimestamp, ); diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts index 65b6ceea..e6111e0f 100644 --- a/src/hooks/useUserPositions.ts +++ b/src/hooks/useUserPositions.ts @@ -3,7 +3,7 @@ import { useState, useEffect, useCallback } from 'react'; 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,11 +11,10 @@ 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 [error, setError] = useState(null); + 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); @@ -30,6 +29,8 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { setLoading(true); } + setPositionsError(null); + // Fetch position data from both networks const [responseMainnet, responseBase] = await Promise.all([ fetch(URLS.MORPHO_BLUE_API, { @@ -64,24 +65,16 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { const result2 = await responseBase.json(); const marketPositions: MarketPosition[] = []; - const transactions: UserTransaction[] = []; - // Collect positions and transactions + // Collect positions 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); } } - // 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 @@ -99,11 +92,11 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => { }), ); - setHistory(transactions); setData(enhancedPositions); - } catch (_error) { - console.error('Error fetching positions:', _error); - setError(_error); + onSuccess?.(); + } catch (err) { + console.error('Error fetching positions:', err); + setPositionsError(err); } finally { setLoading(false); setIsRefetching(false); @@ -113,17 +106,16 @@ 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, + loading, + isRefetching, + positionsError, + refetch: (onSuccess?: () => void) => void fetchData(true, onSuccess), + }; }; export default useUserPositions; diff --git a/src/hooks/useUserPositionsSummaryData.ts b/src/hooks/useUserPositionsSummaryData.ts index dc12fa0b..d28a991e 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,14 @@ 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, ); @@ -189,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 new file mode 100644 index 00000000..e38003c9 --- /dev/null +++ b/src/hooks/useUserTransactions.ts @@ -0,0 +1,93 @@ +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'; + +export type TransactionFilters = { + userAddress: string[]; + marketUniqueKeys?: string[]; + chainIds?: number[]; + timestampGte?: number; + timestampLte?: number; + skip?: number; + first?: number; + hash?: string; + assetIds?: string[]; +}; + +export type TransactionResponse = { + items: UserTransaction[]; + pageInfo: { + count: number; + countTotal: number; + }; + error: string | null; +}; + +const useUserTransactions = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchTransactions = useCallback( + async (filters: TransactionFilters): Promise => { + 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, + hash: filters.hash ?? null, + assetId_in: filters.assetIds ?? null, + }, + first: filters.first ?? 1000, + skip: filters.skip ?? 0, + }, + }), + }); + + 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; + } catch (err) { + console.error('Error fetching transactions:', err); + setError(err); + return { + items: [], + pageInfo: { count: 0, countTotal: 0 }, + error: err instanceof Error ? err.message : 'Unknown error occurred', + }; + } finally { + setLoading(false); + } + }, + [], + ); + + return { + loading, + error, + fetchTransactions, + }; +}; + +export default useUserTransactions; 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; - }; }; }; };