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/PositionStats.tsx b/app/market/[chainId]/[marketid]/components/PositionStats.tsx new file mode 100644 index 00000000..cb9bd2f0 --- /dev/null +++ b/app/market/[chainId]/[marketid]/components/PositionStats.tsx @@ -0,0 +1,198 @@ +import { useState } from 'react'; +import { Card } from '@nextui-org/card'; +import { Switch } from '@nextui-org/switch'; +import { FiUser } from "react-icons/fi"; +import { HiOutlineGlobeAsiaAustralia } from "react-icons/hi2"; +import { Spinner } from '@/components/common/Spinner'; +import { TokenIcon } from '@/components/TokenIcon'; +import { formatBalance, formatReadable } from '@/utils/balance'; +import { Market, MarketPosition } from '@/utils/types'; + +type PositionStatsProps = { + market: Market; + userPosition: MarketPosition | null; + positionLoading: boolean; + cardStyle: string; +} + +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'); + + 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'} + +
+
+ {renderStats()} +
+
+ ); +} \ No newline at end of file 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) { ('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 +60,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 +74,28 @@ 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, + }); + + 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'; + 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 +117,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,15 +141,9 @@ function MarketContent() { return
Market data not available
; } - const cardStyle = 'bg-surface rounded-md 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; - + // 8. Derived values that depend on market data + const cardStyle = 'bg-surface rounded shadow-sm p-4'; + return ( <>
@@ -151,11 +172,20 @@ function MarketContent() {
{showSupplyModal && ( - setShowSupplyModal(false)} /> + setShowSupplyModal(false)} + position={userPosition} + isMarketPage + /> )} {showBorrowModal && ( - setShowBorrowModal(false)} /> + setShowBorrowModal(false)} + oraclePrice={oraclePrice} + /> )}

