From c833854a7a20b68f0c7fbfba57654e121e7136cd Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Wed, 9 Oct 2024 17:46:37 +0800 Subject: [PATCH 1/6] chore: move Market to utils/type.ts --- app/markets/components/MarketTableBody.tsx | 2 +- app/markets/components/marketsTable.tsx | 2 +- app/markets/components/supplyModal.tsx | 2 +- app/positions/components/FromAndToMarkets.tsx | 4 +- app/positions/components/MarketBadge.tsx | 1 - .../components/RebalanceActionInput.tsx | 3 +- app/positions/components/RebalanceCart.tsx | 2 +- app/positions/components/RebalanceModal.tsx | 3 +- src/hooks/useMarkets.ts | 181 ++++++------------ src/hooks/useUserPositions.ts | 120 ++++++------ src/utils/types.ts | 65 +++++++ 11 files changed, 196 insertions(+), 189 deletions(-) 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/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/positions/components/FromAndToMarkets.tsx b/app/positions/components/FromAndToMarkets.tsx index 242e82e8..565db70f 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, @@ -325,4 +325,4 @@ export function FromAndToMarkets({ ); -} +} \ No newline at end of file diff --git a/app/positions/components/MarketBadge.tsx b/app/positions/components/MarketBadge.tsx index 5ae05b99..77ac9dc9 100644 --- a/app/positions/components/MarketBadge.tsx +++ b/app/positions/components/MarketBadge.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { formatUnits } from 'viem'; - type MarketBadgeProps = { market: | { uniqueKey: string; lltv: string; collateralAsset: { symbol: string } } diff --git a/app/positions/components/RebalanceActionInput.tsx b/app/positions/components/RebalanceActionInput.tsx index d7cfe592..3a9d1114 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 = { diff --git a/app/positions/components/RebalanceCart.tsx b/app/positions/components/RebalanceCart.tsx index edef195d..8cc3bb71 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'; diff --git a/app/positions/components/RebalanceModal.tsx b/app/positions/components/RebalanceModal.tsx index c5ca524c..05ed5583 100644 --- a/app/positions/components/RebalanceModal.tsx +++ b/app/positions/components/RebalanceModal.tsx @@ -9,10 +9,11 @@ import { } from '@nextui-org/react'; 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'; diff --git a/src/hooks/useMarkets.ts b/src/hooks/useMarkets.ts index 96a850b6..a2b682e9 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) { @@ -210,75 +146,78 @@ const useMarkets = () => { error: liquidationsError, } = useLiquidations(); - useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - - // Fetch markets - const marketsResponse = await fetch('https://blue-api.morpho.org/graphql', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - query: marketsQuery, - variables: { first: 1000, where: { whitelisted: true } }, - }), - }); - const marketsResult = await marketsResponse.json(); - const markets = marketsResult.data.markets.items as Market[]; - - const filtered = markets - .filter((market) => market.collateralAsset != undefined) - .filter( - (market) => market.warnings.find((w) => w.type === 'not_whitelisted') === undefined, - ) - .filter((market) => isSupportedChain(market.morphoBlue.chain.id)); - - const final = filtered.map((market) => { - const entry = market.state.rewards.find( - (reward) => reward.asset.address.toLowerCase() === MORPHOTokenAddress.toLowerCase(), - ); - - const warningsWithDetail = getMarketWarningsWithDetail(market); - const isProtectedByLiquidationBots = liquidatedMarketIds.has(market.id); - - if (!entry) { - return { - ...market, - rewardPer1000USD: undefined, - warningsWithDetail, - isProtectedByLiquidationBots, - }; - } - - const supplyAssetUSD = Number(market.state.supplyAssetsUsd); - const rewardPer1000USD = getRewardPer1000USD(entry.yearlySupplyTokens, supplyAssetUSD); - + const fetchData = useCallback(async () => { + try { + setLoading(true); + // Fetch markets + const marketsResponse = await fetch('https://blue-api.morpho.org/graphql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: marketsQuery, + variables: { first: 1000, where: { whitelisted: true } }, + }), + }); + const marketsResult = await marketsResponse.json(); + const markets = marketsResult.data.markets.items as Market[]; + + const filtered = markets + .filter((market) => market.collateralAsset != undefined) + .filter( + (market) => market.warnings.find((w) => w.type === 'not_whitelisted') === undefined, + ) + .filter((market) => isSupportedChain(market.morphoBlue.chain.id)); + + const final = filtered.map((market) => { + const entry = market.state.rewards.find( + (reward) => reward.asset.address.toLowerCase() === MORPHOTokenAddress.toLowerCase(), + ); + + const warningsWithDetail = getMarketWarningsWithDetail(market); + const isProtectedByLiquidationBots = liquidatedMarketIds.has(market.id); + + if (!entry) { return { ...market, - rewardPer1000USD, + rewardPer1000USD: undefined, warningsWithDetail, isProtectedByLiquidationBots, }; - }); + } - setData(final); - setLoading(false); - } catch (_error) { - setError(_error); - setLoading(false); - } - }; + const supplyAssetUSD = Number(market.state.supplyAssetsUsd); + const rewardPer1000USD = getRewardPer1000USD(entry.yearlySupplyTokens, supplyAssetUSD); + + return { + ...market, + rewardPer1000USD, + warningsWithDetail, + isProtectedByLiquidationBots, + }; + }); + + setData(final); + setLoading(false); + } catch (_error) { + setError(_error); + setLoading(false); + } + }, [liquidatedMarketIds]); + useEffect(() => { if (!liquidationsLoading) { fetchData().catch(console.error); } - }, [liquidationsLoading, liquidatedMarketIds]); + }, [liquidationsLoading, fetchData]); + + const refetch = useCallback(() => { + fetchData().catch(console.error); + }, [fetchData]); const isLoading = loading || liquidationsLoading; const combinedError = error || liquidationsError; - return { loading: isLoading, data, error: combinedError }; + return { loading: isLoading, data, error: combinedError, refetch }; }; export default useMarkets; diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts index b3164dd0..53ffb0cc 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'; @@ -127,79 +127,83 @@ const useUserPositions = (user: string | undefined) => { const [history, setHistory] = useState([]); const [error, setError] = useState(null); - useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - const [responseMainnet, responseBase] = await Promise.all([ - fetch('https://blue-api.morpho.org/graphql', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + const fetchData = useCallback(async () => { + if (!user) return; + + try { + setLoading(true); + const [responseMainnet, responseBase] = await Promise.all([ + fetch('https://blue-api.morpho.org/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query, + variables: { + address: user, + chainId: SupportedNetworks.Mainnet, }, - body: JSON.stringify({ - query, - variables: { - address: user, - chainId: SupportedNetworks.Mainnet, - }, - }), }), - fetch('https://blue-api.morpho.org/graphql', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + }), + fetch('https://blue-api.morpho.org/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query, + variables: { + address: user, + chainId: SupportedNetworks.Base, }, - body: JSON.stringify({ - query, - variables: { - address: user, - chainId: SupportedNetworks.Base, - }, - }), }), - ]); + }), + ]); - const result1 = await responseMainnet.json(); - const result2 = await responseBase.json(); + const result1 = await responseMainnet.json(); + const result2 = await responseBase.json(); - const marketPositions: MarketPosition[] = []; - const transactions: UserTransaction[] = []; + const marketPositions: MarketPosition[] = []; + const transactions: UserTransaction[] = []; - for (const result of [result1, result2]) { - // eslint-disable-next-line @typescript-eslint/prefer-optional-chain - if (result.data && result.data.userByAddress) { - marketPositions.push( - ...(result.data.userByAddress.marketPositions as MarketPosition[]), - ); + for (const result of [result1, result2]) { + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain + if (result.data && 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 as UserTransaction[])); - } + const parsableTxs = ( + result.data.userByAddress.transactions as UserTransaction[] + ).filter((t) => t.data?.market); + transactions.push(...(parsableTxs as UserTransaction[])); } + } - const filtered = marketPositions.filter( - (position: MarketPosition) => position.supplyShares.toString() !== '0', - ); + const filtered = marketPositions.filter( + (position: MarketPosition) => position.supplyShares.toString() !== '0', + ); - setHistory(transactions); + setHistory(transactions); - setData(filtered); - setLoading(false); - } catch (_error) { - setError(_error); - setLoading(false); - } - }; + setData(filtered); + setLoading(false); + } catch (_error) { + setError(_error); + setLoading(false); + } + }, [user]); - if (!user) return; + useEffect(() => { + fetchData().catch(console.error); + }, [fetchData]); + const refetch = useCallback(() => { fetchData().catch(console.error); - }, [user]); + }, [fetchData]); - return { loading, data, history, error }; + return { loading, 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; +}; From 53933602dac30c09c833e44a19d2e1f8333bed8a Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Wed, 9 Oct 2024 18:18:22 +0800 Subject: [PATCH 2/6] feat: refresh --- app/positions/components/PositionsContent.tsx | 9 +- .../components/PositionsSummaryTable.tsx | 47 ++++++++++- app/positions/components/RebalanceModal.tsx | 37 ++++++-- app/positions/components/withdrawModal.tsx | 7 +- src/hooks/useLiquidations.ts | 84 +++++++++++-------- src/hooks/useMarkets.ts | 21 +++-- src/hooks/useRebalance.ts | 3 +- src/hooks/useTransactionWithToast.tsx | 5 ++ src/hooks/useUserPositions.ts | 17 ++-- 9 files changed, 171 insertions(+), 59 deletions(-) diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx index eb214e13..1aca846b 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,9 @@ export default function Positions() {
-

Your Supplies

+

+ Your Supplies +

)} diff --git a/app/positions/components/PositionsSummaryTable.tsx b/app/positions/components/PositionsSummaryTable.tsx index f580d1b5..7e71c6d3 100644 --- a/app/positions/components/PositionsSummaryTable.tsx +++ b/app/positions/components/PositionsSummaryTable.tsx @@ -1,5 +1,6 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useEffect } from 'react'; import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; +import { Spinner } from '@nextui-org/react'; import Image from 'next/image'; import { toast } from 'react-toastify'; import { useAccount } from 'wagmi'; @@ -10,17 +11,22 @@ import { MarketPosition, GroupedPosition } from '@/utils/types'; import { getCollateralColor } from '../utils/colors'; import { RebalanceModal } from './RebalanceModal'; import { SuppliedMarketsDetail } from './SuppliedMarketsDetail'; +import { GrRefresh } from 'react-icons/gr'; type PositionTableProps = { marketPositions: MarketPosition[]; setShowModal: (show: boolean) => void; setSelectedPosition: (position: MarketPosition) => void; + refetch: () => void; + isRefetching: boolean; }; export function PositionsSummaryTable({ marketPositions, setShowModal, setSelectedPosition, + refetch, + isRefetching, }: PositionTableProps) { const { address: account } = useAccount(); @@ -31,6 +37,7 @@ export function PositionsSummaryTable({ ); const groupedPositions: GroupedPosition[] = useMemo(() => { + console.log('updating groupedPositions'); return marketPositions.reduce((acc: GroupedPosition[], position) => { const loanAssetAddress = position.market.loanAsset.address; const loanAssetDecimals = position.market.loanAsset.decimals; @@ -119,6 +126,20 @@ export function PositionsSummaryTable({ }); }, [groupedPositions]); + // Update selectedGroupedPosition when groupedPositions change + useEffect(() => { + if (selectedGroupedPosition) { + const updatedPosition = processedPositions.find( + (position) => + position.loanAssetAddress === selectedGroupedPosition.loanAssetAddress && + position.chainId === selectedGroupedPosition.chainId, + ); + if (updatedPosition) { + setSelectedGroupedPosition(updatedPosition); + } + } + }, [processedPositions, selectedGroupedPosition]); + const toggleRow = (rowKey: string) => { setExpandedRows((prev) => { const newSet = new Set(prev); @@ -131,8 +152,28 @@ export function PositionsSummaryTable({ }); }; + const handleManualRefresh = () => { + refetch(); + toast.info('Data refreshed', { icon: 🚀, delay: 1000 }); + }; + return (
+
+
+

Position Summary

+ {isRefetching && } +
+ +
@@ -264,8 +305,10 @@ export function PositionsSummaryTable({ groupedPosition={selectedGroupedPosition} onClose={() => setShowRebalanceModal(false)} isOpen={showRebalanceModal} + refetch={refetch} + isRefetching={isRefetching} /> )} ); -} +} \ No newline at end of file diff --git a/app/positions/components/RebalanceModal.tsx b/app/positions/components/RebalanceModal.tsx index 05ed5583..1dbf0501 100644 --- a/app/positions/components/RebalanceModal.tsx +++ b/app/positions/components/RebalanceModal.tsx @@ -6,7 +6,9 @@ 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 from '@/hooks/useMarkets'; @@ -24,11 +26,19 @@ type RebalanceModalProps = { groupedPosition: GroupedPosition; isOpen: boolean; onClose: () => void; + refetch: () => 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(''); @@ -44,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(); @@ -161,6 +171,11 @@ export function RebalanceModal({ groupedPosition, isOpen, onClose }: RebalanceMo } }, [executeRebalance]); + const handleManualRefresh = () => { + refetch(); + toast.info('Data refreshed', {icon: 🚀, delay: 1000}); + }; + return ( <> - - Rebalance {groupedPosition.loanAsset ?? 'Unknown'} Position + +
+ Rebalance {groupedPosition.loanAsset ?? 'Unknown'} Position + {isRefetching && } +
+
@@ -260,4 +287,4 @@ export function RebalanceModal({ groupedPosition, isOpen, onClose }: RebalanceMo )} ); -} +} \ No newline at end of file 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/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 a2b682e9..0ffc10c0 100644 --- a/src/hooks/useMarkets.ts +++ b/src/hooks/useMarkets.ts @@ -138,17 +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(); - const fetchData = useCallback(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', { method: 'POST', @@ -197,10 +204,11 @@ const useMarkets = () => { }); setData(final); - setLoading(false); } catch (_error) { setError(_error); + } finally { setLoading(false); + setIsRefetching(false); } }, [liquidatedMarketIds]); @@ -211,13 +219,14 @@ const useMarkets = () => { }, [liquidationsLoading, fetchData]); const refetch = useCallback(() => { - fetchData().catch(console.error); - }, [fetchData]); + refetchLiquidations(); + fetchData(true).catch(console.error); + }, [refetchLiquidations, fetchData]); const isLoading = loading || liquidationsLoading; const combinedError = error || liquidationsError; - return { loading: isLoading, data, error: combinedError, refetch }; + 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..8b23da19 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..e21a0da4 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, { diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts index 53ffb0cc..a61423f5 100644 --- a/src/hooks/useUserPositions.ts +++ b/src/hooks/useUserPositions.ts @@ -123,15 +123,21 @@ 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); - const fetchData = useCallback(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', @@ -188,10 +194,11 @@ const useUserPositions = (user: string | undefined) => { setHistory(transactions); setData(filtered); - setLoading(false); } catch (_error) { setError(_error); + } finally { setLoading(false); + setIsRefetching(false); } }, [user]); @@ -200,10 +207,10 @@ const useUserPositions = (user: string | undefined) => { }, [fetchData]); const refetch = useCallback(() => { - fetchData().catch(console.error); + fetchData(true).catch(console.error); }, [fetchData]); - return { loading, data, history, error, refetch }; + return { loading, isRefetching, data, history, error, refetch }; }; export default useUserPositions; From 33a99ac6b55a5309fa7d72a7cef177948c2bad87 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Wed, 9 Oct 2024 20:27:29 +0800 Subject: [PATCH 3/6] feat: fix build --- app/markets/components/MarketRowDetail.tsx | 2 +- app/markets/components/RiskIndicator.tsx | 2 +- app/markets/components/markets.tsx | 4 ++-- app/markets/components/utils.ts | 2 +- app/rewards/components/MarketProgram.tsx | 2 +- src/utils/warnings.ts | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/markets/components/MarketRowDetail.tsx b/app/markets/components/MarketRowDetail.tsx index 085dce62..1ae23d82 100644 --- a/app/markets/components/MarketRowDetail.tsx +++ b/app/markets/components/MarketRowDetail.tsx @@ -2,7 +2,7 @@ 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 { Market } from '@/utils/types'; import { formatReadable } from '@/utils/balance'; import { getExplorerURL } from '@/utils/external'; 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..e11d3c46 100644 --- a/app/markets/components/markets.tsx +++ b/app/markets/components/markets.tsx @@ -3,8 +3,8 @@ 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 { Market } from '@/utils/types'; import { SupportedNetworks } from '@/utils/networks'; import * as keys from '@/utils/storageKeys'; import { ERC20Token, getUniqueTokens } from '@/utils/tokens'; diff --git a/app/markets/components/utils.ts b/app/markets/components/utils.ts index a07739fb..36056aae 100644 --- a/app/markets/components/utils.ts +++ b/app/markets/components/utils.ts @@ -1,4 +1,4 @@ -import { Market } from '@/hooks/useMarkets'; +import { Market } from '@/utils/types'; import { SupportedNetworks } from '@/utils/networks'; import { isWhitelisted } from '@/utils/tokens'; import { SortColumn } from './constants'; diff --git a/app/rewards/components/MarketProgram.tsx b/app/rewards/components/MarketProgram.tsx index 83b06625..135d6f2c 100644 --- a/app/rewards/components/MarketProgram.tsx +++ b/app/rewards/components/MarketProgram.tsx @@ -6,7 +6,7 @@ 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 { Market } from '@/utils/types'; import { DistributionResponseType } from '@/hooks/useRewards'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { formatReadable, formatBalance } from '@/utils/balance'; 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[] = [ From ec6e9a04cc50f7647c519af8508dd3023386c880 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Thu, 10 Oct 2024 00:57:55 +0800 Subject: [PATCH 4/6] fix: build errors --- app/markets/components/MarketRowDetail.tsx | 2 +- app/markets/components/markets.tsx | 2 +- app/markets/components/utils.ts | 2 +- app/positions/components/PositionsSummaryTable.tsx | 4 ++-- app/rewards/components/MarketProgram.tsx | 2 +- src/hooks/useTransactionWithToast.tsx | 1 + 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/markets/components/MarketRowDetail.tsx b/app/markets/components/MarketRowDetail.tsx index 1ae23d82..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 '@/utils/types'; 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/markets.tsx b/app/markets/components/markets.tsx index e11d3c46..8a371b2a 100644 --- a/app/markets/components/markets.tsx +++ b/app/markets/components/markets.tsx @@ -4,10 +4,10 @@ import storage from 'local-storage-fallback'; import Header from '@/components/layout/header/Header'; import LoadingScreen from '@/components/Status/LoadingScreen'; import useMarkets from '@/hooks/useMarkets'; -import { Market } from '@/utils/types'; 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/utils.ts b/app/markets/components/utils.ts index 36056aae..84b7cadc 100644 --- a/app/markets/components/utils.ts +++ b/app/markets/components/utils.ts @@ -1,6 +1,6 @@ -import { Market } from '@/utils/types'; 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/PositionsSummaryTable.tsx b/app/positions/components/PositionsSummaryTable.tsx index 7e71c6d3..e9e4b1a4 100644 --- a/app/positions/components/PositionsSummaryTable.tsx +++ b/app/positions/components/PositionsSummaryTable.tsx @@ -1,7 +1,8 @@ import React, { useMemo, useState, useEffect } from 'react'; -import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; 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'; @@ -11,7 +12,6 @@ import { MarketPosition, GroupedPosition } from '@/utils/types'; import { getCollateralColor } from '../utils/colors'; import { RebalanceModal } from './RebalanceModal'; import { SuppliedMarketsDetail } from './SuppliedMarketsDetail'; -import { GrRefresh } from 'react-icons/gr'; type PositionTableProps = { marketPositions: MarketPosition[]; diff --git a/app/rewards/components/MarketProgram.tsx b/app/rewards/components/MarketProgram.tsx index 135d6f2c..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 '@/utils/types'; 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/useTransactionWithToast.tsx b/src/hooks/useTransactionWithToast.tsx index e21a0da4..25973895 100644 --- a/src/hooks/useTransactionWithToast.tsx +++ b/src/hooks/useTransactionWithToast.tsx @@ -101,6 +101,7 @@ export function useTransactionWithToast({ toastId, onClick, renderToastContent, + onSuccess ]); return { sendTransactionAsync, sendTransaction, isConfirming, isConfirmed }; From 421c2704294b5ce97b2ee1aa765cd2f952ea3945 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Thu, 10 Oct 2024 12:02:31 +0800 Subject: [PATCH 5/6] fix: render issue --- app/positions/components/PositionsSummaryTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/positions/components/PositionsSummaryTable.tsx b/app/positions/components/PositionsSummaryTable.tsx index e9e4b1a4..77ba09af 100644 --- a/app/positions/components/PositionsSummaryTable.tsx +++ b/app/positions/components/PositionsSummaryTable.tsx @@ -126,7 +126,7 @@ export function PositionsSummaryTable({ }); }, [groupedPositions]); - // Update selectedGroupedPosition when groupedPositions change + // Update selectedGroupedPosition when groupedPositions change, don't depend on selectedGroupedPosition useEffect(() => { if (selectedGroupedPosition) { const updatedPosition = processedPositions.find( @@ -138,7 +138,7 @@ export function PositionsSummaryTable({ setSelectedGroupedPosition(updatedPosition); } } - }, [processedPositions, selectedGroupedPosition]); + }, [processedPositions]); const toggleRow = (rowKey: string) => { setExpandedRows((prev) => { From 92484b4a0b586549c0b1946da161783001e58001 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Thu, 10 Oct 2024 12:15:22 +0800 Subject: [PATCH 6/6] chore: fix comments --- app/positions/components/FromAndToMarkets.tsx | 2 +- app/positions/components/MarketBadge.tsx | 2 +- app/positions/components/PositionsContent.tsx | 4 +- .../components/PositionsSummaryTable.tsx | 13 +- .../components/RebalanceActionInput.tsx | 2 +- app/positions/components/RebalanceCart.tsx | 2 +- app/positions/components/RebalanceModal.tsx | 11 +- src/hooks/useMarkets.ts | 128 ++++++++-------- src/hooks/useRebalance.ts | 4 +- src/hooks/useTransactionWithToast.tsx | 2 +- src/hooks/useUserPositions.ts | 140 +++++++++--------- 11 files changed, 161 insertions(+), 149 deletions(-) diff --git a/app/positions/components/FromAndToMarkets.tsx b/app/positions/components/FromAndToMarkets.tsx index 565db70f..81dbe564 100644 --- a/app/positions/components/FromAndToMarkets.tsx +++ b/app/positions/components/FromAndToMarkets.tsx @@ -325,4 +325,4 @@ export function FromAndToMarkets({
); -} \ No newline at end of file +} diff --git a/app/positions/components/MarketBadge.tsx b/app/positions/components/MarketBadge.tsx index 77ac9dc9..4096de1d 100644 --- a/app/positions/components/MarketBadge.tsx +++ b/app/positions/components/MarketBadge.tsx @@ -5,7 +5,7 @@ type MarketBadgeProps = { | { 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 1aca846b..e0f267e5 100644 --- a/app/positions/components/PositionsContent.tsx +++ b/app/positions/components/PositionsContent.tsx @@ -28,9 +28,7 @@ export default function Positions() {
-

- Your Supplies -

+

Your Supplies

); -} \ No newline at end of file +} diff --git a/app/positions/components/RebalanceActionInput.tsx b/app/positions/components/RebalanceActionInput.tsx index 3a9d1114..37b634bd 100644 --- a/app/positions/components/RebalanceActionInput.tsx +++ b/app/positions/components/RebalanceActionInput.tsx @@ -15,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 8cc3bb71..81e00808 100644 --- a/app/positions/components/RebalanceCart.tsx +++ b/app/positions/components/RebalanceCart.tsx @@ -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 1dbf0501..e849a7ac 100644 --- a/app/positions/components/RebalanceModal.tsx +++ b/app/positions/components/RebalanceModal.tsx @@ -26,7 +26,7 @@ type RebalanceModalProps = { groupedPosition: GroupedPosition; isOpen: boolean; onClose: () => void; - refetch: () => void; + refetch: (onSuccess?: () => void) => void; isRefetching: boolean; }; @@ -172,8 +172,9 @@ export function RebalanceModal({ }, [executeRebalance]); const handleManualRefresh = () => { - refetch(); - toast.info('Data refreshed', {icon: 🚀, delay: 1000}); + refetch(() => { + toast.info('Data refreshed', { icon: 🚀 }); + }); }; return ( @@ -198,7 +199,7 @@ export function RebalanceModal({ onClick={handleManualRefresh} disabled={isRefetching} type="button" - className="flex items-center gap-2 rounded-md bg-gray-200 dark:bg-gray-700 px-3 py-1 text-sm text-secondary transition-colors hover:bg-gray-300 dark:hover:bg-gray-600 disabled:opacity-50" + className="flex items-center gap-2 rounded-md bg-gray-200 px-3 py-1 text-sm text-secondary transition-colors hover:bg-gray-300 disabled:opacity-50 dark:bg-gray-700 dark:hover:bg-gray-600" > Refresh @@ -287,4 +288,4 @@ export function RebalanceModal({ )} ); -} \ No newline at end of file +} diff --git a/src/hooks/useMarkets.ts b/src/hooks/useMarkets.ts index 0ffc10c0..df5b67c6 100644 --- a/src/hooks/useMarkets.ts +++ b/src/hooks/useMarkets.ts @@ -148,69 +148,72 @@ const useMarkets = () => { refetch: refetchLiquidations, } = useLiquidations(); - const fetchData = useCallback(async (isRefetch = false) => { - try { - if (isRefetch) { - setIsRefetching(true); - } else { - setLoading(true); - } + const fetchData = useCallback( + async (isRefetch = false) => { + try { + if (isRefetch) { + setIsRefetching(true); + } else { + setLoading(true); + } + + // Fetch markets + const marketsResponse = await fetch('https://blue-api.morpho.org/graphql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: marketsQuery, + variables: { first: 1000, where: { whitelisted: true } }, + }), + }); + const marketsResult = await marketsResponse.json(); + const markets = marketsResult.data.markets.items as Market[]; + + const filtered = markets + .filter((market) => market.collateralAsset != undefined) + .filter( + (market) => market.warnings.find((w) => w.type === 'not_whitelisted') === undefined, + ) + .filter((market) => isSupportedChain(market.morphoBlue.chain.id)); + + const final = filtered.map((market) => { + const entry = market.state.rewards.find( + (reward) => reward.asset.address.toLowerCase() === MORPHOTokenAddress.toLowerCase(), + ); + + const warningsWithDetail = getMarketWarningsWithDetail(market); + const isProtectedByLiquidationBots = liquidatedMarketIds.has(market.id); + + if (!entry) { + return { + ...market, + rewardPer1000USD: undefined, + warningsWithDetail, + isProtectedByLiquidationBots, + }; + } + + const supplyAssetUSD = Number(market.state.supplyAssetsUsd); + const rewardPer1000USD = getRewardPer1000USD(entry.yearlySupplyTokens, supplyAssetUSD); - // Fetch markets - const marketsResponse = await fetch('https://blue-api.morpho.org/graphql', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - query: marketsQuery, - variables: { first: 1000, where: { whitelisted: true } }, - }), - }); - const marketsResult = await marketsResponse.json(); - const markets = marketsResult.data.markets.items as Market[]; - - const filtered = markets - .filter((market) => market.collateralAsset != undefined) - .filter( - (market) => market.warnings.find((w) => w.type === 'not_whitelisted') === undefined, - ) - .filter((market) => isSupportedChain(market.morphoBlue.chain.id)); - - const final = filtered.map((market) => { - const entry = market.state.rewards.find( - (reward) => reward.asset.address.toLowerCase() === MORPHOTokenAddress.toLowerCase(), - ); - - const warningsWithDetail = getMarketWarningsWithDetail(market); - const isProtectedByLiquidationBots = liquidatedMarketIds.has(market.id); - - if (!entry) { return { ...market, - rewardPer1000USD: undefined, + rewardPer1000USD, warningsWithDetail, isProtectedByLiquidationBots, }; - } - - const supplyAssetUSD = Number(market.state.supplyAssetsUsd); - const rewardPer1000USD = getRewardPer1000USD(entry.yearlySupplyTokens, supplyAssetUSD); - - return { - ...market, - rewardPer1000USD, - warningsWithDetail, - isProtectedByLiquidationBots, - }; - }); - - setData(final); - } catch (_error) { - setError(_error); - } finally { - setLoading(false); - setIsRefetching(false); - } - }, [liquidatedMarketIds]); + }); + + setData(final); + } catch (_error) { + setError(_error); + } finally { + setLoading(false); + setIsRefetching(false); + } + }, + [liquidatedMarketIds], + ); useEffect(() => { if (!liquidationsLoading) { @@ -218,10 +221,13 @@ const useMarkets = () => { } }, [liquidationsLoading, fetchData]); - const refetch = useCallback(() => { - refetchLiquidations(); - fetchData(true).catch(console.error); - }, [refetchLiquidations, fetchData]); + const refetch = useCallback( + (onSuccess?: () => void) => { + refetchLiquidations(); + fetchData(true).then(onSuccess).catch(console.error); + }, + [refetchLiquidations, fetchData], + ); const isLoading = loading || liquidationsLoading; const combinedError = error || liquidationsError; diff --git a/src/hooks/useRebalance.ts b/src/hooks/useRebalance.ts index 8b23da19..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, onRebalance?: () => void ) => { +export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: () => void) => { const [rebalanceActions, setRebalanceActions] = useState([]); const [isConfirming, setIsConfirming] = useState(false); const [currentStep, setCurrentStep] = useState< @@ -65,7 +65,7 @@ export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: () successText: 'Positions rebalanced successfully', errorText: 'Failed to rebalance positions', chainId: groupedPosition.chainId, - onSuccess: onRebalance + onSuccess: onRebalance, }); const executeRebalance = useCallback(async () => { diff --git a/src/hooks/useTransactionWithToast.tsx b/src/hooks/useTransactionWithToast.tsx index 25973895..2b66fa03 100644 --- a/src/hooks/useTransactionWithToast.tsx +++ b/src/hooks/useTransactionWithToast.tsx @@ -101,7 +101,7 @@ export function useTransactionWithToast({ toastId, onClick, renderToastContent, - onSuccess + onSuccess, ]); return { sendTransactionAsync, sendTransaction, isConfirming, isConfirmed }; diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts index a61423f5..54e63742 100644 --- a/src/hooks/useUserPositions.ts +++ b/src/hooks/useUserPositions.ts @@ -128,87 +128,93 @@ const useUserPositions = (user: string | undefined) => { const [history, setHistory] = useState([]); const [error, setError] = useState(null); - const fetchData = useCallback(async (isRefetch = false) => { - if (!user) return; - - try { - if (isRefetch) { - setIsRefetching(true); - } else { - setLoading(true); - } + const fetchData = useCallback( + async (isRefetch = false) => { + if (!user) return; + + try { + if (isRefetch) { + setIsRefetching(true); + } else { + setLoading(true); + } - const [responseMainnet, responseBase] = await Promise.all([ - fetch('https://blue-api.morpho.org/graphql', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query, - variables: { - address: user, - chainId: SupportedNetworks.Mainnet, + const [responseMainnet, responseBase] = await Promise.all([ + fetch('https://blue-api.morpho.org/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', }, + body: JSON.stringify({ + query, + variables: { + address: user, + chainId: SupportedNetworks.Mainnet, + }, + }), }), - }), - fetch('https://blue-api.morpho.org/graphql', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query, - variables: { - address: user, - chainId: SupportedNetworks.Base, + fetch('https://blue-api.morpho.org/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', }, + body: JSON.stringify({ + query, + variables: { + address: user, + chainId: SupportedNetworks.Base, + }, + }), }), - }), - ]); - - const result1 = await responseMainnet.json(); - const result2 = await responseBase.json(); - - const marketPositions: MarketPosition[] = []; - const transactions: UserTransaction[] = []; - - for (const result of [result1, result2]) { - // eslint-disable-next-line @typescript-eslint/prefer-optional-chain - if (result.data && 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 as UserTransaction[])); + ]); + + const result1 = await responseMainnet.json(); + const result2 = await responseBase.json(); + + const marketPositions: MarketPosition[] = []; + const transactions: UserTransaction[] = []; + + for (const result of [result1, result2]) { + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain + if (result.data && 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 as UserTransaction[])); + } } - } - const filtered = marketPositions.filter( - (position: MarketPosition) => position.supplyShares.toString() !== '0', - ); + const filtered = marketPositions.filter( + (position: MarketPosition) => position.supplyShares.toString() !== '0', + ); - setHistory(transactions); + setHistory(transactions); - setData(filtered); - } catch (_error) { - setError(_error); - } finally { - setLoading(false); - setIsRefetching(false); - } - }, [user]); + setData(filtered); + } catch (_error) { + setError(_error); + } finally { + setLoading(false); + setIsRefetching(false); + } + }, + [user], + ); useEffect(() => { fetchData().catch(console.error); }, [fetchData]); - const refetch = useCallback(() => { - fetchData(true).catch(console.error); - }, [fetchData]); + const refetch = useCallback( + (onSuccess?: () => void) => { + fetchData(true).then(onSuccess).catch(console.error); + }, + [fetchData], + ); return { loading, isRefetching, data, history, error, refetch }; };