From 248d1d0c04a0c43bbff76adafddd44050eaf9aa1 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Thu, 10 Apr 2025 12:11:21 +0800 Subject: [PATCH 01/12] feat: unify style --- app/market/[chainId]/[marketid]/RateChart.tsx | 2 +- .../[chainId]/[marketid]/VolumeChart.tsx | 2 +- .../[marketid]/components/BorrowsTable.tsx | 2 +- .../components/LiquidationsTable.tsx | 2 +- .../[marketid]/components/SuppliesTable.tsx | 2 +- app/market/[chainId]/[marketid]/content.tsx | 6 +- app/markets/components/OracleFilter.tsx | 4 +- app/markets/components/markets.tsx | 8 +- app/positions/components/PositionsContent.tsx | 13 +- docs/Styling.md | 19 +- .../Borrow/AddCollateralAndBorrow.tsx | 2 +- .../Borrow/WithdrawCollateralAndRepay.tsx | 2 +- src/components/SupplyModalContent.tsx | 237 ++++++++++++++++ src/components/SupplyModalV2.tsx | 114 ++++++++ src/components/WithdrawModalContent.tsx | 179 ++++++++++++ src/components/index.ts | 4 + src/components/supplyModal.tsx | 262 ------------------ src/components/withdrawModal.tsx | 228 --------------- src/graphql/queries.ts | 8 + 19 files changed, 584 insertions(+), 512 deletions(-) create mode 100644 src/components/SupplyModalContent.tsx create mode 100644 src/components/SupplyModalV2.tsx create mode 100644 src/components/WithdrawModalContent.tsx create mode 100644 src/components/index.ts delete mode 100644 src/components/supplyModal.tsx delete mode 100644 src/components/withdrawModal.tsx diff --git a/app/market/[chainId]/[marketid]/RateChart.tsx b/app/market/[chainId]/[marketid]/RateChart.tsx index d8af2ecf..632c3036 100644 --- a/app/market/[chainId]/[marketid]/RateChart.tsx +++ b/app/market/[chainId]/[marketid]/RateChart.tsx @@ -120,7 +120,7 @@ function RateChart({ ); return ( - + +
diff --git a/app/market/[chainId]/[marketid]/components/BorrowsTable.tsx b/app/market/[chainId]/[marketid]/components/BorrowsTable.tsx index 4a56250a..b2288d1f 100644 --- a/app/market/[chainId]/[marketid]/components/BorrowsTable.tsx +++ b/app/market/[chainId]/[marketid]/components/BorrowsTable.tsx @@ -53,7 +53,7 @@ export function BorrowsTable({ chainId, market }: BorrowsTableProps) { key={tableKey} aria-label="Borrow and repay activities" classNames={{ - wrapper: 'bg-surface shadow-sm', + wrapper: 'bg-surface shadow-sm rounded', table: 'bg-surface', }} bottomContent={ diff --git a/app/market/[chainId]/[marketid]/components/LiquidationsTable.tsx b/app/market/[chainId]/[marketid]/components/LiquidationsTable.tsx index 15f86d14..7d32d629 100644 --- a/app/market/[chainId]/[marketid]/components/LiquidationsTable.tsx +++ b/app/market/[chainId]/[marketid]/components/LiquidationsTable.tsx @@ -51,7 +51,7 @@ export function LiquidationsTable({ chainId, market }: LiquidationsTableProps) { key={tableKey} aria-label="Liquidations history" classNames={{ - wrapper: 'bg-surface shadow-sm', + wrapper: 'bg-surface shadow-sm rounded', table: 'bg-surface', }} bottomContent={ diff --git a/app/market/[chainId]/[marketid]/components/SuppliesTable.tsx b/app/market/[chainId]/[marketid]/components/SuppliesTable.tsx index 816969f7..a56cb6c3 100644 --- a/app/market/[chainId]/[marketid]/components/SuppliesTable.tsx +++ b/app/market/[chainId]/[marketid]/components/SuppliesTable.tsx @@ -52,7 +52,7 @@ export function SuppliesTable({ chainId, market }: SuppliesTableProps) { Market data not available; } - const cardStyle = 'bg-surface rounded-md shadow-sm p-4'; + const cardStyle = 'bg-surface rounded shadow-sm p-4'; const averageLTV = !market.state.collateralAssetsUsd || @@ -151,7 +151,7 @@ function MarketContent() { {showSupplyModal && ( - setShowSupplyModal(false)} /> + setShowSupplyModal(false)} isMarketPage /> )} {showBorrowModal && ( diff --git a/app/markets/components/OracleFilter.tsx b/app/markets/components/OracleFilter.tsx index 0bd0531a..beef1019 100644 --- a/app/markets/components/OracleFilter.tsx +++ b/app/markets/components/OracleFilter.tsx @@ -41,7 +41,7 @@ export default function OracleFilter({ selectedOracles, setSelectedOracles }: Or return (
diff --git a/app/markets/components/markets.tsx b/app/markets/components/markets.tsx index 887eb5f6..bec31dd9 100644 --- a/app/markets/components/markets.tsx +++ b/app/markets/components/markets.tsx @@ -11,7 +11,7 @@ import Header from '@/components/layout/header/Header'; import { useTokens } from '@/components/providers/TokenProvider'; import EmptyScreen from '@/components/Status/EmptyScreen'; import LoadingScreen from '@/components/Status/LoadingScreen'; -import { SupplyModal } from '@/components/supplyModal'; +import { SupplyModalV2 } from '@/components/SupplyModalV2'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useMarkets } from '@/hooks/useMarkets'; import { usePagination } from '@/hooks/usePagination'; @@ -361,9 +361,9 @@ export default function Markets() {

Markets

- {showSupplyModal && ( - setShowSupplyModal(false)} /> )} diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx index 7d41fcb2..8e5fb46b 100644 --- a/app/positions/components/PositionsContent.tsx +++ b/app/positions/components/PositionsContent.tsx @@ -16,8 +16,8 @@ import { Button } from '@/components/common/Button'; import Header from '@/components/layout/header/Header'; import EmptyScreen from '@/components/Status/EmptyScreen'; import LoadingScreen from '@/components/Status/LoadingScreen'; -import { SupplyModal } from '@/components/supplyModal'; -import { WithdrawModal } from '@/components/withdrawModal'; +import { SupplyModalV2 } from '@/components/SupplyModalV2'; +import { WithdrawModalContent } from '@/components/WithdrawModalContent'; import useUserPositionsSummaryData from '@/hooks/useUserPositionsSummaryData'; import { useUserRebalancerInfo } from '@/hooks/useUserRebalancerInfo'; import { SupportedNetworks } from '@/utils/networks'; @@ -106,23 +106,28 @@ export default function Positions() {
{showWithdrawModal && selectedPosition && ( - { setShowWithdrawModal(false); setSelectedPosition(null); }} refetch={() => void refetch()} + isMarketPage={false} /> )} {showSupplyModal && selectedPosition && ( - { setShowSupplyModal(false); setSelectedPosition(null); }} + refetch={() => void refetch()} + isMarketPage={false} /> )} diff --git a/docs/Styling.md b/docs/Styling.md index 97979624..29546292 100644 --- a/docs/Styling.md +++ b/docs/Styling.md @@ -10,8 +10,23 @@ Use these shared components instead of raw HTML elements: ## Component Guidelines -- Use `rounded` for tables, cards or bigger components -- Use `rounded-sm` for buttons, inputs +### Rounding +- Use `rounded` for: + - Modals + - Cards + - Large content areas + - Container components + - Tables + - Market info blocks + +- Use `rounded-sm` for: + - Buttons + - Inputs + - Filter components + - Small interactive elements + - Tooltips + - Tags/badges + - Helper text containers ### Button Component diff --git a/src/components/Borrow/AddCollateralAndBorrow.tsx b/src/components/Borrow/AddCollateralAndBorrow.tsx index 0a810a2e..71a4d81b 100644 --- a/src/components/Borrow/AddCollateralAndBorrow.tsx +++ b/src/components/Borrow/AddCollateralAndBorrow.tsx @@ -156,7 +156,7 @@ export function AddCollateralAndBorrow({ {!showProcessModal && (
{/* Position Overview Box with dynamic LTV */} -
+
Position Overview + ) : (!permit2Authorized && !useEth) || (!usePermit2Setting && !isApproved) ? ( + + ) : ( + + )} +
+
+ )} + + ); +} diff --git a/src/components/SupplyModalV2.tsx b/src/components/SupplyModalV2.tsx new file mode 100644 index 00000000..a84b6bad --- /dev/null +++ b/src/components/SupplyModalV2.tsx @@ -0,0 +1,114 @@ +import React, { useState } from 'react'; +import { Cross1Icon } from '@radix-ui/react-icons'; +import { FaArrowRightArrowLeft } from 'react-icons/fa6'; +import { useAccount, useBalance } from 'wagmi'; +import { Market, MarketPosition } from '@/utils/types'; +import { Button } from './common'; +import { TokenIcon } from './TokenIcon'; +import { SupplyModalContent } from './SupplyModalContent'; +import { WithdrawModalContent } from './WithdrawModalContent'; + +type SupplyModalV2Props = { + market: Market; + position?: MarketPosition; + onClose: () => void; + refetch?: () => void; + isMarketPage?: boolean; +}; + +export function SupplyModalV2({ market, position, onClose, refetch, isMarketPage }: SupplyModalV2Props): JSX.Element { + const [mode, setMode] = useState<'supply' | 'withdraw'>('supply'); + const { address: account } = useAccount(); + + // Get token balance + const { data: tokenBalance } = useBalance({ + token: market.loanAsset.address as `0x${string}`, + address: account, + chainId: market.morphoBlue.chain.id, + }); + + return ( +
+
+
+ + +
+
+
+ + {market.loanAsset.symbol} +
+ + {mode === 'supply' ? 'Supply Position' : 'Withdraw Position'} + +
+ + +
+ + {/* Position Overview */} +
+
Position Overview
+
+
+

Current Supply

+
+ +

+ {position ? position.state.supplyAssets : '0'} {market.loanAsset.symbol} +

+
+
+
+

Supply APY

+

{(market.state.supplyApy * 100).toFixed(2)}%

+
+
+
+ + {mode === 'supply' ? ( + + ) : ( + {})} + isMarketPage={isMarketPage} + /> + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/WithdrawModalContent.tsx b/src/components/WithdrawModalContent.tsx new file mode 100644 index 00000000..194b50ed --- /dev/null +++ b/src/components/WithdrawModalContent.tsx @@ -0,0 +1,179 @@ +// Import the necessary hooks +import { useCallback, useState } from 'react'; + +import { Address, encodeFunctionData } from 'viem'; +import { useAccount } from 'wagmi'; +import morphoAbi from '@/abis/morpho'; +import { MarketInfoBlock } from '@/components/common/MarketInfoBlock'; +import Input from '@/components/Input/Input'; +import AccountConnect from '@/components/layout/header/AccountConnect'; +import { useMarketNetwork } from '@/hooks/useMarketNetwork'; +import { useStyledToast } from '@/hooks/useStyledToast'; +import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; +import { formatBalance, formatReadable, min } from '@/utils/balance'; +import { MORPHO } from '@/utils/morpho'; +import { Market, MarketPosition } from '@/utils/types'; +import { Button } from './common'; +import { TokenIcon } from './TokenIcon'; + +type WithdrawModalContentProps = { + position?: MarketPosition; + market?: Market; + onClose: () => void; + refetch: () => void; + isMarketPage?: boolean; +}; + +export function WithdrawModalContent({ position, market, onClose, refetch, isMarketPage }: WithdrawModalContentProps): JSX.Element { + const toast = useStyledToast(); + const [inputError, setInputError] = useState(null); + const [withdrawAmount, setWithdrawAmount] = useState(BigInt(0)); + + const { address: account, isConnected, chainId } = useAccount(); + + // Use market from either position or direct prop + const activeMarket = position?.market ?? market; + + if (!activeMarket) { + return ( +
+

Market data not available

+
+ ); + } + + const { needSwitchChain, switchToNetwork } = useMarketNetwork({ + targetChainId: activeMarket.morphoBlue.chain.id, + }); + + const { isConfirming, sendTransaction } = useTransactionWithToast({ + toastId: 'withdraw', + pendingText: `Withdrawing ${formatBalance( + withdrawAmount, + activeMarket.loanAsset.decimals, + )} ${activeMarket.loanAsset.symbol}`, + successText: `${activeMarket.loanAsset.symbol} Withdrawn`, + errorText: 'Failed to withdraw', + chainId, + pendingDescription: `Withdrawing from market ${activeMarket.uniqueKey.slice(2, 8)}...`, + successDescription: `Successfully withdrawn from market ${activeMarket.uniqueKey.slice( + 2, + 8, + )}`, + onSuccess: () => { + refetch(); + onClose(); + }, + }); + + const withdraw = useCallback(async () => { + if (!account) { + toast.info('No account connected', 'Please connect your wallet to continue.'); + return; + } + + if (!position) { + toast.error('No position', 'You do not have a position in this market to withdraw from.'); + return; + } + + const isMax = withdrawAmount.toString() === position.state.supplyAssets.toString(); + + const assetsToWithdraw = isMax ? '0' : withdrawAmount.toString(); + const sharesToWithdraw = isMax ? position.state.supplyShares : '0'; + + sendTransaction({ + account, + to: MORPHO, + data: encodeFunctionData({ + abi: morphoAbi, + functionName: 'withdraw', + args: [ + { + loanToken: activeMarket.loanAsset.address as Address, + collateralToken: activeMarket.collateralAsset.address as Address, + oracle: activeMarket.oracleAddress as Address, + irm: activeMarket.irmAddress as Address, + lltv: BigInt(activeMarket.lltv), + }, + BigInt(assetsToWithdraw), + BigInt(sharesToWithdraw), + account, // onBehalf + account, // receiver + ], + }), + chainId: activeMarket.morphoBlue.chain.id, + }); + }, [ + account, + activeMarket, + position, + withdrawAmount, + sendTransaction, + position?.state.supplyAssets, + position?.state.supplyShares, + toast, + ]); + + return ( +
+ {!isMarketPage && ( + <> + + + )} + + {!isConnected && ( +
+
+ +
+
+ )} + +
+ Withdraw amount +

+ Market Liquidity: {formatReadable( + formatBalance(activeMarket.state.liquidityAssets, activeMarket.loanAsset.decimals) + )} {activeMarket.loanAsset.symbol} +

+
+ +
+
+ + {inputError &&

{inputError}

} +
+ {needSwitchChain ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 00000000..98b3d83f --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,4 @@ +export { SupplyModalV2 } from './SupplyModalV2'; +export { SupplyModalContent } from './SupplyModalContent'; +export { WithdrawModalContent } from './WithdrawModalContent'; +export { BorrowModal } from './BorrowModal'; \ No newline at end of file diff --git a/src/components/supplyModal.tsx b/src/components/supplyModal.tsx deleted file mode 100644 index 99f487a4..00000000 --- a/src/components/supplyModal.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import React from 'react'; -import { Switch } from '@nextui-org/react'; -import { Cross1Icon, ExternalLinkIcon } from '@radix-ui/react-icons'; -import { useAccount } from 'wagmi'; -import Input from '@/components/Input/Input'; -import AccountConnect from '@/components/layout/header/AccountConnect'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; -import { useMarketNetwork } from '@/hooks/useMarketNetwork'; -import { useSupplyMarket } from '@/hooks/useSupplyMarket'; -import { formatBalance, formatReadable } from '@/utils/balance'; -import { getExplorerURL } from '@/utils/external'; -import { getIRMTitle } from '@/utils/morpho'; -import { isWETH } from '@/utils/tokens'; -import { Market } from '@/utils/types'; -import { Button } from './common'; -import { MarketInfoBlock } from './common/MarketInfoBlock'; -import OracleVendorBadge from './OracleVendorBadge'; -import { SupplyProcessModal } from './SupplyProcessModal'; -import { TokenIcon } from './TokenIcon'; - -type SupplyModalProps = { - market: Market; - onClose: () => void; -}; - -export function SupplyModal({ market, onClose }: SupplyModalProps): JSX.Element { - const [usePermit2Setting] = useLocalStorage('usePermit2', true); - const { isConnected } = useAccount(); - - // Use the hook to handle all supply logic - const { - supplyAmount, - setSupplyAmount, - inputError, - setInputError, - useEth, - setUseEth, - showProcessModal, - setShowProcessModal, - currentStep, - tokenBalance, - ethBalance, - isApproved, - permit2Authorized, - isLoadingPermit2, - supplyPending, - approveAndSupply, - signAndSupply, - } = useSupplyMarket(market); - - // Use the market network hook to handle network switching - const { needSwitchChain, switchToNetwork } = useMarketNetwork({ - targetChainId: market.morphoBlue.chain.id, - }); - - return ( -
-
- {showProcessModal && ( - setShowProcessModal(false)} - tokenSymbol={market.loanAsset.symbol} - useEth={useEth} - usePermit2={usePermit2Setting} - /> - )} - {!showProcessModal && ( -
- - -
- Supply {market.loanAsset.symbol} - -
- -

- You are supplying {market.loanAsset.symbol} to the following market:{' '} -

- - - -
-
Market Config
- - - -
-

Oracle:

- - - -
- -
- -
-
Market State
- -
-

Total Supply:

- -

- {formatReadable( - formatBalance(market.state.supplyAssets, market.loanAsset.decimals), - )} -

-
- -
-

Liquidity:

-

- {formatReadable( - formatBalance(market.state.liquidityAssets, market.loanAsset.decimals), - )} -

-
- -
-

Utilization:

-

- {formatReadable(market.state.utilization * 100)}% -

-
-
- - {isConnected ? ( -
-
-

My Balance:

-
-

- {useEth - ? formatBalance(ethBalance ?? BigInt(0), 18) - : formatBalance(tokenBalance ?? BigInt(0), market.loanAsset.decimals)} -

-

- {useEth ? 'ETH' : market.loanAsset.symbol}{' '} -

-
- -
-
-
- {isWETH(market.loanAsset.address, market.morphoBlue.chain.id) && ( -
-
-
-
Use ETH instead
- -
-
- )} -
- ) : ( -
-
- -
-
- )} - -
Supply amount
- -
-
- - {inputError &&

{inputError}

} -
- - {needSwitchChain ? ( - - ) : (!permit2Authorized && !useEth) || (!usePermit2Setting && !isApproved) ? ( - - ) : ( - - )} -
-
- )} -
-
- ); -} diff --git a/src/components/withdrawModal.tsx b/src/components/withdrawModal.tsx deleted file mode 100644 index 5c39177a..00000000 --- a/src/components/withdrawModal.tsx +++ /dev/null @@ -1,228 +0,0 @@ -// Import the necessary hooks -import { useCallback, useState } from 'react'; - -import { Cross1Icon } from '@radix-ui/react-icons'; -import { Address, encodeFunctionData } from 'viem'; -import { useAccount } from 'wagmi'; -import morphoAbi from '@/abis/morpho'; -import { MarketInfoBlock } from '@/components/common/MarketInfoBlock'; -import Input from '@/components/Input/Input'; -import AccountConnect from '@/components/layout/header/AccountConnect'; -import { useMarketNetwork } from '@/hooks/useMarketNetwork'; -import { useStyledToast } from '@/hooks/useStyledToast'; -import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; -import { formatBalance, formatReadable, min } from '@/utils/balance'; -import { MORPHO } from '@/utils/morpho'; -import { MarketPosition } from '@/utils/types'; -import { TokenIcon } from './TokenIcon'; -type ModalProps = { - position: MarketPosition; - onClose: () => void; - refetch: () => void; -}; - -export function WithdrawModal({ position, onClose, refetch }: ModalProps): JSX.Element { - // Add state for the supply amount - const toast = useStyledToast(); - const [inputError, setInputError] = useState(null); - const [withdrawAmount, setWithdrawAmount] = useState(BigInt(0)); - - const { address: account, isConnected, chainId } = useAccount(); - - // Use the market network hook for chain switching - const { needSwitchChain, switchToNetwork } = useMarketNetwork({ - targetChainId: position.market.morphoBlue.chain.id, - }); - - const { isConfirming, sendTransaction } = useTransactionWithToast({ - toastId: 'withdraw', - pendingText: `Withdrawing ${formatBalance( - withdrawAmount, - position.market.loanAsset.decimals, - )} ${position.market.loanAsset.symbol}`, - successText: `${position.market.loanAsset.symbol} Withdrawn`, - errorText: 'Failed to withdraw', - chainId, - pendingDescription: `Withdrawing from market ${position.market.uniqueKey.slice(2, 8)}...`, - successDescription: `Successfully withdrawn from market ${position.market.uniqueKey.slice( - 2, - 8, - )}`, - onSuccess: () => { - refetch(); - onClose(); - }, - }); - - const withdraw = useCallback(async () => { - if (!account) { - toast.info('No account connected', 'Please connect your wallet to continue.'); - return; - } - - const isMax = withdrawAmount.toString() === position.state.supplyAssets.toString(); - - const assetsToWithdraw = isMax ? '0' : withdrawAmount.toString(); - const sharesToWithdraw = isMax ? position.state.supplyShares : '0'; - - sendTransaction({ - account, - to: MORPHO, - data: encodeFunctionData({ - abi: morphoAbi, - functionName: 'withdraw', - args: [ - { - loanToken: position.market.loanAsset.address as Address, - collateralToken: position.market.collateralAsset.address as Address, - oracle: position.market.oracleAddress as Address, - irm: position.market.irmAddress as Address, - lltv: BigInt(position.market.lltv), - }, - BigInt(assetsToWithdraw), - BigInt(sharesToWithdraw), - account, // onBehalf - account, // receiver - ], - }), - chainId: position.market.morphoBlue.chain.id, - }); - }, [ - account, - position.market, - withdrawAmount, - sendTransaction, - position.state.supplyAssets, - position.state.supplyShares, - toast, - ]); - - return ( -
-
- - -
- Withdraw {position.market.loanAsset.symbol} - -
- -

- {' '} - You are withdrawing {position.market.loanAsset.symbol} from the following market:{' '} -

- -
- -
-

Available Liquidity:

- -
-

- {formatReadable( - formatBalance( - position.market.state.liquidityAssets, - position.market.loanAsset.decimals, - ), - )} -

-

{position.market.loanAsset.symbol}

-
- -
-
-
- -
-

Supplied Amount:

- -
-

- {formatReadable( - formatBalance(position.state.supplyAssets, position.market.loanAsset.decimals), - )} -

-

{position.market.loanAsset.symbol}

-
- -
-
-
-
- - {!isConnected && ( -
-
- -
-
- )} - -
Withdraw amount
- -
-
- - {/* input error */} - {inputError &&

{inputError}

} -
- {needSwitchChain ? ( - - ) : ( - - )} -
-
-
- ); -} diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index bfec025c..6e533dce 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -129,6 +129,14 @@ export const marketFragment = ` } } } + riskAnalysis { + analysis { + ... on CredoraRiskAnalysis { + score + rating + } + } + } } ${feedFieldsFragment} `; From d1b2b36e164d75123cb992e02719dbde857201db Mon Sep 17 00:00:00 2001 From: antoncoding Date: Thu, 10 Apr 2025 13:23:53 +0800 Subject: [PATCH 02/12] feat: supply modal style --- app/market/[chainId]/[marketid]/content.tsx | 54 ++-- .../components/PositionsSummaryTable.tsx | 9 +- .../components/SuppliedMarketsDetail.tsx | 23 +- .../Borrow/AddCollateralAndBorrow.tsx | 69 +---- .../Borrow/WithdrawCollateralAndRepay.tsx | 62 +---- src/components/BorrowModal.tsx | 69 +++-- src/components/SupplyModalContent.tsx | 240 ++++++------------ src/components/SupplyModalV2.tsx | 85 ++++--- src/components/WithdrawModalContent.tsx | 116 ++++----- src/components/common/MarketDetailsBlock.tsx | 139 ++++++++++ 10 files changed, 441 insertions(+), 425 deletions(-) create mode 100644 src/components/common/MarketDetailsBlock.tsx diff --git a/app/market/[chainId]/[marketid]/content.tsx b/app/market/[chainId]/[marketid]/content.tsx index a74b6d6b..6e1f39fe 100644 --- a/app/market/[chainId]/[marketid]/content.tsx +++ b/app/market/[chainId]/[marketid]/content.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useCallback } from 'react'; +import { useState, useCallback, useMemo } from 'react'; import { Card, CardHeader, CardBody } from '@nextui-org/card'; import { ExternalLinkIcon, ChevronLeftIcon } from '@radix-ui/react-icons'; import Image from 'next/image'; @@ -26,20 +26,29 @@ import { LiquidationsTable } from './components/LiquidationsTable'; import { SuppliesTable } from './components/SuppliesTable'; import RateChart from './RateChart'; import VolumeChart from './VolumeChart'; +import { useOraclePrice } from '@/hooks/useOraclePrice'; +import { useAccount } from 'wagmi'; const NOW = Math.floor(Date.now() / 1000); const WEEK_IN_SECONDS = 7 * 24 * 60 * 60; function MarketContent() { + // 1. Get URL params and router first const { marketid, chainId } = useParams(); + const router = useRouter(); + const searchParams = useSearchParams(); + const { address: account } = useAccount(); + // 2. Network setup const network = Number(chainId as string) as SupportedNetworks; const networkImg = getNetworkImg(network); + // 3. All useState hooks grouped together const [showSupplyModal, setShowSupplyModal] = useState(false); const [showBorrowModal, setShowBorrowModal] = useState(false); - const router = useRouter(); - const searchParams = useSearchParams(); + const [apyTimeframe, setApyTimeframe] = useState<'1day' | '7day' | '30day'>('7day'); + const [volumeTimeframe, setVolumeTimeframe] = useState<'1day' | '7day' | '30day'>('7day'); + const [volumeView, setVolumeView] = useState<'USD' | 'Asset'>('USD'); const [rateTimeRange, setRateTimeRange] = useState({ startTimestamp: NOW - WEEK_IN_SECONDS, endTimestamp: NOW, @@ -50,10 +59,8 @@ function MarketContent() { endTimestamp: NOW, interval: 'HOUR', }); - const [apyTimeframe, setApyTimeframe] = useState<'1day' | '7day' | '30day'>('7day'); - const [volumeTimeframe, setVolumeTimeframe] = useState<'1day' | '7day' | '30day'>('7day'); - const [volumeView, setVolumeView] = useState<'USD' | 'Asset'>('USD'); + // 4. Data fetching hooks const { data: market, isLoading: isMarketLoading, @@ -66,6 +73,21 @@ function MarketContent() { refetch: refetchHistoricalData, } = useMarketHistoricalData(marketid as string, network, rateTimeRange, volumeTimeRange); + // 5. Oracle price hook - safely handle undefined market + const { price: oraclePrice } = useOraclePrice({ + oracle: market?.oracleAddress as `0x${string}`, + chainId: market?.morphoBlue.chain.id, + }); + + // 6. All memoized values and callbacks + const formattedOraclePrice = useMemo(() => { + if (!market) return '0'; + const adjusted = + (oraclePrice * BigInt(10 ** market.collateralAsset.decimals)) / + BigInt(10 ** market.loanAsset.decimals); + return formatUnits(adjusted, 36); + }, [oraclePrice, market]); + const setTimeRangeAndRefetch = useCallback( (days: number, type: 'rate' | 'volume') => { const endTimestamp = Math.floor(Date.now() / 1000); @@ -87,17 +109,14 @@ function MarketContent() { [refetchHistoricalData, setRateTimeRange, setVolumeTimeRange], ); - const handleBackToMarkets = () => { - // Preserve all current search parameters when going back + const handleBackToMarkets = useCallback(() => { const currentParams = searchParams.toString(); const marketsPath = '/markets'; - - // If we have query params, append them to the markets URL const targetPath = currentParams ? `${marketsPath}?${currentParams}` : marketsPath; - router.push(targetPath); - }; + }, [router, searchParams]); + // 7. Early returns for loading/error states if (isMarketLoading) { return (
@@ -114,8 +133,8 @@ function MarketContent() { return
Market data not available
; } + // 8. Derived values that depend on market data const cardStyle = 'bg-surface rounded shadow-sm p-4'; - const averageLTV = !market.state.collateralAssetsUsd || !market.state.borrowAssetsUsd || @@ -274,11 +293,16 @@ function MarketContent() { )}
+
+ Live Price: + + {Number(formattedOraclePrice).toFixed(4)} {market.loanAsset.symbol} + +

Feed Routes:

{market.oracle.data && (
- {' '} {' '} + />
)}
diff --git a/app/positions/components/PositionsSummaryTable.tsx b/app/positions/components/PositionsSummaryTable.tsx index 4afcd514..8c2bf2a5 100644 --- a/app/positions/components/PositionsSummaryTable.tsx +++ b/app/positions/components/PositionsSummaryTable.tsx @@ -37,8 +37,7 @@ import { SuppliedMarketsDetail } from './SuppliedMarketsDetail'; type PositionsSummaryTableProps = { account: string; marketPositions: MarketPositionWithEarnings[]; - setShowWithdrawModal: (show: boolean) => void; - setShowSupplyModal: (show: boolean) => void; + setShowModal: (show: boolean) => void; setSelectedPosition: (position: MarketPosition) => void; refetch: (onSuccess?: () => void) => void; isRefetching: boolean; @@ -48,8 +47,7 @@ type PositionsSummaryTableProps = { export function PositionsSummaryTable({ marketPositions, - setShowWithdrawModal, - setShowSupplyModal, + setShowModal, setSelectedPosition, refetch, isRefetching, @@ -330,8 +328,7 @@ export function PositionsSummaryTable({ > diff --git a/app/positions/components/SuppliedMarketsDetail.tsx b/app/positions/components/SuppliedMarketsDetail.tsx index 9804e5e8..e5f53680 100644 --- a/app/positions/components/SuppliedMarketsDetail.tsx +++ b/app/positions/components/SuppliedMarketsDetail.tsx @@ -9,10 +9,10 @@ import { TokenIcon } from '@/components/TokenIcon'; import { formatReadable, formatBalance } from '@/utils/balance'; import { MarketPosition, GroupedPosition, WarningWithDetail, WarningCategory } from '@/utils/types'; import { getCollateralColor } from '../utils/colors'; + type SuppliedMarketsDetailProps = { groupedPosition: GroupedPosition; - setShowWithdrawModal: (show: boolean) => void; - setShowSupplyModal: (show: boolean) => void; + setShowModal: (show: boolean) => void; setSelectedPosition: (position: MarketPosition) => void; }; @@ -39,8 +39,7 @@ function WarningTooltip({ warnings }: { warnings: WarningWithDetail[] }) { export function SuppliedMarketsDetail({ groupedPosition, - setShowWithdrawModal, - setShowSupplyModal, + setShowModal, setSelectedPosition, }: SuppliedMarketsDetailProps) { // Sort active markets by size @@ -205,26 +204,16 @@ export function SuppliedMarketsDetail({
diff --git a/src/components/Borrow/AddCollateralAndBorrow.tsx b/src/components/Borrow/AddCollateralAndBorrow.tsx index 71a4d81b..0c7428ee 100644 --- a/src/components/Borrow/AddCollateralAndBorrow.tsx +++ b/src/components/Borrow/AddCollateralAndBorrow.tsx @@ -10,13 +10,13 @@ import AccountConnect from '@/components/layout/header/AccountConnect'; import { useBorrowTransaction } from '@/hooks/useBorrowTransaction'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useMarketNetwork } from '@/hooks/useMarketNetwork'; -import { useOraclePrice } from '@/hooks/useOraclePrice'; import { formatBalance, formatReadable } from '@/utils/balance'; import { isWETH } from '@/utils/tokens'; import { Market, MarketPosition } from '@/utils/types'; import { BorrowProcessModal } from '../BorrowProcessModal'; import { TokenIcon } from '../TokenIcon'; import { getLTVColor, getLTVProgressColor } from './helpers'; +import { MarketDetailsBlock } from '@/components/common/MarketDetailsBlock'; type BorrowLogicProps = { market: Market; @@ -78,11 +78,6 @@ export function AddCollateralAndBorrow({ borrowAmount, }); - const { price: oraclePrice } = useOraclePrice({ - oracle: market.oracleAddress as Address, - chainId: market.morphoBlue.chain.id, - }); - // Calculate current and new LTV whenever relevant values change useEffect(() => { if (!currentPosition) { @@ -90,7 +85,7 @@ export function AddCollateralAndBorrow({ } else { // Calculate current LTV from position data const currentCollateralValue = - (BigInt(currentPosition.state.collateral) * oraclePrice) / BigInt(10 ** 36); + (BigInt(currentPosition.state.collateral) * BigInt(market.collateralPrice)) / BigInt(10 ** 36); const currentBorrowValue = BigInt(currentPosition.state.borrowAssets || 0); if (currentCollateralValue > 0) { @@ -100,14 +95,14 @@ export function AddCollateralAndBorrow({ setCurrentLTV(BigInt(0)); } } - }, [currentPosition, market, oraclePrice]); + }, [currentPosition, market]); useEffect(() => { // Calculate new LTV based on current position plus new amounts const newCollateral = BigInt(currentPosition?.state.collateral ?? 0) + collateralAmount; const newBorrow = BigInt(currentPosition?.state.borrowAssets ?? 0) + borrowAmount; - const newCollateralValueInLoan = (newCollateral * oraclePrice) / BigInt(10 ** 36); + const newCollateralValueInLoan = (newCollateral * BigInt(market.collateralPrice)) / BigInt(10 ** 36); if (newCollateralValueInLoan > 0) { const ltv = (newBorrow * BigInt(10 ** 18)) / newCollateralValueInLoan; @@ -115,14 +110,7 @@ export function AddCollateralAndBorrow({ } else { setNewLTV(BigInt(0)); } - }, [currentPosition, collateralAmount, borrowAmount, market, oraclePrice]); - - const formattedOraclePrice = useMemo(() => { - const adjusted = - (oraclePrice * BigInt(10 ** market.collateralAsset.decimals)) / - BigInt(10 ** market.loanAsset.decimals); - return formatBalance(adjusted, 36); - }, [oraclePrice]); + }, [currentPosition, collateralAmount, borrowAmount, market]); // Function to refresh position data const handleRefreshPosition = () => { @@ -158,7 +146,7 @@ export function AddCollateralAndBorrow({ {/* Position Overview Box with dynamic LTV */}
- Position Overview + My Borrow
- {/* Market Stats */} -
-
Market Stats
- -
-

APY:

-

- {(market.state.borrowApy * 100).toFixed(2)}% -

- -

Available Liquidity:

-

- {formatReadable( - formatBalance(market.state.liquidityAssets, market.loanAsset.decimals), - )} -

- -

Utilization:

-

- {formatReadable(market.state.utilization * 100)}% -

-
-
- - {/* Oracle Price - compact format */} -
-
- - Oracle Price: {market.collateralAsset.symbol}/{market.loanAsset.symbol} - - - {formattedOraclePrice.toFixed(4)} {market.loanAsset.symbol} - -
-
- {isConnected && ( <> {/* Collateral Input Section */} -
+

Add Collateral

@@ -348,10 +300,9 @@ export function AddCollateralAndBorrow({

Borrow

- Balance:{' '} - {formatBalance( - loanTokenBalance ? loanTokenBalance : '0', - market.loanAsset.decimals, + Available:{' '} + {formatReadable( + formatBalance(market.state.liquidityAssets, market.loanAsset.decimals), )}{' '} {market.loanAsset.symbol}

diff --git a/src/components/Borrow/WithdrawCollateralAndRepay.tsx b/src/components/Borrow/WithdrawCollateralAndRepay.tsx index 3a0761e2..1f32085b 100644 --- a/src/components/Borrow/WithdrawCollateralAndRepay.tsx +++ b/src/components/Borrow/WithdrawCollateralAndRepay.tsx @@ -7,12 +7,12 @@ import Input from '@/components/Input/Input'; import AccountConnect from '@/components/layout/header/AccountConnect'; import { RepayProcessModal } from '@/components/RepayProcessModal'; import { useMarketNetwork } from '@/hooks/useMarketNetwork'; -import { useOraclePrice } from '@/hooks/useOraclePrice'; import { useRepayTransaction } from '@/hooks/useRepayTransaction'; import { formatBalance, formatReadable } from '@/utils/balance'; import { Market, MarketPosition } from '@/utils/types'; import { TokenIcon } from '../TokenIcon'; import { getLTVColor, getLTVProgressColor } from './helpers'; +import { MarketDetailsBlock } from '@/components/common/MarketDetailsBlock'; type WithdrawCollateralAndRepayProps = { market: Market; @@ -90,11 +90,6 @@ export function WithdrawCollateralAndRepay({ } }, [repayAssets, currentPosition]); - const { price: oraclePrice } = useOraclePrice({ - oracle: market.oracleAddress as `0x${string}`, - chainId: market.morphoBlue.chain.id, - }); - const maxToRepay = useMemo( () => BigInt(currentPosition?.state.borrowAssets ?? 0) > BigInt(loanTokenBalance ?? 0) @@ -110,7 +105,7 @@ export function WithdrawCollateralAndRepay({ } else { // Calculate current LTV from position data const currentCollateralValue = - (BigInt(currentPosition.state.collateral) * oraclePrice) / BigInt(10 ** 36); + (BigInt(currentPosition.state.collateral) * BigInt(market.collateralPrice)) / BigInt(10 ** 36); const currentBorrowValue = BigInt(currentPosition.state.borrowAssets || 0); if (currentCollateralValue > 0) { @@ -120,7 +115,7 @@ export function WithdrawCollateralAndRepay({ setCurrentLTV(BigInt(0)); } } - }, [currentPosition, market, oraclePrice]); + }, [currentPosition, market]); useEffect(() => { if (!currentPosition) return; @@ -129,7 +124,7 @@ export function WithdrawCollateralAndRepay({ const newCollateral = BigInt(currentPosition.state.collateral) - withdrawAmount; const newBorrow = BigInt(currentPosition.state.borrowAssets || 0) - repayAssets; - const newCollateralValueInLoan = (newCollateral * oraclePrice) / BigInt(10 ** 36); + const newCollateralValueInLoan = (newCollateral * BigInt(market.collateralPrice)) / BigInt(10 ** 36); if (newCollateralValueInLoan > 0) { const ltv = (newBorrow * BigInt(10 ** 18)) / newCollateralValueInLoan; @@ -137,14 +132,7 @@ export function WithdrawCollateralAndRepay({ } else { setNewLTV(BigInt(0)); } - }, [currentPosition, withdrawAmount, repayAssets, market, oraclePrice]); - - const formattedOraclePrice = useMemo(() => { - const adjusted = - (oraclePrice * BigInt(10 ** market.collateralAsset.decimals)) / - BigInt(10 ** market.loanAsset.decimals); - return formatBalance(adjusted, 36); - }, [oraclePrice, market.collateralAsset.decimals, market.loanAsset.decimals]); + }, [currentPosition, withdrawAmount, repayAssets, market]); // Function to refresh position data const handleRefreshPosition = () => { @@ -165,7 +153,7 @@ export function WithdrawCollateralAndRepay({ {/* Position Overview Box with dynamic LTV */}
- Position Overview + My Borrow
- {/* Market Stats */} -
-
Market Stats
- -
-

APY:

-

- {(market.state.borrowApy * 100).toFixed(2)}% -

- -

Available Liquidity:

-

- {formatReadable( - formatBalance(market.state.liquidityAssets, market.loanAsset.decimals), - )} -

- -

Utilization:

-

- {formatReadable(market.state.utilization * 100)}% -

-
-
- - {/* Oracle Price - compact format */} -
-
- - Oracle Price: {market.collateralAsset.symbol}/{market.loanAsset.symbol} - - - {formattedOraclePrice.toFixed(4)} {market.loanAsset.symbol} - -
-
- {isConnected && ( <> {/* Withdraw Input Section */} -
+

Withdraw Collateral

diff --git a/src/components/BorrowModal.tsx b/src/components/BorrowModal.tsx index 7f47c01b..4893b333 100644 --- a/src/components/BorrowModal.tsx +++ b/src/components/BorrowModal.tsx @@ -1,11 +1,11 @@ import React, { useState } from 'react'; import { Cross1Icon } from '@radix-ui/react-icons'; +import { FaArrowRightArrowLeft } from 'react-icons/fa6'; import { useAccount, useBalance } from 'wagmi'; import useUserPosition from '@/hooks/useUserPosition'; import { Market } from '@/utils/types'; import { AddCollateralAndBorrow } from './Borrow/AddCollateralAndBorrow'; import { WithdrawCollateralAndRepay } from './Borrow/WithdrawCollateralAndRepay'; -import ButtonGroup from './ButtonGroup'; import { TokenIcon } from './TokenIcon'; type BorrowModalProps = { @@ -15,7 +15,6 @@ type BorrowModalProps = { export function BorrowModal({ market, onClose }: BorrowModalProps): JSX.Element { const [mode, setMode] = useState<'borrow' | 'repay'>('borrow'); - const { address: account } = useAccount(); // Get user positions to calculate current LTV @@ -43,10 +42,10 @@ export function BorrowModal({ market, onClose }: BorrowModalProps): JSX.Element chainId: market.morphoBlue.chain.id, }); - const modeOptions = [ - { key: 'borrow', label: 'Borrow', value: 'borrow' }, - { key: 'repay', label: 'Repay', value: 'repay' }, - ]; + const hasPosition = currentPosition && ( + BigInt(currentPosition.state.borrowAssets) > 0n || + BigInt(currentPosition.state.collateral) > 0n + ); return (

-
-
- - {market.loanAsset.symbol} Position +
+
+
+
+ +
+ +
+
+
+
+ {market.loanAsset.symbol} + / {market.collateralAsset.symbol} +
+ + {mode === 'borrow' ? 'Borrow against collateral' : 'Repay borrowed assets'} + +
+
- setMode(value as 'borrow' | 'repay')} - variant="default" - size="sm" - /> + {hasPosition && ( + + )}
{mode === 'borrow' ? ( diff --git a/src/components/SupplyModalContent.tsx b/src/components/SupplyModalContent.tsx index f4a3634d..be6f98cc 100644 --- a/src/components/SupplyModalContent.tsx +++ b/src/components/SupplyModalContent.tsx @@ -1,22 +1,17 @@ import React from 'react'; import { Switch } from '@nextui-org/react'; -import { ExternalLinkIcon } from '@radix-ui/react-icons'; import { useAccount } from 'wagmi'; import Input from '@/components/Input/Input'; import AccountConnect from '@/components/layout/header/AccountConnect'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { useSupplyMarket } from '@/hooks/useSupplyMarket'; -import { formatBalance, formatReadable } from '@/utils/balance'; -import { getExplorerURL } from '@/utils/external'; -import { getIRMTitle } from '@/utils/morpho'; +import { formatBalance } from '@/utils/balance'; import { isWETH } from '@/utils/tokens'; import { Market } from '@/utils/types'; import { Button } from './common'; -import { MarketInfoBlock } from './common/MarketInfoBlock'; -import OracleVendorBadge from './OracleVendorBadge'; import { SupplyProcessModal } from './SupplyProcessModal'; -import { TokenIcon } from './TokenIcon'; +import useUserPosition from '@/hooks/useUserPosition'; type SupplyModalContentProps = { market: Market; @@ -27,6 +22,14 @@ type SupplyModalContentProps = { export function SupplyModalContent({ market, onClose, isMarketPage }: SupplyModalContentProps): JSX.Element { const [usePermit2Setting] = useLocalStorage('usePermit2', true); const { isConnected } = useAccount(); + const { address: account } = useAccount(); + + // Get user position + const { position } = useUserPosition( + account, + market.morphoBlue.chain.id, + market.uniqueKey, + ); // Use the hook to handle all supply logic const { @@ -58,12 +61,7 @@ export function SupplyModalContent({ market, onClose, isMarketPage }: SupplyModa <> {showProcessModal && ( setShowProcessModal(false)} tokenSymbol={market.loanAsset.symbol} @@ -73,87 +71,17 @@ export function SupplyModalContent({ market, onClose, isMarketPage }: SupplyModa )} {!showProcessModal && (
- {!isMarketPage && ( + {!isConnected ? ( +
+ +
+ ) : ( <> - - -
-
Market Config
- - - -
-

Oracle:

- - - -
- -
- -
-
Market State
- -
-

Total Supply:

- -

- {formatReadable( - formatBalance(market.state.supplyAssets, market.loanAsset.decimals), - )} -

-
- -
-

Liquidity:

-

- {formatReadable( - formatBalance(market.state.liquidityAssets, market.loanAsset.decimals), - )} -

-
- -
-

Utilization:

-

- {formatReadable(market.state.utilization * 100)}% -

-
-
- - )} - - {isConnected ? ( -
- {isWETH(market.loanAsset.address, market.morphoBlue.chain.id) && ( -
-
-
-
Use ETH instead
+ {/* Supply Input Section */} +
+ {isWETH(market.loanAsset.address, market.morphoBlue.chain.id) && ( +
+
Use ETH instead
+ )} + +
+
+ Supply amount +

+ Balance: {useEth + ? formatBalance(ethBalance ?? BigInt(0), 18) + : formatBalance(tokenBalance ?? BigInt(0), market.loanAsset.decimals) + } {useEth ? 'ETH' : market.loanAsset.symbol} +

+
+ +
+
+ string | null)) => { + if (typeof error === 'string' && !error.includes("You don't have any supplied assets")) { + setInputError(error); + } else { + setInputError(null); + } + }} + exceedMaxErrMessage={supplyAmount && supplyAmount > (useEth ? ethBalance ?? BigInt(0) : tokenBalance ?? BigInt(0)) ? "Insufficient Balance" : undefined} + /> + {inputError && !inputError.includes("You don't have any supplied assets") && ( +

+ {inputError} +

+ )} +
+ + {needSwitchChain ? ( + + ) : (!permit2Authorized && !useEth) || (!usePermit2Setting && !isApproved) ? ( + + ) : ( + + )} +
- )} -
- ) : ( -
-
-
-
+ )} - -
- Supply amount - {isConnected && ( -

- Balance: {useEth - ? formatBalance(ethBalance ?? BigInt(0), 18) - : formatBalance(tokenBalance ?? BigInt(0), market.loanAsset.decimals) - } {useEth ? 'ETH' : market.loanAsset.symbol} -

- )} -
- -
-
- string | null)) => { - if (typeof error === 'string' && !error.includes("You don't have any supplied assets")) { - setInputError(error); - } else { - setInputError(null); - } - }} - exceedMaxErrMessage={supplyAmount && supplyAmount > (useEth ? ethBalance ?? BigInt(0) : tokenBalance ?? BigInt(0)) ? "Insufficient Balance" : undefined} - /> - {inputError && !inputError.includes("You don't have any supplied assets") && ( -

- {inputError} -

- )} -
- - {needSwitchChain ? ( - - ) : (!permit2Authorized && !useEth) || (!usePermit2Setting && !isApproved) ? ( - - ) : ( - - )} -
)} diff --git a/src/components/SupplyModalV2.tsx b/src/components/SupplyModalV2.tsx index a84b6bad..21c70d0c 100644 --- a/src/components/SupplyModalV2.tsx +++ b/src/components/SupplyModalV2.tsx @@ -3,10 +3,11 @@ import { Cross1Icon } from '@radix-ui/react-icons'; import { FaArrowRightArrowLeft } from 'react-icons/fa6'; import { useAccount, useBalance } from 'wagmi'; import { Market, MarketPosition } from '@/utils/types'; -import { Button } from './common'; +import { MarketDetailsBlock } from './common/MarketDetailsBlock'; import { TokenIcon } from './TokenIcon'; import { SupplyModalContent } from './SupplyModalContent'; import { WithdrawModalContent } from './WithdrawModalContent'; +import { formatBalance } from '@/utils/balance'; type SupplyModalV2Props = { market: Market; @@ -27,6 +28,8 @@ export function SupplyModalV2({ market, position, onClose, refetch, isMarketPage chainId: market.morphoBlue.chain.id, }); + const hasPosition = position && BigInt(position.state.supplyAssets) > 0n; + return (
@@ -49,47 +52,61 @@ export function SupplyModalV2({ market, position, onClose, refetch, isMarketPage width={20} height={20} /> - {market.loanAsset.symbol} + {mode === 'supply' ? 'Supply' : 'Withdraw'} {market.loanAsset.symbol}
- {mode === 'supply' ? 'Supply Position' : 'Withdraw Position'} + {mode === 'supply' ? 'Supply to earn interest' : 'Withdraw your supplied assets'}
- + {hasPosition && ( + + )}
- {/* Position Overview */} -
-
Position Overview
-
-
-

Current Supply

-
- -

- {position ? position.state.supplyAssets : '0'} {market.loanAsset.symbol} -

+ {/* Current Position Section */} + {position && BigInt(position.state.supplyAssets) > 0n && ( +
+
+ My Supply +
+
+

Current Supply

+
+ +

+ {formatBalance(position.state.supplyAssets, market.loanAsset.decimals)}{' '} + {market.loanAsset.symbol} +

+
+
-
-
-

Supply APY

-

{(market.state.supplyApy * 100).toFixed(2)}%

-
-
+ )} + + + {/* Market Details Block - includes position overview and collapsible details */} +
+
{mode === 'supply' ? ( diff --git a/src/components/WithdrawModalContent.tsx b/src/components/WithdrawModalContent.tsx index 194b50ed..98f71c49 100644 --- a/src/components/WithdrawModalContent.tsx +++ b/src/components/WithdrawModalContent.tsx @@ -1,10 +1,8 @@ // Import the necessary hooks import { useCallback, useState } from 'react'; - import { Address, encodeFunctionData } from 'viem'; import { useAccount } from 'wagmi'; import morphoAbi from '@/abis/morpho'; -import { MarketInfoBlock } from '@/components/common/MarketInfoBlock'; import Input from '@/components/Input/Input'; import AccountConnect from '@/components/layout/header/AccountConnect'; import { useMarketNetwork } from '@/hooks/useMarketNetwork'; @@ -14,7 +12,6 @@ import { formatBalance, formatReadable, min } from '@/utils/balance'; import { MORPHO } from '@/utils/morpho'; import { Market, MarketPosition } from '@/utils/types'; import { Button } from './common'; -import { TokenIcon } from './TokenIcon'; type WithdrawModalContentProps = { position?: MarketPosition; @@ -117,63 +114,68 @@ export function WithdrawModalContent({ position, market, onClose, refetch, isMar return (
- {!isMarketPage && ( + {!isConnected ? ( +
+ +
+ ) : ( <> - - - )} - - {!isConnected && ( -
-
- + {/* Withdraw Input Section */} +
+
+
+ Withdraw amount +
+

+ Available: {formatReadable( + formatBalance(position?.state.supplyAssets ?? BigInt(0), activeMarket.loanAsset.decimals) + )} {activeMarket.loanAsset.symbol} +

+
+
+ +
+
+ + {inputError && ( +

+ {inputError} +

+ )} +
+ {needSwitchChain ? ( + + ) : ( + + )} +
+
-
+ )} - -
- Withdraw amount -

- Market Liquidity: {formatReadable( - formatBalance(activeMarket.state.liquidityAssets, activeMarket.loanAsset.decimals) - )} {activeMarket.loanAsset.symbol} -

-
- -
-
- - {inputError &&

{inputError}

} -
- {needSwitchChain ? ( - - ) : ( - - )} -
); } diff --git a/src/components/common/MarketDetailsBlock.tsx b/src/components/common/MarketDetailsBlock.tsx new file mode 100644 index 00000000..6e4e028e --- /dev/null +++ b/src/components/common/MarketDetailsBlock.tsx @@ -0,0 +1,139 @@ +import React, { useState } from 'react'; +import { ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon } from '@radix-ui/react-icons'; +import { formatBalance, formatReadable } from '@/utils/balance'; +import { Market, MarketPosition } from '@/utils/types'; +import { TokenIcon } from '../TokenIcon'; +import OracleVendorBadge from '../OracleVendorBadge'; +import { getExplorerURL } from '@/utils/external'; +import { getIRMTitle } from '@/utils/morpho'; + +type MarketDetailsBlockProps = { + market: Market; + position?: MarketPosition; + showPosition?: boolean; + showDetailsLink?: boolean; + defaultCollapsed?: boolean; + mode?: 'supply' | 'borrow'; +}; + +export function MarketDetailsBlock({ + market, + position, + showPosition = false, + showDetailsLink = false, + defaultCollapsed = false, + mode = 'supply' +}: MarketDetailsBlockProps): JSX.Element { + const [isExpanded, setIsExpanded] = useState(!defaultCollapsed); + + // Helper to format APY based on mode + const getAPY = () => { + const apy = mode === 'supply' ? market.state.supplyApy : market.state.borrowApy; + return (apy * 100).toFixed(2); + }; + + return ( +
+ {/* Collapsible Market Details */} +
setIsExpanded(!isExpanded)} + > +
+
+
+
+ +
+
+ +
+
+
+ {market.loanAsset.symbol} + / {market.collateralAsset.symbol} + {showDetailsLink && ( + e.stopPropagation()} + > + + + )} +
+ {!isExpanded && ( +
+ · + + · + {getAPY()}% APY + · + {(Number(market.lltv) / 1e16).toFixed(0)}% LLTV +
+ )} +
+ +
+ {isExpanded ? : } +
+
+ + {/* Expanded Market Details */} + {isExpanded && ( +
+
+
+ + · + {getIRMTitle(market.irmAddress)} +
+
+
+

Market State

+
+
+

{mode === 'supply' ? 'Supply' : 'Borrow'} APY:

+

+ {getAPY()}% +

+
+
+

Total Supply:

+

+ {formatReadable(formatBalance(market.state.supplyAssets, market.loanAsset.decimals))} +

+
+
+

Liquidity:

+

+ {formatReadable(formatBalance(market.state.liquidityAssets, market.loanAsset.decimals))} +

+
+
+

Utilization:

+

+ {formatReadable(market.state.utilization * 100)}% +

+
+
+
+
+ )} +
+
+ ); +} \ No newline at end of file From 803bde9b73cf2854281f0655b701ed83bb974b20 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Thu, 10 Apr 2025 13:30:30 +0800 Subject: [PATCH 03/12] chore: default withdraw and supply --- app/positions/components/PositionsContent.tsx | 1 + .../components/PositionsSummaryTable.tsx | 9 ++-- .../components/SuppliedMarketsDetail.tsx | 23 +++++++--- src/components/SupplyModalV2.tsx | 45 ++++++------------- 4 files changed, 37 insertions(+), 41 deletions(-) diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx index 8e5fb46b..abb2cf02 100644 --- a/app/positions/components/PositionsContent.tsx +++ b/app/positions/components/PositionsContent.tsx @@ -115,6 +115,7 @@ export default function Positions() { }} refetch={() => void refetch()} isMarketPage={false} + defaultMode="withdraw" /> )} diff --git a/app/positions/components/PositionsSummaryTable.tsx b/app/positions/components/PositionsSummaryTable.tsx index 8c2bf2a5..4afcd514 100644 --- a/app/positions/components/PositionsSummaryTable.tsx +++ b/app/positions/components/PositionsSummaryTable.tsx @@ -37,7 +37,8 @@ import { SuppliedMarketsDetail } from './SuppliedMarketsDetail'; type PositionsSummaryTableProps = { account: string; marketPositions: MarketPositionWithEarnings[]; - setShowModal: (show: boolean) => void; + setShowWithdrawModal: (show: boolean) => void; + setShowSupplyModal: (show: boolean) => void; setSelectedPosition: (position: MarketPosition) => void; refetch: (onSuccess?: () => void) => void; isRefetching: boolean; @@ -47,7 +48,8 @@ type PositionsSummaryTableProps = { export function PositionsSummaryTable({ marketPositions, - setShowModal, + setShowWithdrawModal, + setShowSupplyModal, setSelectedPosition, refetch, isRefetching, @@ -328,7 +330,8 @@ export function PositionsSummaryTable({ > diff --git a/app/positions/components/SuppliedMarketsDetail.tsx b/app/positions/components/SuppliedMarketsDetail.tsx index e5f53680..9804e5e8 100644 --- a/app/positions/components/SuppliedMarketsDetail.tsx +++ b/app/positions/components/SuppliedMarketsDetail.tsx @@ -9,10 +9,10 @@ import { TokenIcon } from '@/components/TokenIcon'; import { formatReadable, formatBalance } from '@/utils/balance'; import { MarketPosition, GroupedPosition, WarningWithDetail, WarningCategory } from '@/utils/types'; import { getCollateralColor } from '../utils/colors'; - type SuppliedMarketsDetailProps = { groupedPosition: GroupedPosition; - setShowModal: (show: boolean) => void; + setShowWithdrawModal: (show: boolean) => void; + setShowSupplyModal: (show: boolean) => void; setSelectedPosition: (position: MarketPosition) => void; }; @@ -39,7 +39,8 @@ function WarningTooltip({ warnings }: { warnings: WarningWithDetail[] }) { export function SuppliedMarketsDetail({ groupedPosition, - setShowModal, + setShowWithdrawModal, + setShowSupplyModal, setSelectedPosition, }: SuppliedMarketsDetailProps) { // Sort active markets by size @@ -204,16 +205,26 @@ export function SuppliedMarketsDetail({
diff --git a/src/components/SupplyModalV2.tsx b/src/components/SupplyModalV2.tsx index 21c70d0c..ae8c3102 100644 --- a/src/components/SupplyModalV2.tsx +++ b/src/components/SupplyModalV2.tsx @@ -1,6 +1,5 @@ import React, { useState } from 'react'; -import { Cross1Icon } from '@radix-ui/react-icons'; -import { FaArrowRightArrowLeft } from 'react-icons/fa6'; +import { Cross1Icon, ArrowRightIcon } from '@radix-ui/react-icons'; import { useAccount, useBalance } from 'wagmi'; import { Market, MarketPosition } from '@/utils/types'; import { MarketDetailsBlock } from './common/MarketDetailsBlock'; @@ -15,10 +14,18 @@ type SupplyModalV2Props = { onClose: () => void; refetch?: () => void; isMarketPage?: boolean; + defaultMode?: 'supply' | 'withdraw'; }; -export function SupplyModalV2({ market, position, onClose, refetch, isMarketPage }: SupplyModalV2Props): JSX.Element { - const [mode, setMode] = useState<'supply' | 'withdraw'>('supply'); +export function SupplyModalV2({ + market, + position, + onClose, + refetch, + isMarketPage, + defaultMode = 'supply' +}: SupplyModalV2Props): JSX.Element { + const [mode, setMode] = useState<'supply' | 'withdraw'>(defaultMode); const { address: account } = useAccount(); // Get token balance @@ -63,40 +70,14 @@ export function SupplyModalV2({ market, position, onClose, refetch, isMarketPage )} - {/* Current Position Section */} - {position && BigInt(position.state.supplyAssets) > 0n && ( -
-
- My Supply -
-
-

Current Supply

-
- -

- {formatBalance(position.state.supplyAssets, market.loanAsset.decimals)}{' '} - {market.loanAsset.symbol} -

-
-
-
- )} - - {/* Market Details Block - includes position overview and collapsible details */}
Date: Thu, 10 Apr 2025 13:37:02 +0800 Subject: [PATCH 04/12] chore: unify style --- src/components/Borrow/AddCollateralAndBorrow.tsx | 12 ++++++++++-- .../Borrow/WithdrawCollateralAndRepay.tsx | 14 +++++++++++--- src/components/SupplyModalV2.tsx | 2 -- src/components/common/MarketDetailsBlock.tsx | 9 ++------- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/components/Borrow/AddCollateralAndBorrow.tsx b/src/components/Borrow/AddCollateralAndBorrow.tsx index 0c7428ee..ef9ce9e5 100644 --- a/src/components/Borrow/AddCollateralAndBorrow.tsx +++ b/src/components/Borrow/AddCollateralAndBorrow.tsx @@ -246,8 +246,16 @@ export function AddCollateralAndBorrow({
+ {/* Market Details Block - includes position overview and collapsible details */} +
+ +
+ {isConnected && ( - <> +
{/* Collateral Input Section */}
@@ -322,7 +330,7 @@ export function AddCollateralAndBorrow({
- + )} {/* Action Button */} diff --git a/src/components/Borrow/WithdrawCollateralAndRepay.tsx b/src/components/Borrow/WithdrawCollateralAndRepay.tsx index 1f32085b..793b0540 100644 --- a/src/components/Borrow/WithdrawCollateralAndRepay.tsx +++ b/src/components/Borrow/WithdrawCollateralAndRepay.tsx @@ -12,7 +12,7 @@ import { formatBalance, formatReadable } from '@/utils/balance'; import { Market, MarketPosition } from '@/utils/types'; import { TokenIcon } from '../TokenIcon'; import { getLTVColor, getLTVProgressColor } from './helpers'; -import { MarketDetailsBlock } from '@/components/common/MarketDetailsBlock'; +import { MarketDetailsBlock } from '../common/MarketDetailsBlock'; type WithdrawCollateralAndRepayProps = { market: Market; @@ -250,8 +250,16 @@ export function WithdrawCollateralAndRepay({ + {/* Market Details Block - includes position overview and collapsible details */} +
+ +
+ {isConnected && ( - <> +
{/* Withdraw Input Section */}
@@ -310,7 +318,7 @@ export function WithdrawCollateralAndRepay({
- + )} {/* Action Button */} diff --git a/src/components/SupplyModalV2.tsx b/src/components/SupplyModalV2.tsx index ae8c3102..a10563c3 100644 --- a/src/components/SupplyModalV2.tsx +++ b/src/components/SupplyModalV2.tsx @@ -82,8 +82,6 @@ export function SupplyModalV2({
{/* Collapsible Market Details */}
setIsExpanded(!isExpanded)} >
From f79dca443509aeba107a0387a613cfaf4c706e5a Mon Sep 17 00:00:00 2001 From: antoncoding Date: Thu, 10 Apr 2025 14:24:35 +0800 Subject: [PATCH 05/12] fix: chain id --- .../Borrow/AddCollateralAndBorrow.tsx | 8 +- .../Borrow/WithdrawCollateralAndRepay.tsx | 1 + src/components/BorrowModal.tsx | 2 +- src/components/SupplyModalContent.tsx | 6 +- src/components/SupplyModalV2.tsx | 8 +- src/components/WithdrawModalContent.tsx | 2 +- src/components/common/MarketDetailsBlock.tsx | 100 ++++++++++-------- src/hooks/useMarketNetwork.ts | 6 +- 8 files changed, 76 insertions(+), 57 deletions(-) diff --git a/src/components/Borrow/AddCollateralAndBorrow.tsx b/src/components/Borrow/AddCollateralAndBorrow.tsx index ef9ce9e5..2d34a697 100644 --- a/src/components/Borrow/AddCollateralAndBorrow.tsx +++ b/src/components/Borrow/AddCollateralAndBorrow.tsx @@ -59,6 +59,8 @@ export function AddCollateralAndBorrow({ targetChainId: market.morphoBlue.chain.id, }); + console.log('needSwitchChain', needSwitchChain) + // Use the new hook for borrow transaction logic const { currentStep, @@ -251,6 +253,7 @@ export function AddCollateralAndBorrow({
@@ -369,15 +372,14 @@ export function AddCollateralAndBorrow({ borrowPending || collateralInputError !== null || borrowInputError !== null || - collateralAmount === BigInt(0) || - borrowAmount === BigInt(0) || + (collateralAmount === BigInt(0) && borrowAmount === BigInt(0)) || newLTV >= lltv } onClick={() => void signAndBorrow()} className="min-w-32" variant="cta" > - {useEth ? 'Borrow' : 'Sign and Borrow'} + {collateralAmount > 0n && borrowAmount == 0n ? 'Add Collateral' : 'Borrow'} )}
diff --git a/src/components/Borrow/WithdrawCollateralAndRepay.tsx b/src/components/Borrow/WithdrawCollateralAndRepay.tsx index 793b0540..cf1c74b8 100644 --- a/src/components/Borrow/WithdrawCollateralAndRepay.tsx +++ b/src/components/Borrow/WithdrawCollateralAndRepay.tsx @@ -255,6 +255,7 @@ export function WithdrawCollateralAndRepay({
diff --git a/src/components/BorrowModal.tsx b/src/components/BorrowModal.tsx index 4893b333..a67f88c6 100644 --- a/src/components/BorrowModal.tsx +++ b/src/components/BorrowModal.tsx @@ -56,7 +56,7 @@ export function BorrowModal({ market, onClose }: BorrowModalProps): JSX.Element
) : ( )}
diff --git a/src/components/SupplyModalV2.tsx b/src/components/SupplyModalV2.tsx index a10563c3..7775388d 100644 --- a/src/components/SupplyModalV2.tsx +++ b/src/components/SupplyModalV2.tsx @@ -1,12 +1,12 @@ import React, { useState } from 'react'; -import { Cross1Icon, ArrowRightIcon } from '@radix-ui/react-icons'; +import { Cross1Icon } from '@radix-ui/react-icons'; +import { FaArrowRightArrowLeft } from 'react-icons/fa6'; import { useAccount, useBalance } from 'wagmi'; import { Market, MarketPosition } from '@/utils/types'; import { MarketDetailsBlock } from './common/MarketDetailsBlock'; import { TokenIcon } from './TokenIcon'; import { SupplyModalContent } from './SupplyModalContent'; import { WithdrawModalContent } from './WithdrawModalContent'; -import { formatBalance } from '@/utils/balance'; type SupplyModalV2Props = { market: Market; @@ -43,7 +43,7 @@ export function SupplyModalV2({
)} diff --git a/src/components/WithdrawModalContent.tsx b/src/components/WithdrawModalContent.tsx index 98f71c49..009350ae 100644 --- a/src/components/WithdrawModalContent.tsx +++ b/src/components/WithdrawModalContent.tsx @@ -121,7 +121,7 @@ export function WithdrawModalContent({ position, market, onClose, refetch, isMar ) : ( <> {/* Withdraw Input Section */} -
+
Withdraw amount diff --git a/src/components/common/MarketDetailsBlock.tsx b/src/components/common/MarketDetailsBlock.tsx index 2717e144..bb9540a0 100644 --- a/src/components/common/MarketDetailsBlock.tsx +++ b/src/components/common/MarketDetailsBlock.tsx @@ -1,10 +1,12 @@ import React, { useState } from 'react'; import { ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon } from '@radix-ui/react-icons'; +import { motion, AnimatePresence } from 'framer-motion'; import { formatBalance, formatReadable } from '@/utils/balance'; import { Market } from '@/utils/types'; import { TokenIcon } from '../TokenIcon'; import OracleVendorBadge from '../OracleVendorBadge'; import { getIRMTitle } from '@/utils/morpho'; +import { formatUnits } from 'viem'; type MarketDetailsBlockProps = { market: Market; @@ -36,23 +38,23 @@ export function MarketDetailsBlock({ >
-
+
-
+
@@ -88,46 +90,58 @@ export function MarketDetailsBlock({
{/* Expanded Market Details */} - {isExpanded && ( -
-
-
- - · - {getIRMTitle(market.irmAddress)} -
-
-
-

Market State

-
-
-

{mode === 'supply' ? 'Supply' : 'Borrow'} APY:

-

- {getAPY()}% -

-
-
-

Total Supply:

-

- {formatReadable(formatBalance(market.state.supplyAssets, market.loanAsset.decimals))} -

+ + {isExpanded && ( + +
+
+
+ + · + {getIRMTitle(market.irmAddress)} + · + {formatUnits(BigInt(market.lltv), 16)}% +
-
-

Liquidity:

-

- {formatReadable(formatBalance(market.state.liquidityAssets, market.loanAsset.decimals))} -

-
-
-

Utilization:

-

- {formatReadable(market.state.utilization * 100)}% -

+
+

Market State

+
+
+

{mode === 'supply' ? 'Supply' : 'Borrow'} APY:

+

+ {getAPY()}% +

+
+
+

Total Supply:

+

+ {formatReadable(formatBalance(market.state.supplyAssets, market.loanAsset.decimals))} +

+
+
+

Liquidity:

+

+ {formatReadable(formatBalance(market.state.liquidityAssets, market.loanAsset.decimals))} +

+
+
+

Utilization:

+

+ {formatReadable(market.state.utilization * 100)}% +

+
+
-
-
- )} + + )} +
); diff --git a/src/hooks/useMarketNetwork.ts b/src/hooks/useMarketNetwork.ts index b3954e96..d05f0d6b 100644 --- a/src/hooks/useMarketNetwork.ts +++ b/src/hooks/useMarketNetwork.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react'; -import { useAccount, useSwitchChain } from 'wagmi'; +import { useChainId, useSwitchChain } from 'wagmi'; import { useStyledToast } from '@/hooks/useStyledToast'; type UseMarketNetworkProps = { @@ -40,11 +40,13 @@ export function useMarketNetwork({ targetChainId, onNetworkSwitched, }: UseMarketNetworkProps): UseMarketNetworkReturn { - const { chainId } = useAccount(); + const chainId = useChainId(); const { switchChain } = useSwitchChain(); const toast = useStyledToast(); // Check if chain switch is needed + console.log('chainId', chainId) + console.log('targetChainId', targetChainId) const needSwitchChain = useMemo(() => chainId !== targetChainId, [chainId, targetChainId]); // Function to switch to the target network From adab5b40fbca4595ed58b41c9c1cf49c0167d94c Mon Sep 17 00:00:00 2001 From: antoncoding Date: Thu, 10 Apr 2025 14:37:26 +0800 Subject: [PATCH 06/12] fix: borrow bug --- .../Borrow/AddCollateralAndBorrow.tsx | 4 +- src/components/Input/Input.tsx | 13 ++- src/hooks/useBorrowTransaction.ts | 83 +++++++++++-------- src/hooks/useMarketNetwork.ts | 2 - 4 files changed, 61 insertions(+), 41 deletions(-) diff --git a/src/components/Borrow/AddCollateralAndBorrow.tsx b/src/components/Borrow/AddCollateralAndBorrow.tsx index 2d34a697..04a52488 100644 --- a/src/components/Borrow/AddCollateralAndBorrow.tsx +++ b/src/components/Borrow/AddCollateralAndBorrow.tsx @@ -59,8 +59,6 @@ export function AddCollateralAndBorrow({ targetChainId: market.morphoBlue.chain.id, }); - console.log('needSwitchChain', needSwitchChain) - // Use the new hook for borrow transaction logic const { currentStep, @@ -298,6 +296,7 @@ export function AddCollateralAndBorrow({ setValue={setCollateralAmount} setError={setCollateralInputError} exceedMaxErrMessage="Insufficient Balance" + value={collateralAmount} /> {collateralInputError && (

{collateralInputError}

@@ -326,6 +325,7 @@ export function AddCollateralAndBorrow({ setValue={setBorrowAmount} setError={setBorrowInputError} exceedMaxErrMessage="Exceeds available liquidity" + value={borrowAmount} /> {borrowInputError && (

{borrowInputError}

diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 73bd4a1a..02567752 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useState, useEffect } from 'react'; import { parseUnits } from 'viem'; import { formatBalance } from '@/utils/balance'; @@ -11,6 +11,7 @@ type InputProps = { exceedMaxErrMessage?: string; allowExceedMax?: boolean; // whether to still "setValue" when the input exceeds max onMaxClick?: () => void; + value?: bigint; }; export default function Input({ @@ -21,9 +22,17 @@ export default function Input({ exceedMaxErrMessage, allowExceedMax = false, onMaxClick, + value, }: InputProps): JSX.Element { // State for the input text - const [inputAmount, setInputAmount] = useState('0'); + const [inputAmount, setInputAmount] = useState(value ? formatBalance(value, decimals).toString() : '0'); + + // Update input text when value prop changes + useEffect(() => { + if (value !== undefined) { + setInputAmount(formatBalance(value, decimals).toString()); + } + }, [value, decimals]); const onInputChange = useCallback( (e: React.ChangeEvent) => { diff --git a/src/hooks/useBorrowTransaction.ts b/src/hooks/useBorrowTransaction.ts index 9451133a..5024f725 100644 --- a/src/hooks/useBorrowTransaction.ts +++ b/src/hooks/useBorrowTransaction.ts @@ -69,48 +69,52 @@ export function useBorrowTransaction({ // Core transaction execution logic const executeBorrowTransaction = useCallback(async () => { - const minSharesToBorrow = + const minSharesToBorrow = borrowAmount === 0n ? 0n : (borrowAmount * BigInt(market.state.supplyShares)) / BigInt(market.state.supplyAssets) - 1n; try { const txs: `0x${string}`[] = []; - if (useEth) { - txs.push( - encodeFunctionData({ + if (collateralAmount > 0n) { + if (useEth) { + txs.push( + encodeFunctionData({ + abi: morphoBundlerAbi, + functionName: 'wrapNative', + args: [collateralAmount], + }), + ); + } else if (usePermit2Setting) { + const { sigs, permitSingle } = await signForBundlers(); + console.log('Signed for bundlers:', { sigs, permitSingle }); + + const tx1 = encodeFunctionData({ abi: morphoBundlerAbi, - functionName: 'wrapNative', - args: [collateralAmount], - }), - ); - } else if (usePermit2Setting) { - const { sigs, permitSingle } = await signForBundlers(); - console.log('Signed for bundlers:', { sigs, permitSingle }); - - const tx1 = encodeFunctionData({ - abi: morphoBundlerAbi, - functionName: 'approve2', - args: [permitSingle, sigs, false], - }); - - // transferFrom with permit2 - const tx2 = encodeFunctionData({ - abi: morphoBundlerAbi, - functionName: 'transferFrom2', - args: [market.collateralAsset.address as Address, collateralAmount], - }); + functionName: 'approve2', + args: [permitSingle, sigs, false], + }); - txs.push(tx1); - txs.push(tx2); - } else { - // For standard ERC20 flow, we only need to transfer the tokens - txs.push( - encodeFunctionData({ + // transferFrom with permit2 + const tx2 = encodeFunctionData({ abi: morphoBundlerAbi, - functionName: 'erc20TransferFrom', + functionName: 'transferFrom2', args: [market.collateralAsset.address as Address, collateralAmount], - }), - ); + }); + + txs.push(tx1); + txs.push(tx2); + + } else { + // For standard ERC20 flow, we only need to transfer the tokens + txs.push( + encodeFunctionData({ + abi: morphoBundlerAbi, + functionName: 'erc20TransferFrom', + args: [market.collateralAsset.address as Address, collateralAmount], + }), + ); + } + } setCurrentStep('borrowing'); @@ -152,8 +156,17 @@ export function useBorrowTransaction({ ], }); - txs.push(morphoAddCollat); - txs.push(morphoBorrowTx); + console.log('morphoBorrowTx', morphoBorrowTx) + + if (collateralAmount > 0n) { + txs.push(morphoAddCollat); + } + + if (borrowAmount > 0n) { + txs.push(morphoBorrowTx); + } + + console.log('txs', txs.length) // add timeout here to prevent rabby reverting await new Promise((resolve) => setTimeout(resolve, 800)); diff --git a/src/hooks/useMarketNetwork.ts b/src/hooks/useMarketNetwork.ts index d05f0d6b..f1d1dba1 100644 --- a/src/hooks/useMarketNetwork.ts +++ b/src/hooks/useMarketNetwork.ts @@ -45,8 +45,6 @@ export function useMarketNetwork({ const toast = useStyledToast(); // Check if chain switch is needed - console.log('chainId', chainId) - console.log('targetChainId', targetChainId) const needSwitchChain = useMemo(() => chainId !== targetChainId, [chainId, targetChainId]); // Function to switch to the target network From d6f7ce24d4cca3e6093a2dbdffa2c4b2ff13e783 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Thu, 10 Apr 2025 15:18:49 +0800 Subject: [PATCH 07/12] chore: lint --- app/market/[chainId]/[marketid]/content.tsx | 6 +- app/markets/components/markets.tsx | 5 +- app/positions/components/PositionsContent.tsx | 1 - docs/Styling.md | 2 +- .../Borrow/AddCollateralAndBorrow.tsx | 21 ++-- .../Borrow/WithdrawCollateralAndRepay.tsx | 18 ++-- src/components/BorrowModal.tsx | 13 ++- src/components/Input/Input.tsx | 4 +- src/components/SupplyModalContent.tsx | 62 +++++++----- src/components/SupplyModalV2.tsx | 47 ++++----- src/components/WithdrawModalContent.tsx | 96 ++++++++++--------- src/components/common/MarketDetailsBlock.tsx | 58 +++++++---- src/components/index.ts | 2 +- src/hooks/useBorrowTransaction.ts | 13 +-- src/hooks/useRepayTransaction.ts | 24 ++--- 15 files changed, 199 insertions(+), 173 deletions(-) diff --git a/app/market/[chainId]/[marketid]/content.tsx b/app/market/[chainId]/[marketid]/content.tsx index 6e1f39fe..d6cdea54 100644 --- a/app/market/[chainId]/[marketid]/content.tsx +++ b/app/market/[chainId]/[marketid]/content.tsx @@ -16,6 +16,7 @@ import OracleVendorBadge from '@/components/OracleVendorBadge'; import { SupplyModalV2 } from '@/components/SupplyModalV2'; import { TokenIcon } from '@/components/TokenIcon'; import { useMarket, useMarketHistoricalData } from '@/hooks/useMarket'; +import { useOraclePrice } from '@/hooks/useOraclePrice'; import MORPHO_LOGO from '@/imgs/tokens/morpho.svg'; import { getExplorerURL, getMarketURL } from '@/utils/external'; import { getIRMTitle } from '@/utils/morpho'; @@ -26,8 +27,6 @@ import { LiquidationsTable } from './components/LiquidationsTable'; import { SuppliesTable } from './components/SuppliesTable'; import RateChart from './RateChart'; import VolumeChart from './VolumeChart'; -import { useOraclePrice } from '@/hooks/useOraclePrice'; -import { useAccount } from 'wagmi'; const NOW = Math.floor(Date.now() / 1000); const WEEK_IN_SECONDS = 7 * 24 * 60 * 60; @@ -37,7 +36,6 @@ function MarketContent() { const { marketid, chainId } = useParams(); const router = useRouter(); const searchParams = useSearchParams(); - const { address: account } = useAccount(); // 2. Network setup const network = Number(chainId as string) as SupportedNetworks; @@ -295,7 +293,7 @@ function MarketContent() {
Live Price: - + {Number(formattedOraclePrice).toFixed(4)} {market.loanAsset.symbol}
diff --git a/app/markets/components/markets.tsx b/app/markets/components/markets.tsx index bec31dd9..27cf1436 100644 --- a/app/markets/components/markets.tsx +++ b/app/markets/components/markets.tsx @@ -362,10 +362,7 @@ export default function Markets() {

Markets

{showSupplyModal && selectedMarket && ( - setShowSupplyModal(false)} - /> + setShowSupplyModal(false)} /> )} void) => void; - loanTokenBalance: bigint | undefined; collateralTokenBalance: bigint | undefined; ethBalance: bigint | undefined; }; @@ -31,7 +29,6 @@ export function AddCollateralAndBorrow({ market, currentPosition, refetchPosition, - loanTokenBalance, collateralTokenBalance, ethBalance, }: BorrowLogicProps): JSX.Element { @@ -85,7 +82,8 @@ export function AddCollateralAndBorrow({ } else { // Calculate current LTV from position data const currentCollateralValue = - (BigInt(currentPosition.state.collateral) * BigInt(market.collateralPrice)) / BigInt(10 ** 36); + (BigInt(currentPosition.state.collateral) * BigInt(market.collateralPrice)) / + BigInt(10 ** 36); const currentBorrowValue = BigInt(currentPosition.state.borrowAssets || 0); if (currentCollateralValue > 0) { @@ -102,7 +100,8 @@ export function AddCollateralAndBorrow({ const newCollateral = BigInt(currentPosition?.state.collateral ?? 0) + collateralAmount; const newBorrow = BigInt(currentPosition?.state.borrowAssets ?? 0) + borrowAmount; - const newCollateralValueInLoan = (newCollateral * BigInt(market.collateralPrice)) / BigInt(10 ** 36); + const newCollateralValueInLoan = + (newCollateral * BigInt(market.collateralPrice)) / BigInt(10 ** 36); if (newCollateralValueInLoan > 0) { const ltv = (newBorrow * BigInt(10 ** 18)) / newCollateralValueInLoan; @@ -248,15 +247,11 @@ export function AddCollateralAndBorrow({ {/* Market Details Block - includes position overview and collapsible details */}
- +
{isConnected && ( -
+
{/* Collateral Input Section */}
diff --git a/src/components/Borrow/WithdrawCollateralAndRepay.tsx b/src/components/Borrow/WithdrawCollateralAndRepay.tsx index cf1c74b8..fb7fb500 100644 --- a/src/components/Borrow/WithdrawCollateralAndRepay.tsx +++ b/src/components/Borrow/WithdrawCollateralAndRepay.tsx @@ -8,11 +8,11 @@ import AccountConnect from '@/components/layout/header/AccountConnect'; import { RepayProcessModal } from '@/components/RepayProcessModal'; import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { useRepayTransaction } from '@/hooks/useRepayTransaction'; -import { formatBalance, formatReadable } from '@/utils/balance'; +import { formatBalance } from '@/utils/balance'; import { Market, MarketPosition } from '@/utils/types'; +import { MarketDetailsBlock } from '../common/MarketDetailsBlock'; import { TokenIcon } from '../TokenIcon'; import { getLTVColor, getLTVProgressColor } from './helpers'; -import { MarketDetailsBlock } from '../common/MarketDetailsBlock'; type WithdrawCollateralAndRepayProps = { market: Market; @@ -105,7 +105,8 @@ export function WithdrawCollateralAndRepay({ } else { // Calculate current LTV from position data const currentCollateralValue = - (BigInt(currentPosition.state.collateral) * BigInt(market.collateralPrice)) / BigInt(10 ** 36); + (BigInt(currentPosition.state.collateral) * BigInt(market.collateralPrice)) / + BigInt(10 ** 36); const currentBorrowValue = BigInt(currentPosition.state.borrowAssets || 0); if (currentCollateralValue > 0) { @@ -124,7 +125,8 @@ export function WithdrawCollateralAndRepay({ const newCollateral = BigInt(currentPosition.state.collateral) - withdrawAmount; const newBorrow = BigInt(currentPosition.state.borrowAssets || 0) - repayAssets; - const newCollateralValueInLoan = (newCollateral * BigInt(market.collateralPrice)) / BigInt(10 ** 36); + const newCollateralValueInLoan = + (newCollateral * BigInt(market.collateralPrice)) / BigInt(10 ** 36); if (newCollateralValueInLoan > 0) { const ltv = (newBorrow * BigInt(10 ** 18)) / newCollateralValueInLoan; @@ -252,15 +254,11 @@ export function WithdrawCollateralAndRepay({ {/* Market Details Block - includes position overview and collapsible details */}
- +
{isConnected && ( -
+
{/* Withdraw Input Section */}
diff --git a/src/components/BorrowModal.tsx b/src/components/BorrowModal.tsx index a67f88c6..57926f93 100644 --- a/src/components/BorrowModal.tsx +++ b/src/components/BorrowModal.tsx @@ -42,10 +42,10 @@ export function BorrowModal({ market, onClose }: BorrowModalProps): JSX.Element chainId: market.morphoBlue.chain.id, }); - const hasPosition = currentPosition && ( - BigInt(currentPosition.state.borrowAssets) > 0n || - BigInt(currentPosition.state.collateral) > 0n - ); + const hasPosition = + currentPosition && + (BigInt(currentPosition.state.borrowAssets) > 0n || + BigInt(currentPosition.state.collateral) > 0n); return (
) : (!permit2Authorized && !useEth) || (!usePermit2Setting && !isApproved) ? ( @@ -139,7 +155,9 @@ export function SupplyModalContent({ market, onClose, isMarketPage }: SupplyModa ) : (
{mode === 'supply' ? 'Supply to earn interest' : 'Withdraw your supplied assets'} @@ -80,31 +76,26 @@ export function SupplyModalV2({ {/* Market Details Block - includes position overview and collapsible details */}
-
{mode === 'supply' ? ( - + {})} /> ) : ( {})} - isMarketPage={isMarketPage} /> )}
); -} \ No newline at end of file +} diff --git a/src/components/WithdrawModalContent.tsx b/src/components/WithdrawModalContent.tsx index 009350ae..c315902c 100644 --- a/src/components/WithdrawModalContent.tsx +++ b/src/components/WithdrawModalContent.tsx @@ -18,45 +18,42 @@ type WithdrawModalContentProps = { market?: Market; onClose: () => void; refetch: () => void; - isMarketPage?: boolean; }; -export function WithdrawModalContent({ position, market, onClose, refetch, isMarketPage }: WithdrawModalContentProps): JSX.Element { +export function WithdrawModalContent({ + position, + market, + onClose, + refetch, +}: WithdrawModalContentProps): JSX.Element { const toast = useStyledToast(); const [inputError, setInputError] = useState(null); const [withdrawAmount, setWithdrawAmount] = useState(BigInt(0)); - const { address: account, isConnected, chainId } = useAccount(); // Use market from either position or direct prop const activeMarket = position?.market ?? market; - if (!activeMarket) { - return ( -
-

Market data not available

-
- ); - } - const { needSwitchChain, switchToNetwork } = useMarketNetwork({ - targetChainId: activeMarket.morphoBlue.chain.id, + targetChainId: activeMarket?.morphoBlue.chain.id ?? 0, }); const { isConfirming, sendTransaction } = useTransactionWithToast({ toastId: 'withdraw', - pendingText: `Withdrawing ${formatBalance( - withdrawAmount, - activeMarket.loanAsset.decimals, - )} ${activeMarket.loanAsset.symbol}`, - successText: `${activeMarket.loanAsset.symbol} Withdrawn`, + pendingText: activeMarket + ? `Withdrawing ${formatBalance(withdrawAmount, activeMarket.loanAsset.decimals)} ${ + activeMarket.loanAsset.symbol + }` + : '', + successText: activeMarket ? `${activeMarket.loanAsset.symbol} Withdrawn` : '', errorText: 'Failed to withdraw', chainId, - pendingDescription: `Withdrawing from market ${activeMarket.uniqueKey.slice(2, 8)}...`, - successDescription: `Successfully withdrawn from market ${activeMarket.uniqueKey.slice( - 2, - 8, - )}`, + pendingDescription: activeMarket + ? `Withdrawing from market ${activeMarket.uniqueKey.slice(2, 8)}...` + : '', + successDescription: activeMarket + ? `Successfully withdrawn from market ${activeMarket.uniqueKey.slice(2, 8)}` + : '', onSuccess: () => { refetch(); onClose(); @@ -64,6 +61,11 @@ export function WithdrawModalContent({ position, market, onClose, refetch, isMar }); const withdraw = useCallback(async () => { + if (!activeMarket) { + toast.error('No market', 'Market data not available'); + return; + } + if (!account) { toast.info('No account connected', 'Please connect your wallet to continue.'); return; @@ -101,16 +103,15 @@ export function WithdrawModalContent({ position, market, onClose, refetch, isMar }), chainId: activeMarket.morphoBlue.chain.id, }); - }, [ - account, - activeMarket, - position, - withdrawAmount, - sendTransaction, - position?.state.supplyAssets, - position?.state.supplyShares, - toast, - ]); + }, [account, activeMarket, position, withdrawAmount, sendTransaction, toast]); + + if (!activeMarket) { + return ( +
+

Market data not available

+
+ ); + } return (
@@ -121,15 +122,20 @@ export function WithdrawModalContent({ position, market, onClose, refetch, isMar ) : ( <> {/* Withdraw Input Section */} -
+
Withdraw amount

- Available: {formatReadable( - formatBalance(position?.state.supplyAssets ?? BigInt(0), activeMarket.loanAsset.decimals) - )} {activeMarket.loanAsset.symbol} + Available:{' '} + {formatReadable( + formatBalance( + position?.state.supplyAssets ?? BigInt(0), + activeMarket.loanAsset.decimals, + ), + )}{' '} + {activeMarket.loanAsset.symbol}

@@ -138,10 +144,14 @@ export function WithdrawModalContent({ position, market, onClose, refetch, isMar
{needSwitchChain ? ( - ) : ( diff --git a/src/components/common/MarketDetailsBlock.tsx b/src/components/common/MarketDetailsBlock.tsx index bb9540a0..e89b9aa9 100644 --- a/src/components/common/MarketDetailsBlock.tsx +++ b/src/components/common/MarketDetailsBlock.tsx @@ -1,12 +1,12 @@ import React, { useState } from 'react'; import { ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon } from '@radix-ui/react-icons'; import { motion, AnimatePresence } from 'framer-motion'; +import { formatUnits } from 'viem'; import { formatBalance, formatReadable } from '@/utils/balance'; +import { getIRMTitle } from '@/utils/morpho'; import { Market } from '@/utils/types'; -import { TokenIcon } from '../TokenIcon'; import OracleVendorBadge from '../OracleVendorBadge'; -import { getIRMTitle } from '@/utils/morpho'; -import { formatUnits } from 'viem'; +import { TokenIcon } from '../TokenIcon'; type MarketDetailsBlockProps = { market: Market; @@ -15,11 +15,11 @@ type MarketDetailsBlockProps = { mode?: 'supply' | 'borrow'; }; -export function MarketDetailsBlock({ - market, +export function MarketDetailsBlock({ + market, showDetailsLink = false, defaultCollapsed = false, - mode = 'supply' + mode = 'supply', }: MarketDetailsBlockProps): JSX.Element { const [isExpanded, setIsExpanded] = useState(!defaultCollapsed); @@ -32,9 +32,19 @@ export function MarketDetailsBlock({ return (
{/* Collapsible Market Details */} -
setIsExpanded(!isExpanded)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setIsExpanded(!isExpanded); + } + }} + role="button" + tabIndex={0} + aria-expanded={isExpanded} + aria-label={`${isExpanded ? 'Collapse' : 'Expand'} market details`} >
@@ -59,13 +69,13 @@ export function MarketDetailsBlock({
- {market.loanAsset.symbol} + {market.loanAsset.symbol} / {market.collateralAsset.symbol} {showDetailsLink && ( e.stopPropagation()} > @@ -96,38 +106,48 @@ export function MarketDetailsBlock({ initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} - transition={{ duration: 0.2, ease: "easeInOut" }} + transition={{ duration: 0.2, ease: 'easeInOut' }} className="overflow-hidden" >
- + · {getIRMTitle(market.irmAddress)} · - {formatUnits(BigInt(market.lltv), 16)}% + + {formatUnits(BigInt(market.lltv), 16)}% +

Market State

-

{mode === 'supply' ? 'Supply' : 'Borrow'} APY:

-

- {getAPY()}% +

+ {mode === 'supply' ? 'Supply' : 'Borrow'} APY:

+

{getAPY()}%

Total Supply:

- {formatReadable(formatBalance(market.state.supplyAssets, market.loanAsset.decimals))} + {formatReadable( + formatBalance(market.state.supplyAssets, market.loanAsset.decimals), + )}

Liquidity:

- {formatReadable(formatBalance(market.state.liquidityAssets, market.loanAsset.decimals))} + {formatReadable( + formatBalance(market.state.liquidityAssets, market.loanAsset.decimals), + )}

@@ -145,4 +165,4 @@ export function MarketDetailsBlock({
); -} \ No newline at end of file +} diff --git a/src/components/index.ts b/src/components/index.ts index 98b3d83f..b3900790 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,4 +1,4 @@ export { SupplyModalV2 } from './SupplyModalV2'; export { SupplyModalContent } from './SupplyModalContent'; export { WithdrawModalContent } from './WithdrawModalContent'; -export { BorrowModal } from './BorrowModal'; \ No newline at end of file +export { BorrowModal } from './BorrowModal'; diff --git a/src/hooks/useBorrowTransaction.ts b/src/hooks/useBorrowTransaction.ts index 5024f725..c1b24398 100644 --- a/src/hooks/useBorrowTransaction.ts +++ b/src/hooks/useBorrowTransaction.ts @@ -69,8 +69,11 @@ export function useBorrowTransaction({ // Core transaction execution logic const executeBorrowTransaction = useCallback(async () => { - const minSharesToBorrow = borrowAmount === 0n ? 0n : - (borrowAmount * BigInt(market.state.supplyShares)) / BigInt(market.state.supplyAssets) - 1n; + const minSharesToBorrow = + borrowAmount === 0n + ? 0n + : (borrowAmount * BigInt(market.state.supplyShares)) / BigInt(market.state.supplyAssets) - + 1n; try { const txs: `0x${string}`[] = []; @@ -103,7 +106,6 @@ export function useBorrowTransaction({ txs.push(tx1); txs.push(tx2); - } else { // For standard ERC20 flow, we only need to transfer the tokens txs.push( @@ -114,7 +116,6 @@ export function useBorrowTransaction({ }), ); } - } setCurrentStep('borrowing'); @@ -156,7 +157,7 @@ export function useBorrowTransaction({ ], }); - console.log('morphoBorrowTx', morphoBorrowTx) + console.log('morphoBorrowTx', morphoBorrowTx); if (collateralAmount > 0n) { txs.push(morphoAddCollat); @@ -166,7 +167,7 @@ export function useBorrowTransaction({ txs.push(morphoBorrowTx); } - console.log('txs', txs.length) + console.log('txs', txs.length); // add timeout here to prevent rabby reverting await new Promise((resolve) => setTimeout(resolve, 800)); diff --git a/src/hooks/useRepayTransaction.ts b/src/hooks/useRepayTransaction.ts index ceda648b..cf775b50 100644 --- a/src/hooks/useRepayTransaction.ts +++ b/src/hooks/useRepayTransaction.ts @@ -63,20 +63,21 @@ export function useRepayTransaction({ const { isConfirming: repayPending, sendTransactionAsync } = useTransactionWithToast({ toastId: 'repay', - pendingText: `Repaying ${formatBalance(repayAssets, market.loanAsset.decimals)} ${ - market.loanAsset.symbol - }`, - successText: `${market.loanAsset.symbol} Repaid`, - errorText: 'Failed to repay', + pendingText: `${repayAssets > 0n || repayShares > 0n ? 'Repaying ' + formatBalance(repayAssets, market.loanAsset.decimals).toString() + ' ' + market.loanAsset.symbol : ''}${withdrawAmount > 0n ? (repayAssets > 0n || repayShares > 0n ? ' and ' : '') + 'Withdrawing ' + formatBalance(withdrawAmount, market.collateralAsset.decimals).toString() + ' ' + market.collateralAsset.symbol : ''}`, + successText: `${repayAssets > 0n || repayShares > 0n ? market.loanAsset.symbol + ' Repaid' : ''}${withdrawAmount > 0n ? (repayAssets > 0n || repayShares > 0n ? ' and ' : '') + market.collateralAsset.symbol + ' Withdrawn' : ''}`, + errorText: 'Transaction failed', chainId, - pendingDescription: `Repaying to market ${market.uniqueKey.slice(2, 8)}...`, - successDescription: `Successfully repaid to market ${market.uniqueKey.slice(2, 8)}`, + pendingDescription: `Processing transaction for market ${market.uniqueKey.slice(2, 8)}...`, + successDescription: `Successfully processed transaction for market ${market.uniqueKey.slice( + 2, + 8, + )}`, }); // Core transaction execution logic const executeRepayTransaction = useCallback(async () => { if (!currentPosition) { - toast.error('No Position', 'No active position found to repay'); + toast.error('No Position', 'No active position found'); return; } @@ -84,7 +85,7 @@ export function useRepayTransaction({ const txs: `0x${string}`[] = []; // Add token approval and transfer transactions if repaying - if (repayAssets > 0n || repayShares > 0n) { + if ((repayAssets > 0n || repayShares > 0n) && repayAmountToApprove > 0n) { if (usePermit2Setting) { const { sigs, permitSingle } = await signForBundlers(); const tx1 = encodeFunctionData({ @@ -115,6 +116,7 @@ export function useRepayTransaction({ } // Add the repay transaction if there's an amount to repay + if (useRepayByShares) { const morphoRepayTx = encodeFunctionData({ abi: morphoBundlerAbi, @@ -144,6 +146,7 @@ export function useRepayTransaction({ }); txs.push(refundTx); } else if (repayAssets > 0n) { + console.log('repayAssets', repayAssets); const minShares = 1n; const morphoRepayTx = encodeFunctionData({ abi: morphoBundlerAbi, @@ -265,7 +268,7 @@ export function useRepayTransaction({ throw error; } } else { - // ERC20 approval flow + // ERC20 approval flow or just withdraw if (!isApproved) { try { await approve(); @@ -320,7 +323,6 @@ export function useRepayTransaction({ if (error.message.includes('User rejected')) { toast.error('Transaction rejected', 'Transaction rejected by user'); } else { - console.log('Error in signAndRepay:', error); toast.error('Transaction Error', 'Failed to process transaction'); } } else { From 0693e9e0116d95c8bbefae149ddc2d9acec533fa Mon Sep 17 00:00:00 2001 From: antoncoding Date: Thu, 10 Apr 2025 15:19:39 +0800 Subject: [PATCH 08/12] chore: lint --- src/hooks/useRepayTransaction.ts | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/hooks/useRepayTransaction.ts b/src/hooks/useRepayTransaction.ts index cf775b50..07d221ab 100644 --- a/src/hooks/useRepayTransaction.ts +++ b/src/hooks/useRepayTransaction.ts @@ -63,8 +63,31 @@ export function useRepayTransaction({ const { isConfirming: repayPending, sendTransactionAsync } = useTransactionWithToast({ toastId: 'repay', - pendingText: `${repayAssets > 0n || repayShares > 0n ? 'Repaying ' + formatBalance(repayAssets, market.loanAsset.decimals).toString() + ' ' + market.loanAsset.symbol : ''}${withdrawAmount > 0n ? (repayAssets > 0n || repayShares > 0n ? ' and ' : '') + 'Withdrawing ' + formatBalance(withdrawAmount, market.collateralAsset.decimals).toString() + ' ' + market.collateralAsset.symbol : ''}`, - successText: `${repayAssets > 0n || repayShares > 0n ? market.loanAsset.symbol + ' Repaid' : ''}${withdrawAmount > 0n ? (repayAssets > 0n || repayShares > 0n ? ' and ' : '') + market.collateralAsset.symbol + ' Withdrawn' : ''}`, + pendingText: `${ + repayAssets > 0n || repayShares > 0n + ? 'Repaying ' + + formatBalance(repayAssets, market.loanAsset.decimals).toString() + + ' ' + + market.loanAsset.symbol + : '' + }${ + withdrawAmount > 0n + ? (repayAssets > 0n || repayShares > 0n ? ' and ' : '') + + 'Withdrawing ' + + formatBalance(withdrawAmount, market.collateralAsset.decimals).toString() + + ' ' + + market.collateralAsset.symbol + : '' + }`, + successText: `${ + repayAssets > 0n || repayShares > 0n ? market.loanAsset.symbol + ' Repaid' : '' + }${ + withdrawAmount > 0n + ? (repayAssets > 0n || repayShares > 0n ? ' and ' : '') + + market.collateralAsset.symbol + + ' Withdrawn' + : '' + }`, errorText: 'Transaction failed', chainId, pendingDescription: `Processing transaction for market ${market.uniqueKey.slice(2, 8)}...`, From 89dadc785d508537c36881f4406b7cb8405505be Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 12 Apr 2025 13:59:42 +0800 Subject: [PATCH 09/12] feat: show position on main page --- .../[marketid]/components/PositionStats.tsx | 196 ++++++++++++++++++ app/market/[chainId]/[marketid]/content.tsx | 56 +++-- src/components/SupplyModalV2.tsx | 2 +- src/components/WithdrawModalContent.tsx | 2 +- 4 files changed, 231 insertions(+), 25 deletions(-) create mode 100644 app/market/[chainId]/[marketid]/components/PositionStats.tsx diff --git a/app/market/[chainId]/[marketid]/components/PositionStats.tsx b/app/market/[chainId]/[marketid]/components/PositionStats.tsx new file mode 100644 index 00000000..788dbde8 --- /dev/null +++ b/app/market/[chainId]/[marketid]/components/PositionStats.tsx @@ -0,0 +1,196 @@ +import { useState } from 'react'; +import { Card } from '@nextui-org/card'; +import { Switch } from '@nextui-org/switch'; +import { HiOutlineGlobeAsiaAustralia } from "react-icons/hi2"; +import { FiUser } from "react-icons/fi"; +import { Market, MarketPosition } from '@/utils/types'; +import { TokenIcon } from '@/components/TokenIcon'; +import { formatBalance, formatReadable } from '@/utils/balance'; +import { Spinner } from '@/components/common/Spinner'; + +interface PositionStatsProps { + market: Market; + userPosition: MarketPosition | null; + positionLoading: boolean; + cardStyle: string; +} + +export const PositionStats = ({ market, userPosition, positionLoading, cardStyle }: PositionStatsProps) => { + // Default to user view if they have a position, otherwise global + const [viewMode, setViewMode] = useState<'global' | 'user'>(userPosition ? 'user' : 'global'); + + const toggleView = () => { + setViewMode(prev => prev === 'global' ? 'user' : 'global'); + }; + + const renderStats = () => { + if (viewMode === 'user') { + if (positionLoading) { + return ( +
+ +
+ ); + } + + if (!userPosition) { + return
No active position
; + } + + return ( +
+
+ Supply: +
+ + + {formatBalance( + BigInt(userPosition.state.supplyAssets || 0), + market.loanAsset.decimals, + ).toString()}{' '} + {market.loanAsset.symbol} + +
+
+
+ Borrow: +
+ + + {formatBalance( + BigInt(userPosition.state.borrowAssets || 0), + market.loanAsset.decimals, + ).toString()}{' '} + {market.loanAsset.symbol} + +
+
+
+ Collateral: +
+ + + {formatBalance( + BigInt(userPosition.state.collateral || 0), + market.collateralAsset.decimals, + ).toString()}{' '} + {market.collateralAsset.symbol} + +
+
+
+ ); + } + + // Global stats + return ( +
+
+ Total Supply: +
+ + + {formatReadable( + formatBalance( + BigInt(market.state.supplyAssets || 0), + market.loanAsset.decimals, + ).toString() + )}{' '} + {market.loanAsset.symbol} + +
+
+
+ Total Borrow: +
+ + + {formatReadable( + formatBalance( + BigInt(market.state.borrowAssets || 0), + market.loanAsset.decimals, + ).toString() + )}{' '} + {market.loanAsset.symbol} + +
+
+
+ Total Collateral: +
+ + + {formatReadable( + formatBalance( + BigInt(market.state.collateralAssets || 0), + market.collateralAsset.decimals, + ).toString() + )}{' '} + {market.collateralAsset.symbol} + +
+
+
+ ); + }; + + return ( + +
+ {viewMode === 'global' ? 'Global Stats' : 'Your Position'} + + isSelected ? ( + + ) : ( + + ) + } + /> +
+
+ {renderStats()} +
+
+ ); +}; \ No newline at end of file diff --git a/app/market/[chainId]/[marketid]/content.tsx b/app/market/[chainId]/[marketid]/content.tsx index d6cdea54..935a805f 100644 --- a/app/market/[chainId]/[marketid]/content.tsx +++ b/app/market/[chainId]/[marketid]/content.tsx @@ -7,6 +7,7 @@ import Image from 'next/image'; import Link from 'next/link'; import { useParams, useRouter, useSearchParams } from 'next/navigation'; import { formatUnits } from 'viem'; +import { useAccount } from 'wagmi'; import { BorrowModal } from '@/components/BorrowModal'; import { Button } from '@/components/common'; import { Spinner } from '@/components/common/Spinner'; @@ -17,6 +18,7 @@ import { SupplyModalV2 } from '@/components/SupplyModalV2'; import { TokenIcon } from '@/components/TokenIcon'; import { useMarket, useMarketHistoricalData } from '@/hooks/useMarket'; import { useOraclePrice } from '@/hooks/useOraclePrice'; +import useUserPositions from '@/hooks/useUserPosition'; import MORPHO_LOGO from '@/imgs/tokens/morpho.svg'; import { getExplorerURL, getMarketURL } from '@/utils/external'; import { getIRMTitle } from '@/utils/morpho'; @@ -27,6 +29,8 @@ import { LiquidationsTable } from './components/LiquidationsTable'; import { SuppliesTable } from './components/SuppliesTable'; import RateChart from './RateChart'; import VolumeChart from './VolumeChart'; +import { formatBalance } from '@/utils/balance'; +import { PositionStats } from './components/PositionStats'; const NOW = Math.floor(Date.now() / 1000); const WEEK_IN_SECONDS = 7 * 24 * 60 * 60; @@ -77,6 +81,13 @@ function MarketContent() { chainId: market?.morphoBlue.chain.id, }); + const { address } = useAccount(); + + const { + position: userPosition, + loading: positionLoading, + } = useUserPositions(address, network, marketid as string); + // 6. All memoized values and callbacks const formattedOraclePrice = useMemo(() => { if (!market) return '0'; @@ -168,7 +179,12 @@ function MarketContent() {
{showSupplyModal && ( - setShowSupplyModal(false)} isMarketPage /> + setShowSupplyModal(false)} + position={userPosition} + isMarketPage + /> )} {showBorrowModal && ( @@ -181,24 +197,23 @@ function MarketContent() {
- Basic Info - -
-
- Network: -
+ + Basic Info +
{networkImg && ( )} {getNetworkName(network)} -
-
+
+ + +
Loan Asset:
@@ -253,22 +268,10 @@ function MarketContent() { {getIRMTitle(market.irmAddress)}
-
- - - - - LLTV Info - -
LLTV: {formatUnits(BigInt(market.lltv), 16)}%
-
- Average LTV: - {averageLTV.toFixed(2)}% -
@@ -323,6 +326,13 @@ function MarketContent() {
+ +

Volume

diff --git a/src/components/SupplyModalV2.tsx b/src/components/SupplyModalV2.tsx index 4f8d52dc..875f2707 100644 --- a/src/components/SupplyModalV2.tsx +++ b/src/components/SupplyModalV2.tsx @@ -9,7 +9,7 @@ import { WithdrawModalContent } from './WithdrawModalContent'; type SupplyModalV2Props = { market: Market; - position?: MarketPosition; + position?: MarketPosition | null; onClose: () => void; refetch?: () => void; isMarketPage?: boolean; diff --git a/src/components/WithdrawModalContent.tsx b/src/components/WithdrawModalContent.tsx index c315902c..04903cba 100644 --- a/src/components/WithdrawModalContent.tsx +++ b/src/components/WithdrawModalContent.tsx @@ -14,7 +14,7 @@ import { Market, MarketPosition } from '@/utils/types'; import { Button } from './common'; type WithdrawModalContentProps = { - position?: MarketPosition; + position?: MarketPosition | null; market?: Market; onClose: () => void; refetch: () => void; From 6e9a50be041e796f587abd1788a44defde79ee53 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 12 Apr 2025 14:08:12 +0800 Subject: [PATCH 10/12] misc: styling on switch --- .../[marketid]/components/PositionStats.tsx | 30 ++++++++++--------- app/market/[chainId]/[marketid]/content.tsx | 11 ++----- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/app/market/[chainId]/[marketid]/components/PositionStats.tsx b/app/market/[chainId]/[marketid]/components/PositionStats.tsx index 788dbde8..cb9bd2f0 100644 --- a/app/market/[chainId]/[marketid]/components/PositionStats.tsx +++ b/app/market/[chainId]/[marketid]/components/PositionStats.tsx @@ -1,21 +1,25 @@ import { useState } from 'react'; import { Card } from '@nextui-org/card'; import { Switch } from '@nextui-org/switch'; -import { HiOutlineGlobeAsiaAustralia } from "react-icons/hi2"; import { FiUser } from "react-icons/fi"; -import { Market, MarketPosition } from '@/utils/types'; +import { HiOutlineGlobeAsiaAustralia } from "react-icons/hi2"; +import { Spinner } from '@/components/common/Spinner'; import { TokenIcon } from '@/components/TokenIcon'; import { formatBalance, formatReadable } from '@/utils/balance'; -import { Spinner } from '@/components/common/Spinner'; +import { Market, MarketPosition } from '@/utils/types'; -interface PositionStatsProps { +type PositionStatsProps = { market: Market; userPosition: MarketPosition | null; positionLoading: boolean; cardStyle: string; } -export const PositionStats = ({ market, userPosition, positionLoading, cardStyle }: PositionStatsProps) => { +function ThumbIcon({ isSelected, className }: { isSelected: boolean; className?: string }) { + return isSelected ? : ; +} + +export function PositionStats({ market, userPosition, positionLoading, cardStyle }: PositionStatsProps) { // Default to user view if they have a position, otherwise global const [viewMode, setViewMode] = useState<'global' | 'user'>(userPosition ? 'user' : 'global'); @@ -172,20 +176,18 @@ export const PositionStats = ({ market, userPosition, positionLoading, cardStyle return ( -
+
{viewMode === 'global' ? 'Global Stats' : 'Your Position'} - isSelected ? ( - - ) : ( - - ) - } + thumbIcon={ThumbIcon} />
@@ -193,4 +195,4 @@ export const PositionStats = ({ market, userPosition, positionLoading, cardStyle
); -}; \ No newline at end of file +} \ No newline at end of file diff --git a/app/market/[chainId]/[marketid]/content.tsx b/app/market/[chainId]/[marketid]/content.tsx index 935a805f..b6d3fc54 100644 --- a/app/market/[chainId]/[marketid]/content.tsx +++ b/app/market/[chainId]/[marketid]/content.tsx @@ -26,11 +26,10 @@ import { getNetworkImg, getNetworkName, SupportedNetworks } from '@/utils/networ import { TimeseriesOptions } from '@/utils/types'; import { BorrowsTable } from './components/BorrowsTable'; import { LiquidationsTable } from './components/LiquidationsTable'; +import { PositionStats } from './components/PositionStats'; import { SuppliesTable } from './components/SuppliesTable'; import RateChart from './RateChart'; import VolumeChart from './VolumeChart'; -import { formatBalance } from '@/utils/balance'; -import { PositionStats } from './components/PositionStats'; const NOW = Math.floor(Date.now() / 1000); const WEEK_IN_SECONDS = 7 * 24 * 60 * 60; @@ -144,13 +143,7 @@ function MarketContent() { // 8. Derived values that depend on market data const cardStyle = 'bg-surface rounded shadow-sm p-4'; - const averageLTV = - !market.state.collateralAssetsUsd || - !market.state.borrowAssetsUsd || - market.state.collateralAssetsUsd <= 0 - ? 0 - : (parseFloat(market.state.borrowAssetsUsd) / market.state.collateralAssetsUsd) * 100; - + return ( <>
From 45abd7317ebe41a6076e95692654cf1be10bb074 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 12 Apr 2025 14:17:09 +0800 Subject: [PATCH 11/12] feat: use live oracle price --- app/market/[chainId]/[marketid]/content.tsx | 6 +++++- src/components/Borrow/AddCollateralAndBorrow.tsx | 14 ++++++++------ .../Borrow/WithdrawCollateralAndRepay.tsx | 14 ++++++++------ src/components/BorrowModal.tsx | 5 ++++- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/app/market/[chainId]/[marketid]/content.tsx b/app/market/[chainId]/[marketid]/content.tsx index b6d3fc54..3751ee8d 100644 --- a/app/market/[chainId]/[marketid]/content.tsx +++ b/app/market/[chainId]/[marketid]/content.tsx @@ -181,7 +181,11 @@ function MarketContent() { )} {showBorrowModal && ( - setShowBorrowModal(false)} /> + setShowBorrowModal(false)} + oraclePrice={oraclePrice} + /> )}

diff --git a/src/components/Borrow/AddCollateralAndBorrow.tsx b/src/components/Borrow/AddCollateralAndBorrow.tsx index 8690df1c..cac3ae95 100644 --- a/src/components/Borrow/AddCollateralAndBorrow.tsx +++ b/src/components/Borrow/AddCollateralAndBorrow.tsx @@ -23,6 +23,7 @@ type BorrowLogicProps = { refetchPosition: (onSuccess?: () => void) => void; collateralTokenBalance: bigint | undefined; ethBalance: bigint | undefined; + oraclePrice: bigint; }; export function AddCollateralAndBorrow({ @@ -31,6 +32,7 @@ export function AddCollateralAndBorrow({ refetchPosition, collateralTokenBalance, ethBalance, + oraclePrice, }: BorrowLogicProps): JSX.Element { // State for collateral and borrow amounts const [collateralAmount, setCollateralAmount] = useState(BigInt(0)); @@ -80,9 +82,9 @@ export function AddCollateralAndBorrow({ if (!currentPosition) { setCurrentLTV(BigInt(0)); } else { - // Calculate current LTV from position data + // Calculate current LTV from position data using oracle price const currentCollateralValue = - (BigInt(currentPosition.state.collateral) * BigInt(market.collateralPrice)) / + (BigInt(currentPosition.state.collateral) * oraclePrice) / BigInt(10 ** 36); const currentBorrowValue = BigInt(currentPosition.state.borrowAssets || 0); @@ -93,15 +95,15 @@ export function AddCollateralAndBorrow({ setCurrentLTV(BigInt(0)); } } - }, [currentPosition, market]); + }, [currentPosition, oraclePrice]); useEffect(() => { - // Calculate new LTV based on current position plus new amounts + // Calculate new LTV based on current position plus new amounts using oracle price const newCollateral = BigInt(currentPosition?.state.collateral ?? 0) + collateralAmount; const newBorrow = BigInt(currentPosition?.state.borrowAssets ?? 0) + borrowAmount; const newCollateralValueInLoan = - (newCollateral * BigInt(market.collateralPrice)) / BigInt(10 ** 36); + (newCollateral * oraclePrice) / BigInt(10 ** 36); if (newCollateralValueInLoan > 0) { const ltv = (newBorrow * BigInt(10 ** 18)) / newCollateralValueInLoan; @@ -109,7 +111,7 @@ export function AddCollateralAndBorrow({ } else { setNewLTV(BigInt(0)); } - }, [currentPosition, collateralAmount, borrowAmount, market]); + }, [currentPosition, collateralAmount, borrowAmount, oraclePrice]); // Function to refresh position data const handleRefreshPosition = () => { diff --git a/src/components/Borrow/WithdrawCollateralAndRepay.tsx b/src/components/Borrow/WithdrawCollateralAndRepay.tsx index fb7fb500..3acae0a9 100644 --- a/src/components/Borrow/WithdrawCollateralAndRepay.tsx +++ b/src/components/Borrow/WithdrawCollateralAndRepay.tsx @@ -21,6 +21,7 @@ type WithdrawCollateralAndRepayProps = { loanTokenBalance: bigint | undefined; collateralTokenBalance: bigint | undefined; ethBalance: bigint | undefined; + oraclePrice: bigint; }; export function WithdrawCollateralAndRepay({ @@ -28,6 +29,7 @@ export function WithdrawCollateralAndRepay({ currentPosition, refetchPosition, loanTokenBalance, + oraclePrice, }: WithdrawCollateralAndRepayProps): JSX.Element { // State for withdraw and repay amounts const [withdrawAmount, setWithdrawAmount] = useState(BigInt(0)); @@ -103,9 +105,9 @@ export function WithdrawCollateralAndRepay({ if (!currentPosition) { setCurrentLTV(BigInt(0)); } else { - // Calculate current LTV from position data + // Calculate current LTV from position data using oracle price const currentCollateralValue = - (BigInt(currentPosition.state.collateral) * BigInt(market.collateralPrice)) / + (BigInt(currentPosition.state.collateral) * oraclePrice) / BigInt(10 ** 36); const currentBorrowValue = BigInt(currentPosition.state.borrowAssets || 0); @@ -116,17 +118,17 @@ export function WithdrawCollateralAndRepay({ setCurrentLTV(BigInt(0)); } } - }, [currentPosition, market]); + }, [currentPosition, oraclePrice]); useEffect(() => { if (!currentPosition) return; - // Calculate new LTV based on current position minus withdraw/repay amounts + // Calculate new LTV based on current position minus withdraw/repay amounts using oracle price const newCollateral = BigInt(currentPosition.state.collateral) - withdrawAmount; const newBorrow = BigInt(currentPosition.state.borrowAssets || 0) - repayAssets; const newCollateralValueInLoan = - (newCollateral * BigInt(market.collateralPrice)) / BigInt(10 ** 36); + (newCollateral * oraclePrice) / BigInt(10 ** 36); if (newCollateralValueInLoan > 0) { const ltv = (newBorrow * BigInt(10 ** 18)) / newCollateralValueInLoan; @@ -134,7 +136,7 @@ export function WithdrawCollateralAndRepay({ } else { setNewLTV(BigInt(0)); } - }, [currentPosition, withdrawAmount, repayAssets, market]); + }, [currentPosition, withdrawAmount, repayAssets, oraclePrice]); // Function to refresh position data const handleRefreshPosition = () => { diff --git a/src/components/BorrowModal.tsx b/src/components/BorrowModal.tsx index 57926f93..bebfbc57 100644 --- a/src/components/BorrowModal.tsx +++ b/src/components/BorrowModal.tsx @@ -11,9 +11,10 @@ import { TokenIcon } from './TokenIcon'; type BorrowModalProps = { market: Market; onClose: () => void; + oraclePrice: bigint; }; -export function BorrowModal({ market, onClose }: BorrowModalProps): JSX.Element { +export function BorrowModal({ market, onClose, oraclePrice }: BorrowModalProps): JSX.Element { const [mode, setMode] = useState<'borrow' | 'repay'>('borrow'); const { address: account } = useAccount(); @@ -114,6 +115,7 @@ export function BorrowModal({ market, onClose }: BorrowModalProps): JSX.Element refetchPosition={refetchPosition} collateralTokenBalance={collateralTokenBalance?.value} ethBalance={ethBalance?.value} + oraclePrice={oraclePrice} /> ) : ( )}

From 073fb71062a87eb517d55cbbb7db4f12671f46c2 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 12 Apr 2025 14:18:35 +0800 Subject: [PATCH 12/12] misc: review fixes --- src/components/WithdrawModalContent.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/WithdrawModalContent.tsx b/src/components/WithdrawModalContent.tsx index 04903cba..b4ad0f9d 100644 --- a/src/components/WithdrawModalContent.tsx +++ b/src/components/WithdrawModalContent.tsx @@ -155,7 +155,6 @@ export function WithdrawModalContent({ setValue={setWithdrawAmount} setError={setInputError} exceedMaxErrMessage="Insufficient Liquidity" - allowExceedMax /> {inputError && (

-
- +
-
+
+