@@ -164,24 +194,23 @@ function MarketContent() {
- Basic Info - -
-
- Network: -
+ + Basic Info +
{networkImg && ( )} {getNetworkName(network)} -
-
+
+ + +
Loan Asset:
@@ -236,22 +265,10 @@ function MarketContent() { {getIRMTitle(market.irmAddress)}
-
- - - - - LLTV Info - -
LLTV: {formatUnits(BigInt(market.lltv), 16)}%
-
- Average LTV: - {averageLTV.toFixed(2)}% -
@@ -274,11 +291,16 @@ function MarketContent() { )}
+
+ Live Price: + + {Number(formattedOraclePrice).toFixed(4)} {market.loanAsset.symbol} + +

Feed Routes:

{market.oracle.data && (
- {' '} {' '} + />
)}
+ +

Volume

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..27cf1436 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,11 +361,8 @@ export default function Markets() {

Markets

- {showSupplyModal && ( - setShowSupplyModal(false)} - /> + {showSupplyModal && selectedMarket && ( + setShowSupplyModal(false)} /> )} {showWithdrawModal && selectedPosition && ( - { setShowWithdrawModal(false); setSelectedPosition(null); }} refetch={() => void refetch()} + isMarketPage={false} + defaultMode="withdraw" /> )} {showSupplyModal && selectedPosition && ( - { setShowSupplyModal(false); setSelectedPosition(null); }} + refetch={() => void refetch()} + isMarketPage={false} /> )} diff --git a/docs/Styling.md b/docs/Styling.md index 97979624..1c5552b8 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..cac3ae95 100644 --- a/src/components/Borrow/AddCollateralAndBorrow.tsx +++ b/src/components/Borrow/AddCollateralAndBorrow.tsx @@ -1,16 +1,15 @@ -import React, { useMemo, useState, useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import { Switch } from '@nextui-org/react'; import { ReloadIcon } from '@radix-ui/react-icons'; -import { Address } from 'viem'; import { useAccount } from 'wagmi'; import { Button } from '@/components/common'; import { LTVWarning } from '@/components/common/LTVWarning'; +import { MarketDetailsBlock } from '@/components/common/MarketDetailsBlock'; import Input from '@/components/Input/Input'; 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'; @@ -22,18 +21,18 @@ type BorrowLogicProps = { market: Market; currentPosition: MarketPosition | null; refetchPosition: (onSuccess?: () => void) => void; - loanTokenBalance: bigint | undefined; collateralTokenBalance: bigint | undefined; ethBalance: bigint | undefined; + oraclePrice: bigint; }; export function AddCollateralAndBorrow({ market, currentPosition, refetchPosition, - loanTokenBalance, collateralTokenBalance, ethBalance, + oraclePrice, }: BorrowLogicProps): JSX.Element { // State for collateral and borrow amounts const [collateralAmount, setCollateralAmount] = useState(BigInt(0)); @@ -78,19 +77,15 @@ 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) { 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) * oraclePrice) / BigInt(10 ** 36); + (BigInt(currentPosition.state.collateral) * oraclePrice) / + BigInt(10 ** 36); const currentBorrowValue = BigInt(currentPosition.state.borrowAssets || 0); if (currentCollateralValue > 0) { @@ -100,14 +95,15 @@ export function AddCollateralAndBorrow({ setCurrentLTV(BigInt(0)); } } - }, [currentPosition, market, oraclePrice]); + }, [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 * oraclePrice) / BigInt(10 ** 36); + const newCollateralValueInLoan = + (newCollateral * oraclePrice) / BigInt(10 ** 36); if (newCollateralValueInLoan > 0) { const ltv = (newBorrow * BigInt(10 ** 18)) / newCollateralValueInLoan; @@ -115,14 +111,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, oraclePrice]); // Function to refresh position data const handleRefreshPosition = () => { @@ -156,9 +145,9 @@ export function AddCollateralAndBorrow({ {!showProcessModal && (
{/* 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} - -
+ {/* Market Details Block - includes position overview and collapsible details */} +
+
{isConnected && ( - <> +
{/* Collateral Input Section */} -
+

Add Collateral

@@ -335,6 +293,7 @@ export function AddCollateralAndBorrow({ setValue={setCollateralAmount} setError={setCollateralInputError} exceedMaxErrMessage="Insufficient Balance" + value={collateralAmount} /> {collateralInputError && (

{collateralInputError}

@@ -348,10 +307,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}

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

{borrowInputError}

@@ -371,7 +330,7 @@ export function AddCollateralAndBorrow({
- +
)} {/* Action Button */} @@ -410,15 +369,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 63d0fdf8..3acae0a9 100644 --- a/src/components/Borrow/WithdrawCollateralAndRepay.tsx +++ b/src/components/Borrow/WithdrawCollateralAndRepay.tsx @@ -7,10 +7,10 @@ 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 { formatBalance } from '@/utils/balance'; import { Market, MarketPosition } from '@/utils/types'; +import { MarketDetailsBlock } from '../common/MarketDetailsBlock'; import { TokenIcon } from '../TokenIcon'; import { getLTVColor, getLTVProgressColor } from './helpers'; @@ -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)); @@ -90,11 +92,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) @@ -108,9 +105,10 @@ 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) * oraclePrice) / BigInt(10 ** 36); + (BigInt(currentPosition.state.collateral) * oraclePrice) / + BigInt(10 ** 36); const currentBorrowValue = BigInt(currentPosition.state.borrowAssets || 0); if (currentCollateralValue > 0) { @@ -120,16 +118,17 @@ export function WithdrawCollateralAndRepay({ setCurrentLTV(BigInt(0)); } } - }, [currentPosition, market, oraclePrice]); + }, [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 * oraclePrice) / BigInt(10 ** 36); + const newCollateralValueInLoan = + (newCollateral * oraclePrice) / BigInt(10 ** 36); if (newCollateralValueInLoan > 0) { const ltv = (newBorrow * BigInt(10 ** 18)) / newCollateralValueInLoan; @@ -137,14 +136,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, oraclePrice]); // Function to refresh position data const handleRefreshPosition = () => { @@ -163,9 +155,9 @@ 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} - -
+ {/* Market Details Block - includes position overview and collapsible details */} +
+
{isConnected && ( - <> +
{/* Withdraw Input Section */} -
+

Withdraw Collateral

@@ -358,7 +319,7 @@ export function WithdrawCollateralAndRepay({

- +
)} {/* Action Button */} diff --git a/src/components/BorrowModal.tsx b/src/components/BorrowModal.tsx index 7f47c01b..bebfbc57 100644 --- a/src/components/BorrowModal.tsx +++ b/src/components/BorrowModal.tsx @@ -1,21 +1,21 @@ 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 = { 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(); // Get user positions to calculate current LTV @@ -43,10 +43,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' ? ( @@ -89,9 +113,9 @@ export function BorrowModal({ market, onClose }: BorrowModalProps): JSX.Element market={market} currentPosition={currentPosition} refetchPosition={refetchPosition} - loanTokenBalance={loanTokenBalance?.value} collateralTokenBalance={collateralTokenBalance?.value} ethBalance={ethBalance?.value} + oraclePrice={oraclePrice} /> ) : ( )}
diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 73bd4a1a..4a3e784e 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,19 @@ 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/components/SupplyModalContent.tsx b/src/components/SupplyModalContent.tsx new file mode 100644 index 00000000..2e161206 --- /dev/null +++ b/src/components/SupplyModalContent.tsx @@ -0,0 +1,177 @@ +import React, { useCallback } from 'react'; +import { Switch } from '@nextui-org/react'; +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 } from '@/utils/balance'; +import { isWETH } from '@/utils/tokens'; +import { Market } from '@/utils/types'; +import { Button } from './common'; +import { SupplyProcessModal } from './SupplyProcessModal'; + +type SupplyModalContentProps = { + market: Market; + onClose: () => void; + refetch: () => void; +}; + +export function SupplyModalContent({ + onClose, + market, + refetch, +}: SupplyModalContentProps): JSX.Element { + const [usePermit2Setting] = useLocalStorage('usePermit2', true); + const { isConnected } = useAccount(); + + const onSuccess = useCallback(() => { + onClose(); + refetch(); + }, [onClose]); + + // 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, onSuccess); + + // 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 && ( +
+ {!isConnected ? ( +
+ +
+ ) : ( + <> + {/* 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) ? ( + + ) : ( + + )} +
+
+
+ + )} +
+ )} + + ); +} diff --git a/src/components/SupplyModalV2.tsx b/src/components/SupplyModalV2.tsx new file mode 100644 index 00000000..875f2707 --- /dev/null +++ b/src/components/SupplyModalV2.tsx @@ -0,0 +1,101 @@ +import React, { useState } from 'react'; +import { Cross1Icon } from '@radix-ui/react-icons'; +import { FaArrowRightArrowLeft } from 'react-icons/fa6'; +import { Market, MarketPosition } from '@/utils/types'; +import { MarketDetailsBlock } from './common/MarketDetailsBlock'; +import { SupplyModalContent } from './SupplyModalContent'; +import { TokenIcon } from './TokenIcon'; +import { WithdrawModalContent } from './WithdrawModalContent'; + +type SupplyModalV2Props = { + market: Market; + position?: MarketPosition | null; + onClose: () => void; + refetch?: () => void; + isMarketPage?: boolean; + defaultMode?: 'supply' | 'withdraw'; +}; + +export function SupplyModalV2({ + market, + position, + onClose, + refetch, + isMarketPage, + defaultMode = 'supply', +}: SupplyModalV2Props): JSX.Element { + const [mode, setMode] = useState<'supply' | 'withdraw'>(defaultMode); + + const hasPosition = position && BigInt(position.state.supplyAssets) > 0n; + + return ( +
+
+
+ + +
+
+
+ + + {mode === 'supply' ? 'Supply' : 'Withdraw'} {market.loanAsset.symbol} + +
+ + {mode === 'supply' ? 'Supply to earn interest' : 'Withdraw your supplied assets'} + +
+ + {hasPosition && ( + + )} +
+ + {/* Market Details Block - includes position overview and collapsible details */} +
+ +
+ + {mode === 'supply' ? ( + {})} /> + ) : ( + {})} + /> + )} +
+
+
+ ); +} diff --git a/src/components/WithdrawModalContent.tsx b/src/components/WithdrawModalContent.tsx new file mode 100644 index 00000000..b4ad0f9d --- /dev/null +++ b/src/components/WithdrawModalContent.tsx @@ -0,0 +1,186 @@ +// Import the necessary hooks +import { useCallback, useState } from 'react'; +import { Address, encodeFunctionData } from 'viem'; +import { useAccount } from 'wagmi'; +import morphoAbi from '@/abis/morpho'; +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'; + +type WithdrawModalContentProps = { + position?: MarketPosition | null; + market?: Market; + onClose: () => void; + refetch: () => void; +}; + +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; + + const { needSwitchChain, switchToNetwork } = useMarketNetwork({ + targetChainId: activeMarket?.morphoBlue.chain.id ?? 0, + }); + + const { isConfirming, sendTransaction } = useTransactionWithToast({ + toastId: 'withdraw', + pendingText: activeMarket + ? `Withdrawing ${formatBalance(withdrawAmount, activeMarket.loanAsset.decimals)} ${ + activeMarket.loanAsset.symbol + }` + : '', + successText: activeMarket ? `${activeMarket.loanAsset.symbol} Withdrawn` : '', + errorText: 'Failed to withdraw', + chainId, + pendingDescription: activeMarket + ? `Withdrawing from market ${activeMarket.uniqueKey.slice(2, 8)}...` + : '', + successDescription: activeMarket + ? `Successfully withdrawn from market ${activeMarket.uniqueKey.slice(2, 8)}` + : '', + onSuccess: () => { + refetch(); + onClose(); + }, + }); + + 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; + } + + 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, toast]); + + if (!activeMarket) { + return ( +
+

