diff --git a/app/markets/components/MarketRowDetail.tsx b/app/markets/components/MarketRowDetail.tsx index 085dce62..5ca845a2 100644 --- a/app/markets/components/MarketRowDetail.tsx +++ b/app/markets/components/MarketRowDetail.tsx @@ -2,9 +2,9 @@ import { ExternalLinkIcon } from '@radix-ui/react-icons'; import { zeroAddress } from 'viem'; import { OracleFeedInfo } from '@/components/FeedInfo/OracleFeedInfo'; import { Info } from '@/components/Info/info'; -import { Market } from '@/hooks/useMarkets'; import { formatReadable } from '@/utils/balance'; import { getExplorerURL } from '@/utils/external'; +import { Market } from '@/utils/types'; export function ExpandedMarketDetail({ market }: { market: Market }) { console.log('market.oracleFeed', market.oracleFeed); diff --git a/app/markets/components/MarketTableBody.tsx b/app/markets/components/MarketTableBody.tsx index 29e2e029..870e4975 100644 --- a/app/markets/components/MarketTableBody.tsx +++ b/app/markets/components/MarketTableBody.tsx @@ -4,11 +4,11 @@ import { ExternalLinkIcon } from '@radix-ui/react-icons'; import Image from 'next/image'; import { FaShieldAlt } from 'react-icons/fa'; import { GoStarFill, GoStar } from 'react-icons/go'; -import { Market } from '@/hooks/useMarkets'; import { formatReadable } from '@/utils/balance'; import { getMarketURL } from '@/utils/external'; import { getNetworkImg } from '@/utils/networks'; import { findToken } from '@/utils/tokens'; +import { Market } from '@/utils/types'; import { ExpandedMarketDetail } from './MarketRowDetail'; import { TDAsset, TDTotalSupplyOrBorrow } from './MarketTableUtils'; import { MarketAssetIndicator, MarketOracleIndicator, MarketDebtIndicator } from './RiskIndicator'; diff --git a/app/markets/components/RiskIndicator.tsx b/app/markets/components/RiskIndicator.tsx index f454d8f8..50714821 100644 --- a/app/markets/components/RiskIndicator.tsx +++ b/app/markets/components/RiskIndicator.tsx @@ -1,5 +1,5 @@ import { Tooltip } from '@nextui-org/tooltip'; -import { Market } from '@/hooks/useMarkets'; +import { Market } from '@/utils/types'; import { WarningCategory } from '@/utils/types'; type RiskFlagProps = { diff --git a/app/markets/components/markets.tsx b/app/markets/components/markets.tsx index 0e6895d5..8a371b2a 100644 --- a/app/markets/components/markets.tsx +++ b/app/markets/components/markets.tsx @@ -3,11 +3,11 @@ import { useCallback, useEffect, useState } from 'react'; import storage from 'local-storage-fallback'; import Header from '@/components/layout/header/Header'; import LoadingScreen from '@/components/Status/LoadingScreen'; -import useMarkets, { Market } from '@/hooks/useMarkets'; - +import useMarkets from '@/hooks/useMarkets'; import { SupportedNetworks } from '@/utils/networks'; import * as keys from '@/utils/storageKeys'; import { ERC20Token, getUniqueTokens } from '@/utils/tokens'; +import { Market } from '@/utils/types'; import AssetFilter from './AssetFilter'; import CheckFilter from './CheckFilter'; diff --git a/app/markets/components/marketsTable.tsx b/app/markets/components/marketsTable.tsx index d5520b05..7c622cd4 100644 --- a/app/markets/components/marketsTable.tsx +++ b/app/markets/components/marketsTable.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { Tooltip } from '@nextui-org/tooltip'; -import { Market } from '@/hooks/useMarkets'; import { usePagination } from '@/hooks/usePagination'; +import { Market } from '@/utils/types'; import { SortColumn } from './constants'; import { MarketTableBody } from './MarketTableBody'; import { HTSortable } from './MarketTableUtils'; diff --git a/app/markets/components/supplyModal.tsx b/app/markets/components/supplyModal.tsx index 791a066a..c8680adf 100644 --- a/app/markets/components/supplyModal.tsx +++ b/app/markets/components/supplyModal.tsx @@ -8,13 +8,13 @@ import { useAccount, useBalance, useSwitchChain } from 'wagmi'; import morphoBundlerAbi from '@/abis/bundlerV2'; import Input from '@/components/Input/Input'; import AccountConnect from '@/components/layout/header/AccountConnect'; -import { Market } from '@/hooks/useMarkets'; import { usePermit2 } from '@/hooks/usePermit2'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { formatBalance } from '@/utils/balance'; import { getExplorerURL } from '@/utils/external'; import { getBundlerV2, getIRMTitle } from '@/utils/morpho'; import { findToken } from '@/utils/tokens'; +import { Market } from '@/utils/types'; import { SupplyProcessModal } from './SupplyProcessModal'; type SupplyModalProps = { diff --git a/app/markets/components/utils.ts b/app/markets/components/utils.ts index a07739fb..84b7cadc 100644 --- a/app/markets/components/utils.ts +++ b/app/markets/components/utils.ts @@ -1,6 +1,6 @@ -import { Market } from '@/hooks/useMarkets'; import { SupportedNetworks } from '@/utils/networks'; import { isWhitelisted } from '@/utils/tokens'; +import { Market } from '@/utils/types'; import { SortColumn } from './constants'; export const sortProperties = { diff --git a/app/positions/components/FromAndToMarkets.tsx b/app/positions/components/FromAndToMarkets.tsx index 242e82e8..81dbe564 100644 --- a/app/positions/components/FromAndToMarkets.tsx +++ b/app/positions/components/FromAndToMarkets.tsx @@ -3,10 +3,10 @@ import { Input } from '@nextui-org/react'; import { Pagination } from '@nextui-org/react'; import Image from 'next/image'; import { formatUnits } from 'viem'; -import { Market } from '@/hooks/useMarkets'; import { formatReadable } from '@/utils/balance'; import { getAssetURL } from '@/utils/external'; import { findToken } from '@/utils/tokens'; +import { Market } from '@/utils/types'; import { MarketPosition } from '@/utils/types'; import { MarketAssetIndicator, diff --git a/app/positions/components/MarketBadge.tsx b/app/positions/components/MarketBadge.tsx index 5ae05b99..4096de1d 100644 --- a/app/positions/components/MarketBadge.tsx +++ b/app/positions/components/MarketBadge.tsx @@ -1,12 +1,11 @@ import React from 'react'; import { formatUnits } from 'viem'; - type MarketBadgeProps = { market: | { uniqueKey: string; lltv: string; collateralAsset: { symbol: string } } | null | undefined; -} +}; export function MarketBadge({ market }: MarketBadgeProps) { if (!market) diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx index eb214e13..e0f267e5 100644 --- a/app/positions/components/PositionsContent.tsx +++ b/app/positions/components/PositionsContent.tsx @@ -19,7 +19,7 @@ export default function Positions() { const { account } = useParams<{ account: string }>(); - const { loading, data: marketPositions } = useUserPositions(account); + const { loading, isRefetching, data: marketPositions, refetch } = useUserPositions(account); const hasSuppliedMarkets = marketPositions.length > 0; @@ -28,7 +28,7 @@ export default function Positions() {
-

Your Supplies

+

Your Supplies

)} diff --git a/app/positions/components/PositionsSummaryTable.tsx b/app/positions/components/PositionsSummaryTable.tsx index f580d1b5..1b2c73db 100644 --- a/app/positions/components/PositionsSummaryTable.tsx +++ b/app/positions/components/PositionsSummaryTable.tsx @@ -1,6 +1,8 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useEffect } from 'react'; +import { Spinner } from '@nextui-org/react'; import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; import Image from 'next/image'; +import { GrRefresh } from 'react-icons/gr'; import { toast } from 'react-toastify'; import { useAccount } from 'wagmi'; import { formatReadable, formatBalance } from '@/utils/balance'; @@ -15,12 +17,16 @@ type PositionTableProps = { marketPositions: MarketPosition[]; setShowModal: (show: boolean) => void; setSelectedPosition: (position: MarketPosition) => void; + refetch: (onSuccess?: () => void) => void; + isRefetching: boolean; }; export function PositionsSummaryTable({ marketPositions, setShowModal, setSelectedPosition, + refetch, + isRefetching, }: PositionTableProps) { const { address: account } = useAccount(); @@ -119,6 +125,21 @@ export function PositionsSummaryTable({ }); }, [groupedPositions]); + // Update selectedGroupedPosition when groupedPositions change, don't depend on selectedGroupedPosition + useEffect(() => { + if (selectedGroupedPosition) { + const updatedPosition = processedPositions.find( + (position) => + position.loanAssetAddress === selectedGroupedPosition.loanAssetAddress && + position.chainId === selectedGroupedPosition.chainId, + ); + if (updatedPosition) { + setSelectedGroupedPosition(updatedPosition); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [processedPositions]); + const toggleRow = (rowKey: string) => { setExpandedRows((prev) => { const newSet = new Set(prev); @@ -131,8 +152,29 @@ export function PositionsSummaryTable({ }); }; + const handleManualRefresh = () => { + refetch(() => { + toast.info('Data refreshed', { icon: 🚀 }); + }); + }; + return (
+
+
+

Position Summary

+ {isRefetching && } +
+ +
@@ -264,6 +306,8 @@ export function PositionsSummaryTable({ groupedPosition={selectedGroupedPosition} onClose={() => setShowRebalanceModal(false)} isOpen={showRebalanceModal} + refetch={refetch} + isRefetching={isRefetching} /> )} diff --git a/app/positions/components/RebalanceActionInput.tsx b/app/positions/components/RebalanceActionInput.tsx index d7cfe592..37b634bd 100644 --- a/app/positions/components/RebalanceActionInput.tsx +++ b/app/positions/components/RebalanceActionInput.tsx @@ -2,9 +2,8 @@ import React from 'react'; import { Button } from '@nextui-org/react'; import { ArrowRightIcon } from '@radix-ui/react-icons'; import Image from 'next/image'; -import { Market } from '@/hooks/useMarkets'; import { ERC20Token } from '@/utils/tokens'; -import { GroupedPosition } from '@/utils/types'; +import { GroupedPosition, Market } from '@/utils/types'; import { MarketBadge } from './MarketBadge'; type RebalanceActionInputProps = { @@ -16,7 +15,7 @@ type RebalanceActionInputProps = { eligibleMarkets: Market[]; token: ERC20Token | undefined; onAddAction: () => void; -} +}; export function RebalanceActionInput({ amount, diff --git a/app/positions/components/RebalanceCart.tsx b/app/positions/components/RebalanceCart.tsx index edef195d..81e00808 100644 --- a/app/positions/components/RebalanceCart.tsx +++ b/app/positions/components/RebalanceCart.tsx @@ -9,7 +9,7 @@ import { Button, } from '@nextui-org/react'; import { formatUnits } from 'viem'; -import { Market } from '@/hooks/useMarkets'; +import { Market } from '@/utils/types'; import { GroupedPosition, RebalanceAction } from '@/utils/types'; import { MarketBadge } from './MarketBadge'; @@ -18,7 +18,7 @@ type RebalanceCartProps = { groupedPosition: GroupedPosition; eligibleMarkets: Market[]; removeRebalanceAction: (index: number) => void; -} +}; export function RebalanceCart({ rebalanceActions, diff --git a/app/positions/components/RebalanceModal.tsx b/app/positions/components/RebalanceModal.tsx index c5ca524c..e849a7ac 100644 --- a/app/positions/components/RebalanceModal.tsx +++ b/app/positions/components/RebalanceModal.tsx @@ -6,13 +6,16 @@ import { ModalBody, ModalFooter, Button, + Spinner, } from '@nextui-org/react'; +import { GrRefresh } from 'react-icons/gr'; import { toast } from 'react-toastify'; import { parseUnits } from 'viem'; -import useMarkets, { Market } from '@/hooks/useMarkets'; +import useMarkets from '@/hooks/useMarkets'; import { usePagination } from '@/hooks/usePagination'; import { useRebalance } from '@/hooks/useRebalance'; import { findToken } from '@/utils/tokens'; +import { Market } from '@/utils/types'; import { GroupedPosition, RebalanceAction } from '@/utils/types'; import { FromAndToMarkets } from './FromAndToMarkets'; import { RebalanceActionInput } from './RebalanceActionInput'; @@ -23,11 +26,19 @@ type RebalanceModalProps = { groupedPosition: GroupedPosition; isOpen: boolean; onClose: () => void; + refetch: (onSuccess?: () => void) => void; + isRefetching: boolean; }; export const PER_PAGE = 5; -export function RebalanceModal({ groupedPosition, isOpen, onClose }: RebalanceModalProps) { +export function RebalanceModal({ + groupedPosition, + isOpen, + onClose, + refetch, + isRefetching, +}: RebalanceModalProps) { const [fromMarketFilter, setFromMarketFilter] = useState(''); const [toMarketFilter, setToMarketFilter] = useState(''); const [selectedFromMarketUniqueKey, setSelectedFromMarketUniqueKey] = useState(''); @@ -43,7 +54,7 @@ export function RebalanceModal({ groupedPosition, isOpen, onClose }: RebalanceMo executeRebalance, isConfirming, currentStep, - } = useRebalance(groupedPosition); + } = useRebalance(groupedPosition, refetch); const token = findToken(groupedPosition.loanAssetAddress, groupedPosition.chainId); const fromPagination = usePagination(); @@ -160,6 +171,12 @@ export function RebalanceModal({ groupedPosition, isOpen, onClose }: RebalanceMo } }, [executeRebalance]); + const handleManualRefresh = () => { + refetch(() => { + toast.info('Data refreshed', { icon: 🚀 }); + }); + }; + return ( <> - - Rebalance {groupedPosition.loanAsset ?? 'Unknown'} Position + +
+ Rebalance {groupedPosition.loanAsset ?? 'Unknown'} Position + {isRefetching && } +
+
diff --git a/app/positions/components/withdrawModal.tsx b/app/positions/components/withdrawModal.tsx index d1c606ea..99f67d74 100644 --- a/app/positions/components/withdrawModal.tsx +++ b/app/positions/components/withdrawModal.tsx @@ -18,9 +18,10 @@ import { MarketPosition } from '@/utils/types'; type ModalProps = { position: MarketPosition; onClose: () => void; + refetch: () => void; }; -export function WithdrawModal({ position, onClose }: ModalProps): JSX.Element { +export function WithdrawModal({ position, onClose, refetch }: ModalProps): JSX.Element { // Add state for the supply amount const [inputError, setInputError] = useState(null); const [withdrawAmount, setWithdrawAmount] = useState(BigInt(0)); @@ -53,6 +54,10 @@ export function WithdrawModal({ position, onClose }: ModalProps): JSX.Element { 2, 8, )}`, + onSuccess: () => { + refetch(); + onClose(); + }, }); const withdraw = useCallback(async () => { diff --git a/app/rewards/components/MarketProgram.tsx b/app/rewards/components/MarketProgram.tsx index 83b06625..487beb61 100644 --- a/app/rewards/components/MarketProgram.tsx +++ b/app/rewards/components/MarketProgram.tsx @@ -6,13 +6,13 @@ import Image from 'next/image'; import { toast } from 'react-toastify'; import { Address } from 'viem'; import { useAccount, useSwitchChain } from 'wagmi'; -import { Market } from '@/hooks/useMarkets'; import { DistributionResponseType } from '@/hooks/useRewards'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { formatReadable, formatBalance } from '@/utils/balance'; import { getMarketURL } from '@/utils/external'; import { getNetworkImg } from '@/utils/networks'; import { findToken } from '@/utils/tokens'; +import { Market } from '@/utils/types'; import { MarketProgramType } from '@/utils/types'; type MarketProgramProps = { diff --git a/src/hooks/useLiquidations.ts b/src/hooks/useLiquidations.ts index 8b8b1e35..00920e23 100644 --- a/src/hooks/useLiquidations.ts +++ b/src/hooks/useLiquidations.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; const liquidationsQuery = ` query getLiquidations($first: Int, $skip: Int) { @@ -68,53 +68,63 @@ type QueryResult = { const useLiquidations = () => { const [loading, setLoading] = useState(true); + const [isRefetching, setIsRefetching] = useState(false); const [liquidatedMarketIds, setLiquidatedMarketIds] = useState>(new Set()); const [error, setError] = useState(null); - useEffect(() => { - const fetchLiquidations = async () => { - try { + const fetchLiquidations = useCallback(async (isRefetch = false) => { + try { + if (isRefetch) { + setIsRefetching(true); + } else { setLoading(true); - const liquidatedIds = new Set(); - let skip = 0; - const pageSize = 1000; - let totalCount = 0; + } + const liquidatedIds = new Set(); + let skip = 0; + const pageSize = 1000; + let totalCount = 0; - do { - const response = await fetch('https://blue-api.morpho.org/graphql', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - query: liquidationsQuery, - variables: { first: pageSize, skip }, - }), - }); - const result = (await response.json()) as QueryResult; - const liquidations = result.data.transactions.items; - const pageInfo = result.data.transactions.pageInfo; + do { + const response = await fetch('https://blue-api.morpho.org/graphql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: liquidationsQuery, + variables: { first: pageSize, skip }, + }), + }); + const result = (await response.json()) as QueryResult; + const liquidations = result.data.transactions.items; + const pageInfo = result.data.transactions.pageInfo; - liquidations.forEach((tx) => { - if (tx.data && 'market' in tx.data) { - liquidatedIds.add(tx.data.market.id); - } - }); + liquidations.forEach((tx) => { + if (tx.data && 'market' in tx.data) { + liquidatedIds.add(tx.data.market.id); + } + }); - totalCount = pageInfo.countTotal; - skip += pageInfo.count; - } while (skip < totalCount); + totalCount = pageInfo.countTotal; + skip += pageInfo.count; + } while (skip < totalCount); - setLiquidatedMarketIds(liquidatedIds); - setLoading(false); - } catch (_error) { - setError(_error); - setLoading(false); - } - }; + setLiquidatedMarketIds(liquidatedIds); + } catch (_error) { + setError(_error); + } finally { + setLoading(false); + setIsRefetching(false); + } + }, []); + useEffect(() => { fetchLiquidations().catch(console.error); - }, []); + }, [fetchLiquidations]); + + const refetch = useCallback(() => { + fetchLiquidations(true).catch(console.error); + }, [fetchLiquidations]); - return { loading, liquidatedMarketIds, error }; + return { loading, isRefetching, liquidatedMarketIds, error, refetch }; }; export default useLiquidations; diff --git a/src/hooks/useMarkets.ts b/src/hooks/useMarkets.ts index 96a850b6..df5b67c6 100644 --- a/src/hooks/useMarkets.ts +++ b/src/hooks/useMarkets.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { getRewardPer1000USD } from '@/utils/morpho'; import { isSupportedChain } from '@/utils/networks'; import { MORPHOTokenAddress } from '@/utils/tokens'; -import { OracleFeedsInfo, MarketWarning, WarningWithDetail, TokenInfo } from '@/utils/types'; +import { Market } from '@/utils/types'; import { getMarketWarningsWithDetail } from '@/utils/warnings'; import useLiquidations from './useLiquidations'; @@ -24,70 +24,6 @@ export type Reward = { }[]; }; -export type Market = { - id: string; - lltv: string; - uniqueKey: string; - irmAddress: string; - oracleAddress: string; - collateralPrice: string; - morphoBlue: { - id: string; - address: string; - chain: { - id: number; - }; - }; - oracleInfo: { - type: string; - }; - oracleFeed?: OracleFeedsInfo; - loanAsset: TokenInfo; - collateralAsset: TokenInfo; - state: { - borrowAssets: string; - supplyAssets: string; - borrowAssetsUsd: string; - supplyAssetsUsd: string; - borrowShares: string; - supplyShares: string; - liquidityAssets: string; - liquidityAssetsUsd: number; - collateralAssets: string; - collateralAssetsUsd: number | null; - utilization: number; - supplyApy: number; - borrowApy: number; - fee: number; - timestamp: number; - rateAtUTarget: number; - rewards: { - yearlySupplyTokens: string; - asset: { - address: string; - priceUsd: string | null; - spotPriceEth: string | null; - }; - amountPerSuppliedToken: string; - amountPerBorrowedToken: string; - }[]; - }; - warnings: MarketWarning[]; - badDebt?: { - underlying: number; - usd: number; - }; - realizedBadDebt?: { - underlying: number; - usd: number; - }; - - // appended by us - rewardPer1000USD?: string; - warningsWithDetail: WarningWithDetail[]; - isProtectedByLiquidationBots: boolean; -}; - const marketsQuery = ` query getMarkets($first: Int, $where: MarketFilters) { markets(first: $first, where: $where) { @@ -202,18 +138,24 @@ const marketsQuery = ` const useMarkets = () => { const [loading, setLoading] = useState(true); + const [isRefetching, setIsRefetching] = useState(false); const [data, setData] = useState([]); const [error, setError] = useState(null); const { loading: liquidationsLoading, liquidatedMarketIds, error: liquidationsError, + refetch: refetchLiquidations, } = useLiquidations(); - useEffect(() => { - const fetchData = async () => { + const fetchData = useCallback( + async (isRefetch = false) => { try { - setLoading(true); + if (isRefetch) { + setIsRefetching(true); + } else { + setLoading(true); + } // Fetch markets const marketsResponse = await fetch('https://blue-api.morpho.org/graphql', { @@ -263,22 +205,34 @@ const useMarkets = () => { }); setData(final); - setLoading(false); } catch (_error) { setError(_error); + } finally { setLoading(false); + setIsRefetching(false); } - }; + }, + [liquidatedMarketIds], + ); + useEffect(() => { if (!liquidationsLoading) { fetchData().catch(console.error); } - }, [liquidationsLoading, liquidatedMarketIds]); + }, [liquidationsLoading, fetchData]); + + const refetch = useCallback( + (onSuccess?: () => void) => { + refetchLiquidations(); + fetchData(true).then(onSuccess).catch(console.error); + }, + [refetchLiquidations, fetchData], + ); const isLoading = loading || liquidationsLoading; const combinedError = error || liquidationsError; - return { loading: isLoading, data, error: combinedError }; + return { loading: isLoading, isRefetching, data, error: combinedError, refetch }; }; export default useMarkets; diff --git a/src/hooks/useRebalance.ts b/src/hooks/useRebalance.ts index 713e0223..472d8249 100644 --- a/src/hooks/useRebalance.ts +++ b/src/hooks/useRebalance.ts @@ -9,7 +9,7 @@ import { getBundlerV2, MORPHO } from '@/utils/morpho'; import { GroupedPosition, RebalanceAction } from '@/utils/types'; import { usePermit2 } from './usePermit2'; -export const useRebalance = (groupedPosition: GroupedPosition) => { +export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: () => void) => { const [rebalanceActions, setRebalanceActions] = useState([]); const [isConfirming, setIsConfirming] = useState(false); const [currentStep, setCurrentStep] = useState< @@ -65,6 +65,7 @@ export const useRebalance = (groupedPosition: GroupedPosition) => { successText: 'Positions rebalanced successfully', errorText: 'Failed to rebalance positions', chainId: groupedPosition.chainId, + onSuccess: onRebalance, }); const executeRebalance = useCallback(async () => { diff --git a/src/hooks/useTransactionWithToast.tsx b/src/hooks/useTransactionWithToast.tsx index e9e3fc7b..2b66fa03 100644 --- a/src/hooks/useTransactionWithToast.tsx +++ b/src/hooks/useTransactionWithToast.tsx @@ -14,6 +14,7 @@ type UseTransactionWithToastProps = { chainId?: number; pendingDescription?: string; successDescription?: string; + onSuccess?: () => void; }; export function useTransactionWithToast({ @@ -24,6 +25,7 @@ export function useTransactionWithToast({ chainId, pendingDescription, successDescription, + onSuccess, }: UseTransactionWithToastProps) { const { data: hash, @@ -72,6 +74,9 @@ export function useTransactionWithToast({ autoClose: 5000, onClick, }); + if (onSuccess) { + onSuccess(); + } } if (txError) { toast.update(toastId, { @@ -96,6 +101,7 @@ export function useTransactionWithToast({ toastId, onClick, renderToastContent, + onSuccess, ]); return { sendTransactionAsync, sendTransaction, isConfirming, isConfirmed }; diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts index b3164dd0..54e63742 100644 --- a/src/hooks/useUserPositions.ts +++ b/src/hooks/useUserPositions.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { SupportedNetworks } from '@/utils/networks'; import { MarketPosition, UserTransaction } from '@/utils/types'; @@ -123,14 +123,22 @@ const query = `query getUserMarketPositions( const useUserPositions = (user: string | undefined) => { const [loading, setLoading] = useState(true); + const [isRefetching, setIsRefetching] = useState(false); const [data, setData] = useState([]); const [history, setHistory] = useState([]); const [error, setError] = useState(null); - useEffect(() => { - const fetchData = async () => { + const fetchData = useCallback( + async (isRefetch = false) => { + if (!user) return; + try { - setLoading(true); + if (isRefetch) { + setIsRefetching(true); + } else { + setLoading(true); + } + const [responseMainnet, responseBase] = await Promise.all([ fetch('https://blue-api.morpho.org/graphql', { method: 'POST', @@ -187,19 +195,28 @@ const useUserPositions = (user: string | undefined) => { setHistory(transactions); setData(filtered); - setLoading(false); } catch (_error) { setError(_error); + } finally { setLoading(false); + setIsRefetching(false); } - }; - - if (!user) return; + }, + [user], + ); + useEffect(() => { fetchData().catch(console.error); - }, [user]); + }, [fetchData]); + + const refetch = useCallback( + (onSuccess?: () => void) => { + fetchData(true).then(onSuccess).catch(console.error); + }, + [fetchData], + ); - return { loading, data, history, error }; + return { loading, isRefetching, data, history, error, refetch }; }; export default useUserPositions; diff --git a/src/utils/types.ts b/src/utils/types.ts index 487a3384..86755df5 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -239,3 +239,68 @@ export type GroupedPosition = { percentage: number; }[]; }; + +// Add this type to the existing types in the file +export type Market = { + id: string; + lltv: string; + uniqueKey: string; + irmAddress: string; + oracleAddress: string; + collateralPrice: string; + morphoBlue: { + id: string; + address: string; + chain: { + id: number; + }; + }; + oracleInfo: { + type: string; + }; + oracleFeed?: OracleFeedsInfo; + loanAsset: TokenInfo; + collateralAsset: TokenInfo; + state: { + borrowAssets: string; + supplyAssets: string; + borrowAssetsUsd: string; + supplyAssetsUsd: string; + borrowShares: string; + supplyShares: string; + liquidityAssets: string; + liquidityAssetsUsd: number; + collateralAssets: string; + collateralAssetsUsd: number | null; + utilization: number; + supplyApy: number; + borrowApy: number; + fee: number; + timestamp: number; + rateAtUTarget: number; + rewards: { + yearlySupplyTokens: string; + asset: { + address: string; + priceUsd: string | null; + spotPriceEth: string | null; + }; + amountPerSuppliedToken: string; + amountPerBorrowedToken: string; + }[]; + }; + warnings: MarketWarning[]; + badDebt?: { + underlying: number; + usd: number; + }; + realizedBadDebt?: { + underlying: number; + usd: number; + }; + + // appended by us + rewardPer1000USD?: string; + warningsWithDetail: WarningWithDetail[]; + isProtectedByLiquidationBots: boolean; +}; diff --git a/src/utils/warnings.ts b/src/utils/warnings.ts index e4939dad..88765638 100644 --- a/src/utils/warnings.ts +++ b/src/utils/warnings.ts @@ -1,4 +1,4 @@ -import { Market } from '@/hooks/useMarkets'; +import { Market } from '@/utils/types'; import { WarningCategory, WarningWithDetail } from './types'; const morphoOfficialWarnings: WarningWithDetail[] = [