Market data not available

+
+ ); + } + + return ( +
+ {!isConnected ? ( +
+ +
+ ) : ( + <> + {/* Withdraw Input Section */} +
+
+
+ Withdraw amount +
+

+ Available:{' '} + {formatReadable( + formatBalance( + position?.state.supplyAssets ?? BigInt(0), + 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..e89b9aa9 --- /dev/null +++ b/src/components/common/MarketDetailsBlock.tsx @@ -0,0 +1,168 @@ +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 OracleVendorBadge from '../OracleVendorBadge'; +import { TokenIcon } from '../TokenIcon'; + +type MarketDetailsBlockProps = { + market: Market; + showDetailsLink?: boolean; + defaultCollapsed?: boolean; + mode?: 'supply' | 'borrow'; +}; + +export function MarketDetailsBlock({ + market, + 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)} + 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`} + > +
+
+
+
+ +
+
+ +
+
+
+ {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)} + · + + {formatUnits(BigInt(market.lltv), 16)}% + +
+
+
+

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/components/index.ts b/src/components/index.ts new file mode 100644 index 00000000..b3900790 --- /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'; 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} `; diff --git a/src/hooks/useBorrowTransaction.ts b/src/hooks/useBorrowTransaction.ts index 9451133a..c1b24398 100644 --- a/src/hooks/useBorrowTransaction.ts +++ b/src/hooks/useBorrowTransaction.ts @@ -70,47 +70,52 @@ export function useBorrowTransaction({ // Core transaction execution logic const executeBorrowTransaction = useCallback(async () => { const minSharesToBorrow = - (borrowAmount * BigInt(market.state.supplyShares)) / BigInt(market.state.supplyAssets) - 1n; + 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 +157,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 b3954e96..f1d1dba1 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,7 +40,7 @@ export function useMarketNetwork({ targetChainId, onNetworkSwitched, }: UseMarketNetworkProps): UseMarketNetworkReturn { - const { chainId } = useAccount(); + const chainId = useChainId(); const { switchChain } = useSwitchChain(); const toast = useStyledToast(); diff --git a/src/hooks/useRepayTransaction.ts b/src/hooks/useRepayTransaction.ts index ceda648b..07d221ab 100644 --- a/src/hooks/useRepayTransaction.ts +++ b/src/hooks/useRepayTransaction.ts @@ -63,20 +63,44 @@ export function useRepayTransaction({ const { isConfirming: repayPending, sendTransactionAsync } = useTransactionWithToast({ toastId: 'repay', - pendingText: `Repaying ${formatBalance(repayAssets, market.loanAsset.decimals)} ${ - market.loanAsset.symbol + 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: `${market.loanAsset.symbol} Repaid`, - errorText: 'Failed to repay', + 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 +108,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 +139,7 @@ export function useRepayTransaction({ } // Add the repay transaction if there's an amount to repay + if (useRepayByShares) { const morphoRepayTx = encodeFunctionData({ abi: morphoBundlerAbi, @@ -144,6 +169,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 +291,7 @@ export function useRepayTransaction({ throw error; } } else { - // ERC20 approval flow + // ERC20 approval flow or just withdraw if (!isApproved) { try { await approve(); @@ -320,7 +346,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 {