diff --git a/packages/ui/app/_components/AnimateHeight.tsx b/packages/ui/app/_components/AnimateHeight.tsx new file mode 100644 index 000000000..54a602725 --- /dev/null +++ b/packages/ui/app/_components/AnimateHeight.tsx @@ -0,0 +1,30 @@ +import { useState, useRef, useLayoutEffect } from 'react'; + +const AnimateHeight = ({ children }: { children: React.ReactNode }) => { + const [height, setHeight] = useState(null); + const contentRef = useRef(null); + + useLayoutEffect(() => { + if (contentRef.current) { + const resizeObserver = new ResizeObserver(() => { + if (contentRef.current) { + setHeight(contentRef.current.scrollHeight); + } + }); + + resizeObserver.observe(contentRef.current); + return () => resizeObserver.disconnect(); + } + }, []); + + return ( +
+
{children}
+
+ ); +}; + +export default AnimateHeight; diff --git a/packages/ui/app/_components/CommonTable.tsx b/packages/ui/app/_components/CommonTable.tsx index 07b1c44bf..71cb8408d 100644 --- a/packages/ui/app/_components/CommonTable.tsx +++ b/packages/ui/app/_components/CommonTable.tsx @@ -1,10 +1,19 @@ +import type { ReactNode } from 'react'; import { useState } from 'react'; +import { + ArrowDownIcon, + ArrowUpIcon, + CaretSortIcon +} from '@radix-ui/react-icons'; import { flexRender, getCoreRowModel, getSortedRowModel, - useReactTable + useReactTable, + type ColumnDef, + type Row, + type SortingState } from '@tanstack/react-table'; import { @@ -16,98 +25,191 @@ import { TableRow } from '@ui/components/ui/table'; -import { TableLoader } from './TableLoader'; +import ResultHandler from './ResultHandler'; + +export const sortingFunctions = { + numerical: (a: any, b: any) => { + if (typeof a === 'number' && typeof b === 'number') { + return a - b; + } + const aValue = parseFloat(String(a).replace(/[^0-9.-]+/g, '')) || 0; + const bValue = parseFloat(String(b).replace(/[^0-9.-]+/g, '')) || 0; + return aValue - bValue; + }, + alphabetical: (a: any, b: any) => String(a).localeCompare(String(b)), + percentage: (a: any, b: any) => { + const aValue = parseFloat(String(a).replace('%', '')) || 0; + const bValue = parseFloat(String(b).replace('%', '')) || 0; + return aValue - bValue; + } +}; + +type SortingType = keyof typeof sortingFunctions; + +export type EnhancedColumnDef = Omit< + ColumnDef, + 'header' | 'sortingFn' +> & { + id: string; + header: ReactNode | string; + sortingFn?: SortingType | ((rowA: any, rowB: any) => number); + enableSorting?: boolean; + accessorFn?: (row: T) => any; +}; + +interface RowBadge { + text: string; + className?: string; +} -import type { ColumnDef, SortingState } from '@tanstack/react-table'; +interface RowStyle { + badge?: RowBadge; + borderClassName?: string; +} -interface CommonTableProps { +interface CommonTableProps { data: T[]; - columns: ColumnDef[]; + columns: EnhancedColumnDef[]; isLoading?: boolean; - loadingRows?: number; - showFooter?: boolean; + getRowStyle?: (row: Row) => RowStyle; } -function CommonTable({ +const SortableHeader = ({ + column, + children +}: { + column: any; + children: ReactNode; +}) => { + const isSortable = column.getCanSort(); + const sorted = column.getIsSorted(); + + const getSortIcon = () => { + if (!isSortable) return null; + if (sorted === 'asc') return ; + if (sorted === 'desc') return ; + return ; + }; + + return ( + + ); +}; + +function CommonTable({ data, columns, - isLoading = false, - loadingRows = 5, - showFooter = false + isLoading: externalIsLoading = false, + getRowStyle }: CommonTableProps) { const [sorting, setSorting] = useState([]); + const [hasInitialized, setHasInitialized] = useState(false); + + const isLoading = !hasInitialized || externalIsLoading; + + if (!hasInitialized && data.length > 0) { + setHasInitialized(true); + } + + const processedColumns = columns.map( + (col): ColumnDef => ({ + ...col, + accessorFn: col.accessorFn || ((row: T) => (row as any)[col.id]), + header: ({ column }) => ( + {col.header} + ), + sortingFn: + typeof col.sortingFn === 'string' + ? (rowA: any, rowB: any) => { + const sortFn = sortingFunctions[col.sortingFn as SortingType]; + return sortFn(rowA.getValue(col.id), rowB.getValue(col.id)); + } + : col.sortingFn, + enableSorting: col.enableSorting !== false + }) + ); const table = useReactTable({ data, - columns, + columns: processedColumns, getCoreRowModel: getCoreRowModel(), onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(), - state: { - sorting - } + state: { sorting }, + enableMultiSort: false }); - if (isLoading) { - return ( - - ); - } - return ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ))} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( + +
+ + {table.getHeaderGroups().map((headerGroup) => ( - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + ))} - )) - ) : ( - - - No results. - - - )} - -
+ ))} + + + {!isLoading && table.getRowModel().rows?.length === 0 ? ( + + + No results. + + + ) : ( + table.getRowModel().rows.map((row) => { + const rowStyle = getRowStyle ? getRowStyle(row) : {}; + + return ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ); + }) + )} + + + ); } diff --git a/packages/ui/app/_components/Modal.tsx b/packages/ui/app/_components/Modal.tsx deleted file mode 100644 index c99224528..000000000 --- a/packages/ui/app/_components/Modal.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { useEffect, useState } from 'react'; - -import Image from 'next/image'; - -export type ModalProps = { - children: React.ReactNode; - close: () => void; -}; - -export default function Modal({ children, close }: ModalProps) { - const [isMounted, setIsMounted] = useState(false); - - /** - * Animation - */ - useEffect(() => { - setIsMounted(true); - }, []); - - useEffect(() => { - let closeTimer: ReturnType; - - if (!isMounted) { - closeTimer = setTimeout(() => { - close(); - }, 301); - } - - return () => { - clearTimeout(closeTimer); - }; - }, [close, isMounted]); - - return ( -
-
- close setIsMounted(false)} - src="/img/assets/close.png" - width="20" - /> - - {children} -
-
- ); -} diff --git a/packages/ui/app/_components/ProgressBar.tsx b/packages/ui/app/_components/ProgressBar.tsx new file mode 100644 index 000000000..1f8f09a2e --- /dev/null +++ b/packages/ui/app/_components/ProgressBar.tsx @@ -0,0 +1,33 @@ +import React, { memo, useMemo } from 'react'; + +import { Progress } from '@ui/components/ui/progress'; + +export type ProgressBarProps = { + max: number; + value: number; +}; + +function ProgressBar({ max, value }: ProgressBarProps) { + const percentage = useMemo(() => (value / max) * 100, [max, value]); + + const percentageFormatted = useMemo( + () => `${percentage.toFixed(2)}%`, + [percentage] + ); + + return ( +
+ +
+ {percentageFormatted} utilized +
+
+ ); +} + +const MemoizedProgressBar = memo(ProgressBar); + +export default MemoizedProgressBar; diff --git a/packages/ui/app/_components/ResultHandler.tsx b/packages/ui/app/_components/ResultHandler.tsx index 896eda052..6966e2d1c 100644 --- a/packages/ui/app/_components/ResultHandler.tsx +++ b/packages/ui/app/_components/ResultHandler.tsx @@ -6,10 +6,10 @@ type ResultHandlerProps = { center?: boolean; children: React.ReactNode; color?: string; - height?: string; + height?: number | string; + width?: number | string; isFetching?: boolean; isLoading: boolean; - width?: string; }; export default function ResultHandler({ diff --git a/packages/ui/app/_components/UtilizationStats.tsx b/packages/ui/app/_components/UtilizationStats.tsx new file mode 100644 index 000000000..f9e048e87 --- /dev/null +++ b/packages/ui/app/_components/UtilizationStats.tsx @@ -0,0 +1,51 @@ +import React, { memo, useMemo } from 'react'; + +import millify from 'millify'; + +import MemoizedDonutChart from './dialogs/manage/DonutChart'; + +interface UtilizationStatsProps { + label: string; + value: number; + max: number; + symbol?: string; + valueInFiat: number; + maxInFiat: number; +} + +function UtilizationStats({ + label, + value, + max, + symbol = '', + valueInFiat, + maxInFiat +}: UtilizationStatsProps) { + return ( +
+
+
+ +
+
+
{label}
+
+ + {millify(value)} of {millify(max)} {symbol} + +
+
+ ${millify(valueInFiat)} of ${millify(maxInFiat)} +
+
+
+
+ ); +} + +const MemoizedUtilizationStats = memo(UtilizationStats); + +export default MemoizedUtilizationStats; diff --git a/packages/ui/app/_components/dashboards/CollateralSwapPopup.tsx b/packages/ui/app/_components/dashboards/CollateralSwapPopup.tsx index 728162a92..da83a48d1 100644 --- a/packages/ui/app/_components/dashboards/CollateralSwapPopup.tsx +++ b/packages/ui/app/_components/dashboards/CollateralSwapPopup.tsx @@ -30,10 +30,10 @@ import { } from 'viem'; import { useAccount, useChainId, useWriteContract } from 'wagmi'; -import SliderComponent from '@ui/app/_components/popup/Slider'; +import SliderComponent from '@ui/app/_components/dialogs/manage/Slider'; import TransactionStepsHandler, { useTransactionSteps -} from '@ui/app/_components/popup/TransactionStepsHandler'; +} from '@ui/app/_components/dialogs/manage/TransactionStepsHandler'; import { donutoptions, getDonutData @@ -168,9 +168,7 @@ export default function CollateralSwapPopup({ }; const resetTransactionSteps = () => { - // refetchUsedQueries(); upsertTransactionStep(undefined); - // initiateCloseAnimation(); }; const { isConnected } = useAccount(); diff --git a/packages/ui/app/_components/dashboards/InfoRows.tsx b/packages/ui/app/_components/dashboards/InfoRows.tsx index d3a792287..3a768225b 100644 --- a/packages/ui/app/_components/dashboards/InfoRows.tsx +++ b/packages/ui/app/_components/dashboards/InfoRows.tsx @@ -16,12 +16,10 @@ import { useMerklApr } from '@ui/hooks/useMerklApr'; import { multipliers } from '@ui/utils/multipliers'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; -const Rewards = dynamic(() => import('../markets/FlyWheelRewards'), { +const FlyWheelRewards = dynamic(() => import('../markets/FlyWheelRewards'), { ssr: false }); -import BorrowPopover from '../markets/BorrowPopover'; -import SupplyPopover from '../markets/SupplyPopover'; -import { PopupMode } from '../popup/page'; +import APRCell from '../markets/APRCell'; import type { Address } from 'viem'; @@ -32,6 +30,8 @@ export enum InfoMode { BORROW = 1 } +type ActiveTab = 'borrow' | 'repay' | 'supply' | 'withdraw'; + export type InfoRowsProps = { amount: string; apr: string; @@ -45,7 +45,8 @@ export type InfoRowsProps = { comptrollerAddress: Address; rewards: FlywheelReward[]; selectedChain: number; - setPopupMode: Dispatch>; + setActiveTab: Dispatch>; + setIsManageDialogOpen: Dispatch>; setSelectedSymbol: Dispatch>; utilization: string; toggler?: () => void; @@ -59,7 +60,8 @@ const InfoRows = ({ membership, mode, setSelectedSymbol, - setPopupMode, + setActiveTab, + setIsManageDialogOpen, apr, selectedChain, cToken, @@ -73,6 +75,9 @@ const InfoRows = ({ const { data: merklApr } = useMerklApr(); const { getSdk } = useMultiIonic(); const sdk = getSdk(+selectedChain); + const type = mode === InfoMode.SUPPLY ? 'supply' : 'borrow'; + const hasFlywheelRewards = + multipliers[selectedChain]?.[pool]?.[asset]?.[type]?.flywheel; const merklAprForToken = merklApr?.find( (a) => Object.keys(a)[0].toLowerCase() === cToken.toLowerCase() @@ -87,12 +92,6 @@ const InfoRows = ({ ), [selectedChain, rewards] ); - const totalSupplyRewardsAPR = useMemo( - () => - (supplyRewards?.reduce((acc, reward) => acc + (reward.apy ?? 0), 0) ?? - 0) + (merklAprForToken ?? 0), - [supplyRewards, merklAprForToken] - ); const borrowRewards = useMemo( () => @@ -103,111 +102,79 @@ const InfoRows = ({ ), [selectedChain, rewards] ); + + const totalSupplyRewardsAPR = useMemo( + () => + (supplyRewards?.reduce((acc, reward) => acc + (reward.apy ?? 0), 0) ?? + 0) + (merklAprForToken ?? 0), + [supplyRewards, merklAprForToken] + ); + const totalBorrowRewardsAPR = useMemo( () => borrowRewards?.reduce((acc, reward) => acc + (reward.apy ?? 0), 0) ?? 0, [borrowRewards] ); + + const baseAPR = Number.parseFloat(apr.replace('%', '')); const totalApr = mode === InfoMode.BORROW - ? 0 - Number(apr) + totalBorrowRewardsAPR - : Number(apr) + totalSupplyRewardsAPR; + ? 0 - baseAPR + totalBorrowRewardsAPR + : baseAPR + totalSupplyRewardsAPR; return (
{membership && ( Collateral )} -
+
{asset} -

{asset}

+

{asset}

-

- + +

+ AMOUNT: {amount}

-

+ +

{mode === InfoMode.SUPPLY ? 'SUPPLY' : 'BORROW'} APR: -
- {mode === InfoMode.SUPPLY - ? totalApr.toLocaleString('en-US', { - maximumFractionDigits: 2 - }) - : (totalApr > 0 ? '+' : '') + - totalApr.toLocaleString('en-US', { - maximumFractionDigits: 1 - })} - % - {mode === InfoMode.SUPPLY ? ( - <> - - - ) : ( - <> - - - )} -
-

- {multipliers[selectedChain]?.[pool]?.[asset]?.borrow?.flywheel && - mode == InfoMode.BORROW ? ( - - ) : multipliers[selectedChain]?.[pool]?.[asset]?.supply?.flywheel && - mode == InfoMode.SUPPLY ? ( - + {hasFlywheelRewards ? ( + ) : (
@@ -224,9 +191,8 @@ const InfoRows = ({ ); if (result) { setSelectedSymbol(asset); - setPopupMode( - mode === InfoMode.SUPPLY ? PopupMode.SUPPLY : PopupMode.REPAY - ); + setIsManageDialogOpen(true); + setActiveTab(mode === InfoMode.SUPPLY ? 'supply' : 'repay'); } }} > @@ -253,7 +219,8 @@ const InfoRows = ({ // Router.push() // toggle the mode setSelectedSymbol(asset); - setPopupMode(PopupMode.BORROW); + setIsManageDialogOpen(true); + setActiveTab('borrow'); } } }} diff --git a/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx b/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx new file mode 100644 index 000000000..2db7be5f4 --- /dev/null +++ b/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx @@ -0,0 +1,135 @@ +import type { Dispatch, SetStateAction } from 'react'; +import React from 'react'; + +import { type Address } from 'viem'; +import { useChainId } from 'wagmi'; + +import { Slider } from '@ui/components/ui/slider'; +import { useFusePoolData } from '@ui/hooks/useFusePoolData'; +import type { MarketData } from '@ui/types/TokensDataMap'; + +import ResultHandler from '../../ResultHandler'; +import Amount from '../manage/Amount'; + +export type LoopProps = { + borrowableAssets: Address[]; + closeLoop: () => void; + comptrollerAddress: Address; + currentBorrowAsset?: MarketData; + isOpen: boolean; + selectedCollateralAsset: MarketData; +}; + +type BorrowActionsProps = { + borrowAmount?: string; + borrowableAssets: LoopProps['borrowableAssets']; + currentLeverage: number; + currentPositionLeverage?: number; + selectedBorrowAsset?: MarketData; + selectedBorrowAssetUSDPrice: number; + setCurrentLeverage: Dispatch>; + setSelectedBorrowAsset: React.Dispatch< + React.SetStateAction + >; +}; + +function BorrowActions({ + borrowAmount, + borrowableAssets, + currentLeverage, + currentPositionLeverage, + selectedBorrowAsset, + selectedBorrowAssetUSDPrice, + setCurrentLeverage, + setSelectedBorrowAsset +}: BorrowActionsProps) { + const chainId = useChainId(); + const { data: marketData, isLoading: isLoadingMarketData } = useFusePoolData( + '0', + chainId, + true + ); + const maxAllowedLoop = 3; + + const marks = Array.from({ length: 8 }, (_, i) => ({ + value: i + 2, + label: `${i + 2}x`, + isDisabled: i + 2 > maxAllowedLoop + })); + + return ( + + {selectedBorrowAsset && ( +
+
+ + borrowableAssets.find( + (borrowableAsset) => borrowableAsset === asset.cToken + ) + )} + handleInput={() => {}} + hintText="Available:" + isLoading={false} + mainText="AMOUNT TO BORROW" + max={''} + readonly + setSelectedAsset={(asset: MarketData) => + setSelectedBorrowAsset(asset) + } + symbol={selectedBorrowAsset.underlyingSymbol} + /> +
+ +
+ $ + {( + selectedBorrowAssetUSDPrice * parseFloat(borrowAmount ?? '0') + ).toFixed(2)} +
+ +
+
+ LOOP +
+ {currentLeverage.toFixed(1)}x +
+
+ +
+ { + if (value >= 2 && value <= maxAllowedLoop) { + setCurrentLeverage(value); + } + }} + onValueChange={(value) => { + const newValue = value[0]; + if (newValue >= 2 && newValue <= maxAllowedLoop) { + setCurrentLeverage(newValue); + } + }} + className="w-full" + /> + +
+ {'<'} Repay + Borrow {'>'} +
+
+
+
+ )} +
+ ); +} + +export default BorrowActions; diff --git a/packages/ui/app/_components/dialogs/loop/LoopHealthRatioDisplay.tsx b/packages/ui/app/_components/dialogs/loop/LoopHealthRatioDisplay.tsx new file mode 100644 index 000000000..3541c112d --- /dev/null +++ b/packages/ui/app/_components/dialogs/loop/LoopHealthRatioDisplay.tsx @@ -0,0 +1,82 @@ +import React, { useCallback } from 'react'; + +import { type Address } from 'viem'; + +import type { MarketData } from '@ui/types/TokensDataMap'; + +export type LoopProps = { + borrowableAssets: Address[]; + closeLoop: () => void; + comptrollerAddress: Address; + currentBorrowAsset?: MarketData; + isOpen: boolean; + selectedCollateralAsset: MarketData; +}; + +type LoopHealthRatioDisplayProps = { + currentValue: string; + healthRatio: number; + liquidationValue: string; + projectedHealthRatio?: number; +}; + +function LoopHealthRatioDisplay({ + currentValue, + healthRatio, + liquidationValue, + projectedHealthRatio +}: LoopHealthRatioDisplayProps) { + const healthRatioPosition = useCallback((value: number): number => { + if (value < 0) { + return 0; + } + + if (value > 1) { + return 100; + } + + return value * 100; + }, []); + + return ( +
+
+ Health Ratio +
+ +
+
+ +
+
+ +
+
+ ${liquidationValue} + Liquidation +
+ +
+ ${currentValue} + Current value +
+
+
+ ); +} + +export default LoopHealthRatioDisplay; diff --git a/packages/ui/app/_components/dialogs/loop/LoopInfoDisplay.tsx b/packages/ui/app/_components/dialogs/loop/LoopInfoDisplay.tsx new file mode 100644 index 000000000..51b0f622e --- /dev/null +++ b/packages/ui/app/_components/dialogs/loop/LoopInfoDisplay.tsx @@ -0,0 +1,85 @@ +import React from 'react'; + +import Image from 'next/image'; + +import { type Address } from 'viem'; + +import type { MarketData } from '@ui/types/TokensDataMap'; + +import ResultHandler from '../../ResultHandler'; + +export type LoopProps = { + borrowableAssets: Address[]; + closeLoop: () => void; + comptrollerAddress: Address; + currentBorrowAsset?: MarketData; + isOpen: boolean; + selectedCollateralAsset: MarketData; +}; + +type LoopInfoDisplayProps = { + aprPercentage?: string; + aprText?: string; + isLoading: boolean; + nativeAmount: string; + symbol: string; + title: string; + usdAmount: string; +}; + +function LoopInfoDisplay({ + aprText, + aprPercentage, + isLoading, + nativeAmount, + symbol, + title, + usdAmount +}: LoopInfoDisplayProps) { + return ( +
+
{title}
+ +
+
+ + {nativeAmount} $ + {usdAmount} + +
+ +
+ + + {symbol} +
+
+ + {aprText && aprPercentage && ( +
+ {aprText} + + + {aprPercentage} + +
+ )} +
+ ); +} + +export default LoopInfoDisplay; diff --git a/packages/ui/app/_components/dialogs/loop/SupplyActions.tsx b/packages/ui/app/_components/dialogs/loop/SupplyActions.tsx new file mode 100644 index 000000000..e9dbfe9d1 --- /dev/null +++ b/packages/ui/app/_components/dialogs/loop/SupplyActions.tsx @@ -0,0 +1,162 @@ +import React, { useEffect, useState } from 'react'; + +import { type Address, formatUnits } from 'viem'; +import { useChainId } from 'wagmi'; + +import { useMaxSupplyAmount } from '@ui/hooks/useMaxSupplyAmount'; +import type { MarketData } from '@ui/types/TokensDataMap'; + +import Amount from '../manage/Amount'; + +export type LoopProps = { + borrowableAssets: Address[]; + closeLoop: () => void; + comptrollerAddress: Address; + currentBorrowAsset?: MarketData; + isOpen: boolean; + selectedCollateralAsset: MarketData; +}; + +type SupplyActionsProps = { + amount?: string; + comptrollerAddress: LoopProps['comptrollerAddress']; + handleClosePosition: () => void; + isClosingPosition: boolean; + selectedCollateralAsset: LoopProps['selectedCollateralAsset']; + selectedCollateralAssetUSDPrice: number; + setAmount: React.Dispatch>; +}; + +enum SupplyActionsMode { + DEPOSIT, + WITHDRAW +} + +function SupplyActions({ + amount, + comptrollerAddress, + handleClosePosition, + isClosingPosition, + selectedCollateralAsset, + selectedCollateralAssetUSDPrice, + setAmount +}: SupplyActionsProps) { + const chainId = useChainId(); + const [mode, setMode] = useState( + SupplyActionsMode.DEPOSIT + ); + const [utilization, setUtilization] = useState(0); + const { data: maxSupplyAmount, isLoading: isLoadingMaxSupply } = + useMaxSupplyAmount(selectedCollateralAsset, comptrollerAddress, chainId); + + const handleSupplyUtilization = (utilizationPercentage: number) => { + if (utilizationPercentage >= 100) { + setAmount( + formatUnits( + maxSupplyAmount?.bigNumber ?? 0n, + parseInt(selectedCollateralAsset.underlyingDecimals.toString()) + ) + ); + + return; + } + + setAmount( + ((utilizationPercentage / 100) * (maxSupplyAmount?.number ?? 0)).toFixed( + parseInt(selectedCollateralAsset.underlyingDecimals.toString()) + ) + ); + }; + + useEffect(() => { + switch (mode) { + case SupplyActionsMode.DEPOSIT: + setUtilization( + Math.round( + (parseFloat(amount ?? '0') / + (maxSupplyAmount && maxSupplyAmount.number > 0 + ? maxSupplyAmount.number + : 1)) * + 100 + ) + ); + + break; + + case SupplyActionsMode.WITHDRAW: + break; + } + }, [amount, maxSupplyAmount, mode]); + + return ( +
+
+
setMode(SupplyActionsMode.DEPOSIT)} + > + Deposit +
+ +
setMode(SupplyActionsMode.WITHDRAW)} + > + Withdraw +
+
+ + {mode === SupplyActionsMode.DEPOSIT && ( + <> + setAmount(val)} + hintText="Available:" + isLoading={isLoadingMaxSupply} + mainText="AMOUNT TO DEPOSIT" + max={formatUnits( + maxSupplyAmount?.bigNumber ?? 0n, + selectedCollateralAsset.underlyingDecimals + )} + symbol={selectedCollateralAsset.underlyingSymbol} + currentUtilizationPercentage={utilization} + handleUtilization={handleSupplyUtilization} + /> + +
+ $ + {( + selectedCollateralAssetUSDPrice * parseFloat(amount ?? '0') + ).toFixed(2)} +
+ + )} + + {mode === SupplyActionsMode.WITHDRAW && ( +
+

+ Click the button to withdraw your funds +

+ + +
+ )} +
+ ); +} + +export default SupplyActions; diff --git a/packages/ui/app/_components/dialogs/loop/index.tsx b/packages/ui/app/_components/dialogs/loop/index.tsx new file mode 100644 index 000000000..4ef247a21 --- /dev/null +++ b/packages/ui/app/_components/dialogs/loop/index.tsx @@ -0,0 +1,882 @@ +import React, { useEffect, useMemo, useState } from 'react'; + +import dynamic from 'next/dynamic'; +import Image from 'next/image'; + +import { useQueryClient } from '@tanstack/react-query'; +import millify from 'millify'; +import { + type Address, + formatEther, + formatUnits, + parseEther, + parseUnits +} from 'viem'; +import { useBalance, useChainId } from 'wagmi'; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle +} from '@ui/components/ui/dialog'; +import { INFO_MESSAGES } from '@ui/constants/index'; +import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import { useCurrentLeverageRatio } from '@ui/hooks/leverage/useCurrentLeverageRatio'; +import { useGetNetApy } from '@ui/hooks/leverage/useGetNetApy'; +import { useGetPositionBorrowApr } from '@ui/hooks/leverage/useGetPositionBorrowApr'; +import { usePositionInfo } from '@ui/hooks/leverage/usePositionInfo'; +import { usePositionsQuery } from '@ui/hooks/leverage/usePositions'; +import { usePositionsSupplyApy } from '@ui/hooks/leverage/usePositionsSupplyApy'; +import { useUsdPrice } from '@ui/hooks/useAllUsdPrices'; +import { useFusePoolData } from '@ui/hooks/useFusePoolData'; +import type { MarketData } from '@ui/types/TokensDataMap'; +import { getScanUrlByChainId } from '@ui/utils/networkData'; + +import BorrowActions from './BorrowActions'; +import LoopHealthRatioDisplay from './LoopHealthRatioDisplay'; +import LoopInfoDisplay from './LoopInfoDisplay'; +import SupplyActions from './SupplyActions'; +import ResultHandler from '../../ResultHandler'; +import TransactionStepsHandler, { + useTransactionSteps +} from '../manage/TransactionStepsHandler'; + +import type { OpenPosition } from '@ionicprotocol/types'; + +const SwapWidget = dynamic(() => import('../../markets/SwapWidget'), { + ssr: false +}); + +export type LoopProps = { + isOpen: boolean; + setIsOpen: (open: boolean) => void; + borrowableAssets: Address[]; + comptrollerAddress: Address; + currentBorrowAsset?: MarketData; + selectedCollateralAsset: MarketData; +}; + +export default function Loop({ + isOpen, + setIsOpen, + borrowableAssets, + comptrollerAddress, + currentBorrowAsset, + selectedCollateralAsset +}: LoopProps) { + const chainId = useChainId(); + const [amount, setAmount] = useState(); + const amountAsBInt = useMemo( + () => parseUnits(amount ?? '0', selectedCollateralAsset.underlyingDecimals), + [amount, selectedCollateralAsset] + ); + const [swapWidgetOpen, setSwapWidgetOpen] = useState(false); + const { data: marketData } = useFusePoolData('0', chainId, true); + const { data: usdPrice } = useUsdPrice(chainId.toString()); + const [selectedBorrowAsset, setSelectedBorrowAsset] = useState< + MarketData | undefined + >(currentBorrowAsset); + const { data: positions, refetch: refetchPositions } = + usePositionsQuery(chainId); + const currentPosition = useMemo(() => { + return positions?.openPositions.find( + (position) => + position.borrowable.underlyingToken === + selectedBorrowAsset?.underlyingToken && + position.collateral.underlyingToken === + selectedCollateralAsset.underlyingToken && + !position.isClosed + ); + }, [positions, selectedBorrowAsset, selectedCollateralAsset]); + const { data: currentPositionLeverageRatio } = useCurrentLeverageRatio( + currentPosition?.address ?? ('' as Address), + chainId + ); + const collateralsAPR = usePositionsSupplyApy( + positions?.openPositions.map((position) => position.collateral) ?? [], + positions?.openPositions.map((position) => position.chainId) ?? [] + ); + const { + data: positionInfo, + isFetching: isFetchingPositionInfo, + refetch: refetchPositionInfo + } = usePositionInfo( + currentPosition?.address ?? ('' as Address), + collateralsAPR && + collateralsAPR[selectedCollateralAsset.cToken] !== undefined + ? parseEther( + collateralsAPR[selectedCollateralAsset.cToken].totalApy.toFixed(18) + ) + : undefined, + chainId + ); + const { data: positionNetApy, isFetching: isFetchingPositionNetApy } = + useGetNetApy( + selectedCollateralAsset.cToken, + selectedBorrowAsset?.cToken ?? ('' as Address), + positionInfo?.equityAmount, + currentPositionLeverageRatio, + collateralsAPR && + collateralsAPR[selectedCollateralAsset.cToken] !== undefined + ? parseEther( + collateralsAPR[selectedCollateralAsset.cToken].totalApy.toFixed(18) + ) + : undefined, + chainId + ); + const [currentLeverage, setCurrentLeverage] = useState(2); + const { data: borrowApr } = useGetPositionBorrowApr({ + amount: amountAsBInt, + borrowMarket: selectedBorrowAsset?.cToken ?? ('' as Address), + collateralMarket: selectedCollateralAsset.cToken, + leverage: parseEther(currentLeverage.toString()) + }); + + const { + borrowedAssetAmount, + borrowedToCollateralRatio, + positionValueMillified, + projectedHealthRatio, + liquidationValue, + healthRatio, + projectedCollateral, + projectedBorrowAmount, + projectedCollateralValue, + selectedBorrowAssetUSDPrice, + selectedCollateralAssetUSDPrice + } = useMemo(() => { + const selectedCollateralAssetUSDPrice = + (usdPrice ?? 0) * + parseFloat(formatEther(selectedCollateralAsset.underlyingPrice)); + const selectedBorrowAssetUSDPrice = + usdPrice && selectedBorrowAsset + ? (usdPrice ?? 0) * + parseFloat(formatEther(selectedBorrowAsset.underlyingPrice)) + : 0; + const positionValue = + Number(formatEther(positionInfo?.positionSupplyAmount ?? 0n)) * + (selectedCollateralAssetUSDPrice ?? 0); + const liquidationValue = + positionValue * Number(formatEther(positionInfo?.safetyBuffer ?? 0n)); + const healthRatio = positionValue / liquidationValue - 1; + const borrowedToCollateralRatio = + selectedBorrowAssetUSDPrice / selectedCollateralAssetUSDPrice; + const borrowedAssetAmount = Number( + formatUnits( + positionInfo?.debtAmount ?? 0n, + currentPosition?.borrowable.underlyingDecimals ?? 18 + ) + ); + const projectedCollateral = formatUnits( + positionInfo?.equityAmount ?? 0n + amountAsBInt, + selectedCollateralAsset.underlyingDecimals + ); + const projectedCollateralValue = + Number(projectedCollateral) * selectedCollateralAssetUSDPrice; + const projectedBorrowAmount = + (Number(projectedCollateral) / borrowedToCollateralRatio) * + (currentLeverage - 1); + const projectedHealthRatio = currentPosition + ? (projectedCollateralValue + + projectedBorrowAmount * selectedBorrowAssetUSDPrice) / + liquidationValue - + 1 + : undefined; + + return { + borrowedAssetAmount, + borrowedToCollateralRatio, + healthRatio, + liquidationValue, + positionValue, + positionValueMillified: `${millify(positionValue)}`, + projectedBorrowAmount, + projectedCollateral, + projectedCollateralValue, + projectedHealthRatio, + selectedBorrowAssetUSDPrice, + selectedCollateralAssetUSDPrice + }; + }, [ + amountAsBInt, + currentLeverage, + currentPosition, + selectedBorrowAsset, + selectedCollateralAsset, + positionInfo, + usdPrice + ]); + const { currentSdk, address } = useMultiIonic(); + const { addStepsForAction, transactionSteps, upsertTransactionStep } = + useTransactionSteps(); + const { refetch: refetchBalance } = useBalance({ + address, + token: selectedCollateralAsset.underlyingToken as `0x${string}` + }); + const queryClient = useQueryClient(); + + /** + * Force new borrow asset + * when currentBorrowAsset + * is present + */ + useEffect(() => { + if (currentBorrowAsset && isOpen) { + setSelectedBorrowAsset(currentBorrowAsset); + } + }, [currentBorrowAsset, isOpen]); + + /** + * Update selected borrow asset + * when market data loads + */ + useEffect(() => { + if (!selectedBorrowAsset && marketData) { + setSelectedBorrowAsset( + marketData.assets.filter((asset) => + borrowableAssets.find( + (borrowableAsset) => borrowableAsset === asset.cToken + ) + )[0] + ); + } + }, [borrowableAssets, marketData, selectedBorrowAsset]); + + /** + * Reset neccessary queries after actions + */ + const resetQueries = async (): Promise => { + queryClient.invalidateQueries({ queryKey: ['useCurrentLeverageRatio'] }); + queryClient.invalidateQueries({ queryKey: ['useGetNetApy'] }); + queryClient.invalidateQueries({ queryKey: ['usePositionInfo'] }); + queryClient.invalidateQueries({ queryKey: ['positions'] }); + queryClient.invalidateQueries({ queryKey: ['useMaxSupplyAmount'] }); + await refetchBalance(); + await refetchPositionInfo(); + await refetchPositions(); + }; + + /** + * Handle position opening + */ + const handleOpenPosition = async (): Promise => { + if (!currentSdk || !address) { + return; + } + + let currentTransactionStep = 0; + + addStepsForAction([ + { + error: false, + message: INFO_MESSAGES.OPEN_POSITION.APPROVE, + success: false + }, + { + error: false, + message: INFO_MESSAGES.OPEN_POSITION.OPENING, + success: false + } + ]); + + try { + const token = currentSdk.getEIP20TokenInstance( + selectedCollateralAsset.underlyingToken, + currentSdk.publicClient as any + ); + const factory = currentSdk.createLeveredPositionFactory(); + const hasApprovedEnough = + (await token.read.allowance([address, factory.address])) >= + amountAsBInt; + + if (!hasApprovedEnough) { + const tx = await currentSdk.approve( + factory.address, + selectedCollateralAsset.underlyingToken, + amountAsBInt + ); + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + txHash: tx + } + }); + + await currentSdk.publicClient.waitForTransactionReceipt({ hash: tx }); + } + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + success: true + } + }); + + currentTransactionStep++; + + const tx = await currentSdk.createAndFundPositionAtRatio( + selectedCollateralAsset.cToken, + selectedBorrowAsset?.cToken ?? ('' as Address), + selectedCollateralAsset.underlyingToken, + amountAsBInt, + parseEther(currentLeverage.toString()) + ); + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + txHash: tx + } + }); + + await currentSdk.publicClient.waitForTransactionReceipt({ hash: tx }); + await refetchPositions(); + setAmount('0'); + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + success: true + } + }); + } catch (error) { + console.error(error); + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + error: true + } + }); + } + }; + + /** + * Handle leverage adjustment + */ + const handleLeverageAdjustment = async (): Promise => { + const currentTransactionStep = 0; + + addStepsForAction([ + { + error: false, + message: INFO_MESSAGES.ADJUST_LEVERAGE.ADJUSTING, + success: false + } + ]); + + try { + const tx = await currentSdk?.adjustLeverageRatio( + currentPosition?.address ?? ('' as Address), + currentLeverage + ); + + if (!tx) { + throw new Error('Error while adjusting leverage'); + } + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + txHash: tx + } + }); + + await currentSdk?.publicClient.waitForTransactionReceipt({ hash: tx }); + await refetchPositions(); + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + success: true + } + }); + } catch (error) { + console.error(error); + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + error: true + } + }); + } + }; + + /** + * Handle position funding + */ + const handlePositionFunding = async (): Promise => { + if (!currentSdk || !address || !currentPosition) { + return; + } + + let currentTransactionStep = 0; + + addStepsForAction([ + { + error: false, + message: INFO_MESSAGES.FUNDING_POSITION.APPROVE, + success: false + }, + { + error: false, + message: INFO_MESSAGES.FUNDING_POSITION.FUNDING, + success: false + } + ]); + + try { + const token = currentSdk.getEIP20TokenInstance( + selectedCollateralAsset.underlyingToken, + currentSdk.walletClient as any + ); + const hasApprovedEnough = + (await token.read.allowance([address, currentPosition.address])) >= + amountAsBInt; + + if (!hasApprovedEnough) { + const tx = await currentSdk.approve( + currentPosition.address, + selectedCollateralAsset.underlyingToken, + amountAsBInt + ); + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + txHash: tx + } + }); + + await currentSdk.publicClient.waitForTransactionReceipt({ hash: tx }); + } + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + success: true + } + }); + + currentTransactionStep++; + + const tx = await currentSdk.fundPosition( + currentPosition?.address ?? '', + selectedCollateralAsset.underlyingToken, + amountAsBInt + ); + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + txHash: tx + } + }); + + await currentSdk.publicClient.waitForTransactionReceipt({ hash: tx }); + + setAmount('0'); + await refetchPositions(); + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + success: true + } + }); + } catch (error) { + console.error(error); + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + error: true + } + }); + } + }; + + /** + * Handle position closing + */ + const handleClosePosition = async (): Promise => { + const currentTransactionStep = 0; + + addStepsForAction([ + { + error: false, + message: INFO_MESSAGES.CLOSE_POSITION.CLOSING, + success: false + } + ]); + + try { + const tx = await currentSdk?.closeLeveredPosition( + currentPosition?.address ?? ('' as Address) + ); + + if (!tx) { + throw new Error('Error while closing position'); + } + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + txHash: tx + } + }); + + await currentSdk?.publicClient.waitForTransactionReceipt({ hash: tx }); + + await refetchPositions(); + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + success: true + } + }); + } catch (error) { + console.error(error); + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + error: true + } + }); + } + }; + + /** + * Handle transaction steps reset + */ + const handleTransactionStepsReset = async (): Promise => { + resetQueries(); + upsertTransactionStep(undefined); + }; + + return ( + <> + setSwapWidgetOpen(false)} + open={swapWidgetOpen} + fromChain={chainId} + toChain={chainId} + toToken={selectedCollateralAsset.underlyingToken} + /> + + + + +
+ + {selectedCollateralAsset.underlyingSymbol} +
+
+
+ +
+ {currentPosition + ? `Loop Position Found: ` + : 'No Loop Position Found, Create a New One'} + {currentPosition && ( + + 0x{currentPosition.address.slice(2, 4)}... + {currentPosition.address.slice(-6)} + + )} +
+ + +
+
+
+ Position Value + + + ${positionValueMillified} + + +
+
+ Net APR + + + {positionNetApy?.toFixed(2) ?? '0.00'}% + + +
+ + {/*
+ Annual yield + + TODO + +
*/} +
+ +
+ +
+ + 0 || + (!!currentPositionLeverageRatio && + Math.round(currentPositionLeverageRatio) !== currentLeverage) + ? projectedHealthRatio + : undefined + } + /> +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + {currentPosition && ( + <> +
+ + +
+ +
+ + +
+ +
+ + )} + +
+ + +
+ +
+ + +
+ +
+ + <> + {transactionSteps.length > 0 ? ( +
+ +
+ ) : ( + <> + {currentPosition ? ( +
+ + + +
+ ) : ( + + )} + + )} + +
+
+ +
+ + ); +} diff --git a/packages/ui/app/_components/dialogs/manage/Amount.tsx b/packages/ui/app/_components/dialogs/manage/Amount.tsx new file mode 100644 index 000000000..e8e0679c9 --- /dev/null +++ b/packages/ui/app/_components/dialogs/manage/Amount.tsx @@ -0,0 +1,292 @@ +import React, { useState } from 'react'; + +import { Slider } from '@ui/components/ui/slider'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from '@ui/components/ui/tooltip'; +import { cn } from '@ui/lib/utils'; +import type { MarketData } from '@ui/types/TokensDataMap'; + +import ResultHandler from '../../ResultHandler'; + +interface IAmount { + amount?: string; + availableAssets?: MarketData[]; + handleInput: (val?: string) => void; + hintText?: string; + isLoading?: boolean; + mainText?: string; + max?: string; + readonly?: boolean; + setSelectedAsset?: (asset: MarketData) => void; + symbol: string; + currentUtilizationPercentage?: number; + handleUtilization?: (val: number) => void; +} + +const AmountInput = ({ + mainText, + handleInput, + readonly, + amount, + max, + isLoading +}: { + mainText?: string; + handleInput: (val?: string) => void; + readonly?: boolean; + amount?: string; + max?: string; + isLoading?: boolean; +}) => { + const isDisabled = readonly || max === '0' || isLoading; + + const handleChange = (e: React.ChangeEvent) => { + const value = e.target.value; + if (value === '') { + handleInput(undefined); + return; + } + + const numValue = parseFloat(value); + const maxValue = parseFloat(max || '0'); + + if (numValue > maxValue) { + handleInput(max); + return; + } + + handleInput(value); + }; + + return ( +
+
{mainText}
+ +
+ ); +}; + +const AssetSelector = ({ + symbol, + availableAssets, + onClick, + children +}: { + symbol: string; + availableAssets: any; + onClick: () => void; + children: React.ReactNode; +}) => ( +
+ {children} + +
+ {symbol} + {symbol} + {availableAssets && ( + dropdown + )} +
+
+); + +const UtilizationSlider = ({ + currentUtilizationPercentage, + handleUtilization, + max +}: { + currentUtilizationPercentage: number; + handleUtilization?: (val: number) => void; + max: string; +}) => { + const isDisabled = max === '0'; + + return ( + + + +
+ + !isDisabled && handleUtilization?.(value[0]) + } + disabled={isDisabled} + className="w-full" + /> +
+
+ {isDisabled && ( + +

No balance available

+
+ )} +
+
+ ); +}; + +const Amount = ({ + handleInput, + amount, + availableAssets, + hintText = 'Balance', + mainText = 'Token Amount', + max = '0', + symbol, + isLoading = false, + readonly, + setSelectedAsset, + currentUtilizationPercentage, + handleUtilization +}: IAmount) => { + const [availableAssetsOpen, setAvailableAssetsOpen] = useState(false); + + const MaxButton = ( + + + + ); + + const AssetsDropdown = availableAssets && ( +
+ {availableAssets.map((asset) => ( +
{ + setSelectedAsset?.(asset); + setAvailableAssetsOpen(false); + }} + > + {asset.underlyingSymbol} + {asset.underlyingSymbol} +
+ ))} +
+ ); + + return ( +
+ {/* Mobile Layout */} +
+
+ + setAvailableAssetsOpen(!availableAssetsOpen)} + > + {MaxButton} + +
+ + {currentUtilizationPercentage !== undefined && ( + + )} +
+ + {/* Desktop Layout */} +
+ + + {currentUtilizationPercentage !== undefined && ( +
+ +
+ )} + + setAvailableAssetsOpen(!availableAssetsOpen)} + > + {MaxButton} + +
+ + {AssetsDropdown} +
+ ); +}; + +export default Amount; diff --git a/packages/ui/app/_components/popup/Approved.tsx b/packages/ui/app/_components/dialogs/manage/Approved.tsx similarity index 100% rename from packages/ui/app/_components/popup/Approved.tsx rename to packages/ui/app/_components/dialogs/manage/Approved.tsx diff --git a/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx b/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx new file mode 100644 index 000000000..a4bf608c9 --- /dev/null +++ b/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx @@ -0,0 +1,218 @@ +import { useEffect, useMemo } from 'react'; + +import { Info } from 'lucide-react'; +import { formatUnits } from 'viem'; + +import { Alert, AlertDescription } from '@ui/components/ui/alert'; +import { Button } from '@ui/components/ui/button'; +import { + HFPStatus, + TransactionType, + useManageDialogContext +} from '@ui/context/ManageDialogContext'; +import { useBorrow } from '@ui/hooks/market/useBorrow'; +import { useHealth } from '@ui/hooks/market/useHealth'; + +import Amount from './Amount'; +import StatusAlerts from './StatusAlerts'; +import TransactionStepsHandler from './TransactionStepsHandler'; +import ResultHandler from '../../ResultHandler'; +import MemoizedUtilizationStats from '../../UtilizationStats'; + +interface BorrowTabProps { + maxAmount: bigint; + isLoadingMax: boolean; + totalStats?: { + capAmount: number; + totalAmount: number; + capFiat: number; + totalFiat: number; + }; +} + +const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { + const { + selectedMarketData, + resetTransactionSteps, + chainId, + isLoadingUpdatedAssets, + updatedValues, + comptrollerAddress, + setPredictionAmount, + getStepsForTypes + } = useManageDialogContext(); + + const { + isWaitingForIndexing, + borrowAmount, + isPolling, + borrowLimits, + isUnderMinBorrow, + amount, + setAmount, + utilizationPercentage, + handleUtilization, + amountAsBInt + } = useBorrow({ + selectedMarketData, + chainId, + comptrollerAddress + }); + + const { isLoadingPredictedHealthFactor, healthFactor, hfpStatus } = useHealth( + { + comptrollerAddress, + cToken: selectedMarketData.cToken, + activeTab: 'borrow', + amount: amountAsBInt, + exchangeRate: selectedMarketData.exchangeRate, + decimals: selectedMarketData.underlyingDecimals + } + ); + + const isDisabled = + !amount || + amountAsBInt === 0n || + isLoadingPredictedHealthFactor || + hfpStatus === HFPStatus.CRITICAL || + hfpStatus === HFPStatus.UNKNOWN; + + useEffect(() => { + setPredictionAmount(amountAsBInt); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [amountAsBInt]); + + const transactionSteps = useMemo(() => { + return getStepsForTypes(TransactionType.BORROW); + }, [getStepsForTypes]); + + return ( +
+ setAmount(val ?? '')} + isLoading={isLoadingMax || isPolling} + max={formatUnits(maxAmount, selectedMarketData.underlyingDecimals)} + symbol={selectedMarketData.underlyingSymbol} + currentUtilizationPercentage={utilizationPercentage} + handleUtilization={handleUtilization} + /> + + {isUnderMinBorrow && ( + +
+ + + Amount must be greater than minimum borrow amount ( + {borrowLimits.min} {selectedMarketData.underlyingSymbol}) + +
+
+ )} + + + +
+
+
+ MIN BORROW + {borrowLimits.min} +
+ +
+ MAX BORROW + {borrowLimits.max} +
+ +
+ CURRENTLY BORROWING +
+ {updatedValues.borrowBalanceFrom} + + + {updatedValues.borrowBalanceTo} + +
+
+ +
+ Market Borrow APR +
+ {updatedValues.borrowAPR?.toFixed(2)}% + + + {updatedValues.updatedBorrowAPR?.toFixed(2)}% + +
+
+ +
+ Health Factor +
+ {healthFactor.current} + + + {healthFactor.predicted} + +
+
+
+ + {totalStats && ( + + )} +
+ + {transactionSteps.length > 0 ? ( +
+ +
+ ) : ( + + )} +
+ ); +}; + +export default BorrowTab; diff --git a/packages/ui/app/_components/dialogs/manage/DialogWrapper.tsx b/packages/ui/app/_components/dialogs/manage/DialogWrapper.tsx new file mode 100644 index 000000000..26b8a6e2c --- /dev/null +++ b/packages/ui/app/_components/dialogs/manage/DialogWrapper.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { Dialog } from '@ui/components/ui/dialog'; +import { useManageDialogContext } from '@ui/context/ManageDialogContext'; + +import type { ActiveTab } from '.'; + +const DialogWrapper = ({ + isOpen, + setIsOpen, + children, + setCurrentActiveTab +}: { + isOpen: boolean; + setIsOpen: (open: boolean) => void; + children: React.ReactNode; + setCurrentActiveTab: (tab: ActiveTab) => void; +}) => { + const { resetTransactionSteps } = useManageDialogContext(); + + return ( + { + setIsOpen(open); + if (!open) { + resetTransactionSteps(); + setCurrentActiveTab('supply'); + } + }} + > + {children} + + ); +}; + +export default DialogWrapper; diff --git a/packages/ui/app/_components/popup/DonutChart.tsx b/packages/ui/app/_components/dialogs/manage/DonutChart.tsx similarity index 100% rename from packages/ui/app/_components/popup/DonutChart.tsx rename to packages/ui/app/_components/dialogs/manage/DonutChart.tsx diff --git a/packages/ui/app/_components/dialogs/manage/ManageDialogTabs.tsx b/packages/ui/app/_components/dialogs/manage/ManageDialogTabs.tsx new file mode 100644 index 000000000..4d64cff8e --- /dev/null +++ b/packages/ui/app/_components/dialogs/manage/ManageDialogTabs.tsx @@ -0,0 +1,248 @@ +'use client'; +import { useMemo } from 'react'; + +import Image from 'next/image'; + +import { type Address, formatEther, formatUnits } from 'viem'; + +import { DialogContent } from '@ui/components/ui/dialog'; +import { + Tabs, + TabsList, + TabsTrigger, + TabsContent +} from '@ui/components/ui/tabs'; +import { useManageDialogContext } from '@ui/context/ManageDialogContext'; +import { useBorrowCapsDataForAsset } from '@ui/hooks/ionic/useBorrowCapsDataForAsset'; +import { useSupplyCapsDataForAsset } from '@ui/hooks/ionic/useSupplyCapsDataForPool'; +import { useUsdPrice } from '@ui/hooks/useAllUsdPrices'; +import { useMaxBorrowAmount } from '@ui/hooks/useMaxBorrowAmount'; +import { useMaxRepayAmount } from '@ui/hooks/useMaxRepayAmount'; +import { useMaxSupplyAmount } from '@ui/hooks/useMaxSupplyAmount'; +import { useMaxWithdrawAmount } from '@ui/hooks/useMaxWithdrawAmount'; +import type { MarketData } from '@ui/types/TokensDataMap'; + +import BorrowTab from './BorrowTab'; +import RepayTab from './RepayTab'; +import SupplyTab from './SupplyTab'; +import WithdrawTab from './WithdrawTab'; +import AnimateHeight from '../../AnimateHeight'; + +import type { ActiveTab } from '.'; + +const ManageDialogTabs = ({ + selectedMarketData, + comptrollerAddress, + isBorrowDisabled, + currentActiveTab, + setCurrentActiveTab, + setSwapWidgetOpen, + chainId +}: { + selectedMarketData: MarketData; + comptrollerAddress: Address; + isBorrowDisabled: boolean; + currentActiveTab: ActiveTab; + setCurrentActiveTab: (tab: ActiveTab) => void; + setSwapWidgetOpen: (open: boolean) => void; + chainId: number; +}) => { + const { data: usdPrice } = useUsdPrice(chainId.toString()); + const { data: maxSupplyAmount, isLoading: isLoadingMaxSupply } = + useMaxSupplyAmount(selectedMarketData, comptrollerAddress, chainId); + + const { data: maxRepayAmount, isLoading: isLoadingMaxRepayAmount } = + useMaxRepayAmount(selectedMarketData, chainId); + + const { data: maxBorrowAmount, isLoading: isLoadingMaxBorrowAmount } = + useMaxBorrowAmount(selectedMarketData, comptrollerAddress, chainId); + + const { data: maxWithdrawAmount, isLoading: isLoadingMaxWithdrawAmount } = + useMaxWithdrawAmount(selectedMarketData, chainId); + + // Memoize calculations + const pricePerSingleAsset = useMemo( + () => + parseFloat(formatEther(selectedMarketData.underlyingPrice)) * + (usdPrice ?? 0), + [selectedMarketData.underlyingPrice, usdPrice] + ); + + const { data: supplyCap } = useSupplyCapsDataForAsset( + comptrollerAddress, + selectedMarketData.cToken, + chainId + ); + + const supplyCapAsNumber = useMemo( + () => + parseFloat( + formatUnits( + supplyCap?.supplyCaps ?? 0n, + selectedMarketData.underlyingDecimals + ) + ), + [supplyCap?.supplyCaps, selectedMarketData.underlyingDecimals] + ); + + const supplyCapAsFiat = useMemo( + () => pricePerSingleAsset * supplyCapAsNumber, + [pricePerSingleAsset, supplyCapAsNumber] + ); + + const totalSupplyAsNumber = useMemo( + () => + parseFloat( + formatUnits( + selectedMarketData.totalSupply, + selectedMarketData.underlyingDecimals + ) + ), + [selectedMarketData.totalSupply, selectedMarketData.underlyingDecimals] + ); + + const { data: borrowCap } = useBorrowCapsDataForAsset( + selectedMarketData.cToken, + chainId + ); + + const borrowCapAsNumber = useMemo( + () => + parseFloat( + formatUnits( + borrowCap?.totalBorrowCap ?? 0n, + selectedMarketData.underlyingDecimals + ) + ), + [borrowCap?.totalBorrowCap, selectedMarketData.underlyingDecimals] + ); + + const borrowCapAsFiat = useMemo( + () => pricePerSingleAsset * borrowCapAsNumber, + [pricePerSingleAsset, borrowCapAsNumber] + ); + + const totalBorrowAsNumber = useMemo( + () => + parseFloat( + formatUnits( + selectedMarketData.totalBorrow, + selectedMarketData.underlyingDecimals + ) + ), + [selectedMarketData.totalBorrow, selectedMarketData.underlyingDecimals] + ); + + const TabsComponent = () => { + const { setActive } = useManageDialogContext(); + + const handleTabChange = (value: string) => { + const newTab = value as ActiveTab; + setCurrentActiveTab(newTab); + setActive(newTab); + }; + + return ( +
+ + + Supply + + Borrow + + + Repay + + Withdraw + + + + + + + + + + + + + + + +
+ ); + }; + + return ( + +
+ modlogo +
+ + + +
+ ); +}; + +export default ManageDialogTabs; diff --git a/packages/ui/app/_components/dialogs/manage/RepayTab.tsx b/packages/ui/app/_components/dialogs/manage/RepayTab.tsx new file mode 100644 index 000000000..7c0707d0b --- /dev/null +++ b/packages/ui/app/_components/dialogs/manage/RepayTab.tsx @@ -0,0 +1,183 @@ +import { useEffect, useMemo } from 'react'; + +import { formatUnits } from 'viem'; + +import { Button } from '@ui/components/ui/button'; +import { + HFPStatus, + TransactionType, + useManageDialogContext +} from '@ui/context/ManageDialogContext'; +import { useHealth } from '@ui/hooks/market/useHealth'; +import { useRepay } from '@ui/hooks/market/useRepay'; + +import Amount from './Amount'; +import StatusAlerts from './StatusAlerts'; +import TransactionStepsHandler from './TransactionStepsHandler'; +import ResultHandler from '../../ResultHandler'; +import MemoizedUtilizationStats from '../../UtilizationStats'; + +interface RepayTabProps { + maxAmount: bigint; + isLoadingMax: boolean; + totalStats?: { + capAmount: number; + totalAmount: number; + capFiat: number; + totalFiat: number; + }; +} + +const RepayTab = ({ maxAmount, isLoadingMax, totalStats }: RepayTabProps) => { + const { + selectedMarketData, + resetTransactionSteps, + chainId, + isLoadingUpdatedAssets, + updatedValues, + comptrollerAddress, + setPredictionAmount, + getStepsForTypes + } = useManageDialogContext(); + + const { + isWaitingForIndexing, + repayAmount, + isPolling, + currentBorrowAmountAsFloat, + amount, + setAmount, + utilizationPercentage, + handleUtilization, + amountAsBInt + } = useRepay({ + maxAmount, + selectedMarketData, + chainId + }); + + const { healthFactor, hfpStatus } = useHealth({ + comptrollerAddress, + cToken: selectedMarketData.cToken, + activeTab: 'repay', + amount: amountAsBInt, + exchangeRate: selectedMarketData.exchangeRate, + decimals: selectedMarketData.underlyingDecimals + }); + + useEffect(() => { + setPredictionAmount(amountAsBInt); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [amountAsBInt]); + + const transactionSteps = useMemo(() => { + return getStepsForTypes(TransactionType.REPAY); + }, [getStepsForTypes]); + + return ( +
+ setAmount(val ?? '')} + isLoading={isLoadingMax || isPolling} + max={formatUnits(maxAmount, selectedMarketData.underlyingDecimals)} + symbol={selectedMarketData.underlyingSymbol} + currentUtilizationPercentage={utilizationPercentage} + handleUtilization={handleUtilization} + /> + + + +
+
+
+ CURRENTLY BORROWING +
+ + {updatedValues.borrowBalanceFrom} + + + + + {updatedValues.borrowBalanceTo} + + +
+
+ +
+ Market Borrow APR +
+ {updatedValues.borrowAPR?.toFixed(2)}% + + + {updatedValues.updatedBorrowAPR?.toFixed(2)}% + +
+
+ +
+ Health Factor +
+ {healthFactor.current} + + + {healthFactor.predicted} + +
+
+
+ + {totalStats && ( + + )} +
+ + {transactionSteps.length > 0 ? ( +
+ +
+ ) : ( + + )} +
+ ); +}; + +export default RepayTab; diff --git a/packages/ui/app/_components/popup/Slider.tsx b/packages/ui/app/_components/dialogs/manage/Slider.tsx similarity index 65% rename from packages/ui/app/_components/popup/Slider.tsx rename to packages/ui/app/_components/dialogs/manage/Slider.tsx index 0cc0fb9cc..c31b64582 100644 --- a/packages/ui/app/_components/popup/Slider.tsx +++ b/packages/ui/app/_components/dialogs/manage/Slider.tsx @@ -1,15 +1,25 @@ 'use client'; -// SliderComponent.js -import React from 'react'; +import React, { useEffect } from 'react'; + interface IUtilization { currentUtilizationPercentage: number; handleUtilization: (val: number) => void; max?: number; } + const SliderComponent = ({ currentUtilizationPercentage, - handleUtilization + handleUtilization, + max = 1 // Default to 1 to avoid division by zero }: IUtilization) => { + // Reset slider when max changes (including when switching tabs) + useEffect(() => { + // Reset to 0 if max is 0, otherwise maintain current value + if (max === 0) { + handleUtilization(0); + } + }, [max, handleUtilization]); + const handleSliderChange = (e: React.ChangeEvent) => { handleUtilization(+e.target.value); }; @@ -18,45 +28,49 @@ const SliderComponent = ({ if (currentUtilizationPercentage <= 50) { return 'bg-accent'; } - return 'bg-lime'; }; - const gettextColor = () => { + + const getTextColor = () => { if (currentUtilizationPercentage <= 50) { return 'text-accent'; } - return 'text-lime'; }; + const isDisabled = max === 0; + return ( -
+
- + {currentUtilizationPercentage}% 80% 100%
-
+
-
+
@@ -64,5 +78,3 @@ const SliderComponent = ({ }; export default SliderComponent; - -// #666666ff diff --git a/packages/ui/app/_components/dialogs/manage/StatusAlerts.tsx b/packages/ui/app/_components/dialogs/manage/StatusAlerts.tsx new file mode 100644 index 000000000..64e188978 --- /dev/null +++ b/packages/ui/app/_components/dialogs/manage/StatusAlerts.tsx @@ -0,0 +1,75 @@ +import { + AlertCircle, + AlertTriangle, + CheckCircle, + HelpCircle +} from 'lucide-react'; + +import { Alert, AlertDescription } from '@ui/components/ui/alert'; +import { HFPStatus } from '@ui/context/ManageDialogContext'; + +import type { LucideProps } from 'lucide-react'; + +interface StatusAlert { + status: HFPStatus; + message: string; + icon: React.ComponentType; // Update the type here +} + +const alerts: Record = { + [HFPStatus.WARNING]: { + status: HFPStatus.WARNING, + message: + 'You are close to the liquidation threshold. Manage your health factor carefully.', + icon: AlertTriangle + }, + [HFPStatus.CRITICAL]: { + status: HFPStatus.CRITICAL, + message: 'Health factor too low.', + icon: AlertCircle + }, + [HFPStatus.UNKNOWN]: { + status: HFPStatus.UNKNOWN, + message: 'Unable to calculate health factor.', + icon: HelpCircle + }, + [HFPStatus.NORMAL]: { + status: HFPStatus.NORMAL, + message: 'Your health factor is normal.', + icon: CheckCircle + } +}; + +interface StatusAlertsProps { + status: HFPStatus | undefined; + availableStates: HFPStatus[]; +} + +const StatusAlerts = ({ status, availableStates }: StatusAlertsProps) => { + if (!status || !availableStates.includes(status)) return null; + + const alert = alerts[status]; + if (!alert) return null; + + const Icon = alert.icon; + + return ( + +
+ + + {alert.message} + +
+
+ ); +}; + +export default StatusAlerts; diff --git a/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx b/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx new file mode 100644 index 000000000..bc739c806 --- /dev/null +++ b/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx @@ -0,0 +1,206 @@ +import { useEffect, useMemo } from 'react'; + +import { formatUnits } from 'viem'; + +import { Button } from '@ui/components/ui/button'; +import { Switch } from '@ui/components/ui/switch'; +import { + Tooltip, + TooltipContent, + TooltipTrigger +} from '@ui/components/ui/tooltip'; +import { + TransactionType, + useManageDialogContext +} from '@ui/context/ManageDialogContext'; +import { useCollateralToggle } from '@ui/hooks/market/useCollateralToggle'; +import { useSupply } from '@ui/hooks/market/useSupply'; + +import Amount from './Amount'; +import TransactionStepsHandler from './TransactionStepsHandler'; +import ResultHandler from '../../ResultHandler'; +import MemoizedUtilizationStats from '../../UtilizationStats'; + +interface SupplyTabProps { + maxAmount: bigint; + isLoadingMax: boolean; + totalStats?: { + capAmount: number; + totalAmount: number; + capFiat: number; + totalFiat: number; + }; + setSwapWidgetOpen: (open: boolean) => void; +} + +const SupplyTab = ({ + maxAmount, + isLoadingMax, + totalStats, + setSwapWidgetOpen +}: SupplyTabProps) => { + const { + selectedMarketData, + resetTransactionSteps, + chainId, + comptrollerAddress, + updatedValues, + isLoadingUpdatedAssets, + refetchUsedQueries, + setPredictionAmount, + getStepsForTypes + } = useManageDialogContext(); + + const { enableCollateral, handleCollateralToggle } = useCollateralToggle({ + selectedMarketData, + comptrollerAddress, + onSuccess: refetchUsedQueries + }); + + const { + isWaitingForIndexing, + supplyAmount, + isPolling, + amount, + setAmount, + utilizationPercentage, + handleUtilization, + amountAsBInt + } = useSupply({ + maxAmount, + enableCollateral, + selectedMarketData, + comptrollerAddress, + chainId + }); + + useEffect(() => { + setPredictionAmount(amountAsBInt); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [amountAsBInt]); + + const combinedTransactionSteps = useMemo(() => { + return getStepsForTypes(TransactionType.SUPPLY, TransactionType.COLLATERAL); + }, [getStepsForTypes]); + + const isDisabled = !amount || amountAsBInt === 0n; + const hasActiveTransactions = combinedTransactionSteps.length > 0; + + return ( +
+
+ +
+ + setAmount(val ?? '')} + isLoading={isLoadingMax || isPolling} + max={formatUnits(maxAmount, selectedMarketData.underlyingDecimals)} + symbol={selectedMarketData.underlyingSymbol} + currentUtilizationPercentage={utilizationPercentage} + handleUtilization={handleUtilization} + /> + +
+
+
+
+ Market Supply Balance +
+ {updatedValues.supplyBalanceFrom} + + + {updatedValues.supplyBalanceTo} + +
+
+ +
+ Market Supply APR +
+ {updatedValues.supplyAPY?.toFixed(2)}% + + + {updatedValues.updatedSupplyAPY?.toFixed(2)}% + +
+
+ +
+ Enable Collateral + + +
+ +
+
+ {(hasActiveTransactions || + !selectedMarketData.supplyBalance) && ( + + {hasActiveTransactions + ? 'Cannot modify collateral during an active transaction' + : 'You need to supply assets first before enabling as collateral'} + + )} +
+
+
+
+ + {totalStats && ( + + )} +
+ + {hasActiveTransactions ? ( +
+ +
+ ) : ( + + )} +
+ ); +}; + +export default SupplyTab; diff --git a/packages/ui/app/_components/popup/Swap.tsx b/packages/ui/app/_components/dialogs/manage/Swap.tsx similarity index 99% rename from packages/ui/app/_components/popup/Swap.tsx rename to packages/ui/app/_components/dialogs/manage/Swap.tsx index 58918d4f6..419815216 100644 --- a/packages/ui/app/_components/popup/Swap.tsx +++ b/packages/ui/app/_components/dialogs/manage/Swap.tsx @@ -22,8 +22,8 @@ import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; import TransactionStepsHandler, { useTransactionSteps } from './TransactionStepsHandler'; -import ConnectButton from '../ConnectButton'; -import ResultHandler from '../ResultHandler'; +import ConnectButton from '../../ConnectButton'; +import ResultHandler from '../../ResultHandler'; import type { GetBalanceData } from 'wagmi/query'; diff --git a/packages/ui/app/_components/popup/TransactionStepsHandler.tsx b/packages/ui/app/_components/dialogs/manage/TransactionStepsHandler.tsx similarity index 84% rename from packages/ui/app/_components/popup/TransactionStepsHandler.tsx rename to packages/ui/app/_components/dialogs/manage/TransactionStepsHandler.tsx index c2de24ef1..547b092c4 100644 --- a/packages/ui/app/_components/popup/TransactionStepsHandler.tsx +++ b/packages/ui/app/_components/dialogs/manage/TransactionStepsHandler.tsx @@ -5,6 +5,7 @@ import { useReducer } from 'react'; import { ThreeCircles } from 'react-loader-spinner'; +import { Button } from '@ui/components/ui/button'; import { getScanUrlByChainId } from '@ui/utils/networkData'; export type TransactionStep = { @@ -84,10 +85,18 @@ function TransactionStepsHandler({ resetTransactionSteps, chainId }: TransactionStepsHandlerProps) { + const isComplete = + transactionSteps.filter((step) => step.success).length === + transactionSteps.length || + transactionSteps.find((step) => step.error) !== undefined; + return ( -
+
{transactionSteps.map((transactionStep, i) => ( -
+
))} - {(transactionSteps.filter((step) => step.success).length === - transactionSteps.length || - transactionSteps.find((step) => step.error) !== undefined) && ( + {isComplete && (
- + Continue +
)}
diff --git a/packages/ui/app/_components/dialogs/manage/WithdrawTab.tsx b/packages/ui/app/_components/dialogs/manage/WithdrawTab.tsx new file mode 100644 index 000000000..8ae10643a --- /dev/null +++ b/packages/ui/app/_components/dialogs/manage/WithdrawTab.tsx @@ -0,0 +1,194 @@ +import { useEffect, useMemo } from 'react'; + +import { formatUnits } from 'viem'; + +import { Button } from '@ui/components/ui/button'; +import { + HFPStatus, + TransactionType, + useManageDialogContext +} from '@ui/context/ManageDialogContext'; +import { useHealth } from '@ui/hooks/market/useHealth'; +import { useWithdraw } from '@ui/hooks/market/useWithdraw'; + +import Amount from './Amount'; +import StatusAlerts from './StatusAlerts'; +import TransactionStepsHandler from './TransactionStepsHandler'; +import ResultHandler from '../../ResultHandler'; +import MemoizedUtilizationStats from '../../UtilizationStats'; + +interface WithdrawTabProps { + maxAmount: bigint; + isLoadingMax: boolean; + totalStats?: { + capAmount: number; + totalAmount: number; + capFiat: number; + totalFiat: number; + }; +} + +const WithdrawTab = ({ + maxAmount, + isLoadingMax, + totalStats +}: WithdrawTabProps) => { + const { + selectedMarketData, + resetTransactionSteps, + chainId, + updatedValues, + isLoadingUpdatedAssets, + comptrollerAddress, + setPredictionAmount, + getStepsForTypes // Add this from context + } = useManageDialogContext(); + + const { + isWaitingForIndexing, + withdrawAmount, + isPolling, + amount, + setAmount, + utilizationPercentage, + handleUtilization, + amountAsBInt + } = useWithdraw({ + maxAmount, + selectedMarketData, + chainId + }); + + const { isLoadingPredictedHealthFactor, healthFactor, hfpStatus } = useHealth( + { + comptrollerAddress, + cToken: selectedMarketData.cToken, + activeTab: 'withdraw', + amount: amountAsBInt, + exchangeRate: selectedMarketData.exchangeRate, + decimals: selectedMarketData.underlyingDecimals + } + ); + + const isDisabled = + !amount || + amountAsBInt === 0n || + isLoadingPredictedHealthFactor || + hfpStatus === HFPStatus.CRITICAL || + hfpStatus === HFPStatus.UNKNOWN; + + useEffect(() => { + setPredictionAmount(amountAsBInt); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [amountAsBInt]); + + const transactionSteps = useMemo(() => { + return getStepsForTypes(TransactionType.WITHDRAW); + }, [getStepsForTypes]); + + return ( +
+ setAmount(val ?? '')} + isLoading={isLoadingMax || isPolling} + max={formatUnits(maxAmount, selectedMarketData.underlyingDecimals)} + symbol={selectedMarketData.underlyingSymbol} + hintText="Max Withdraw" + currentUtilizationPercentage={utilizationPercentage} + handleUtilization={handleUtilization} + /> + + + +
+
+
+ Market Supply Balance +
+ {updatedValues.supplyBalanceFrom} + + + {updatedValues.supplyBalanceTo} + +
+
+ +
+ Market Supply APR +
+ {updatedValues.supplyAPY?.toFixed(2)}% + + + {updatedValues.updatedSupplyAPY?.toFixed(2)}% + +
+
+ +
+ Health Factor +
+ {healthFactor.current} + + + {healthFactor.predicted} + +
+
+
+ + {totalStats && ( + + )} +
+ + {transactionSteps.length > 0 ? ( +
+ +
+ ) : ( + + )} +
+ ); +}; + +export default WithdrawTab; diff --git a/packages/ui/app/_components/dialogs/manage/index.tsx b/packages/ui/app/_components/dialogs/manage/index.tsx new file mode 100644 index 000000000..921595429 --- /dev/null +++ b/packages/ui/app/_components/dialogs/manage/index.tsx @@ -0,0 +1,89 @@ +'use client'; +import { useState } from 'react'; + +import dynamic from 'next/dynamic'; + +import { type Address } from 'viem'; +import { useChainId } from 'wagmi'; + +import { ManageDialogProvider } from '@ui/context/ManageDialogContext'; +import { useMaxSupplyAmount } from '@ui/hooks/useMaxSupplyAmount'; +import type { MarketData } from '@ui/types/TokensDataMap'; + +import DialogWrapper from './DialogWrapper'; +import ManageDialogTabs from './ManageDialogTabs'; + +const SwapWidget = dynamic(() => import('../../markets/SwapWidget'), { + ssr: false +}); + +export type ActiveTab = 'borrow' | 'repay' | 'supply' | 'withdraw'; + +export enum HFPStatus { + CRITICAL = 'CRITICAL', + NORMAL = 'NORMAL', + UNKNOWN = 'UNKNOWN', + WARNING = 'WARNING' +} + +interface ManageDialogProps { + isOpen: boolean; + setIsOpen: (open: boolean) => void; + comptrollerAddress: Address; + selectedMarketData: MarketData; + isBorrowDisabled?: boolean; + activeTab?: ActiveTab; +} + +const ManageDialog = ({ + isOpen, + setIsOpen, + selectedMarketData, + comptrollerAddress, + isBorrowDisabled = false, + activeTab = 'supply' +}: ManageDialogProps) => { + const [swapWidgetOpen, setSwapWidgetOpen] = useState(false); + const chainId = useChainId(); + const [currentActiveTab, setCurrentActiveTab] = + useState(activeTab); + const { refetch: refetchMaxSupplyAmount } = useMaxSupplyAmount( + selectedMarketData, + comptrollerAddress, + chainId + ); + + return ( + + + + + + setSwapWidgetOpen(false)} + open={swapWidgetOpen} + fromChain={chainId} + toChain={chainId} + toToken={selectedMarketData.underlyingToken} + onRouteExecutionCompleted={() => refetchMaxSupplyAmount()} + /> + + ); +}; + +export default ManageDialog; diff --git a/packages/ui/app/_components/markets/APRCell.tsx b/packages/ui/app/_components/markets/APRCell.tsx new file mode 100644 index 000000000..445706454 --- /dev/null +++ b/packages/ui/app/_components/markets/APRCell.tsx @@ -0,0 +1,306 @@ +import dynamic from 'next/dynamic'; +import Image from 'next/image'; +import Link from 'next/link'; + +import { + HoverCard, + HoverCardTrigger, + HoverCardContent +} from '@ui/components/ui/hover-card'; +import { pools } from '@ui/constants'; +import { useMerklApr } from '@ui/hooks/useMerklApr'; +import { useRewardsBadge } from '@ui/hooks/useRewardsBadge'; +import { cn } from '@ui/lib/utils'; +import { multipliers } from '@ui/utils/multipliers'; + +import { RewardIcons } from './RewardsIcon'; + +import type { Address } from 'viem'; + +import type { FlywheelReward } from '@ionicprotocol/types'; + +const FlyWheelRewards = dynamic(() => import('./FlyWheelRewards'), { + ssr: false +}); + +export type APRCellProps = { + type: 'borrow' | 'supply'; + aprTotal: number | undefined; + baseAPR: number; + asset: string; + cToken: Address; + dropdownSelectedChain: number; + pool: Address; + selectedPoolId: string; + rewards?: FlywheelReward[]; +}; + +export default function APRCell({ + type, + aprTotal, + baseAPR, + asset, + cToken, + dropdownSelectedChain, + pool, + selectedPoolId, + rewards +}: APRCellProps) { + const isMainModeMarket = + dropdownSelectedChain === 34443 && + (asset === 'USDC' || asset === 'WETH') && + selectedPoolId === '0'; + + const { data: merklApr } = useMerklApr(); + const merklAprForToken = merklApr?.find( + (a) => Object.keys(a)[0].toLowerCase() === cToken.toLowerCase() + )?.[cToken]; + + const config = + multipliers[dropdownSelectedChain]?.[selectedPoolId]?.[asset]?.[type]; + const showRewardsBadge = useRewardsBadge( + dropdownSelectedChain, + selectedPoolId, + asset, + type, + rewards + ); + + const showOPRewards = merklAprForToken || asset === 'dMBTC'; + + const formatBaseAPR = () => { + if (type === 'borrow' && baseAPR > 0) + return ( + '-' + baseAPR.toLocaleString('en-US', { maximumFractionDigits: 2 }) + ); + return ( + (type === 'supply' ? '+' : '') + + baseAPR.toLocaleString('en-US', { maximumFractionDigits: 2 }) + ); + }; + + const formatTotalAPR = () => { + const numericValue = aprTotal ?? 0; + const prefix = type === 'supply' || numericValue > 0 ? '+' : ''; + return ( + prefix + + numericValue.toLocaleString('en-US', { + maximumFractionDigits: type === 'supply' ? 2 : 1 + }) + ); + }; + + const RewardRow = ({ icon, text }: { icon: string; text: string }) => ( +
+ + {text} +
+ ); + + const getRewardIcons = () => { + const icons: string[] = []; + + if (showOPRewards) icons.push('op'); + if (config?.ionic) icons.push('ionic'); + if (config?.turtle) icons.push('turtle'); + if (config?.etherfi) icons.push('etherfi'); + if (config?.kelp) icons.push('kelp'); + if (config?.eigenlayer) icons.push('eigen'); + if (config?.spice) icons.push('spice'); + if (type === 'supply') { + if (config?.anzen) icons.push('anzen'); + if (config?.nektar) icons.push('nektar'); + } + + return icons; + }; + + return ( + + +
+ {formatTotalAPR()}% +
+ + + ION APR + + + {showRewardsBadge && ( +
+ + Rewards + +
+ )} + + {config?.turtle && !isMainModeMarket && ( + + + + TURTLE{' '} + external-link + + + )} +
+
+
+ +
+
+ Base APR: {formatBaseAPR()}% +
+ + {showOPRewards && ( + + OP + + + OP Rewards:{' '} + {merklAprForToken?.toLocaleString('en-US', { + maximumFractionDigits: 2 + })} + % + + + )} + + {config?.underlyingAPR && ( +

+ Native Asset Yield: + + {config.underlyingAPR.toLocaleString('en-US', { + maximumFractionDigits: 2 + })} + % +

+ )} + + {config?.flywheel && ( +
+ +
+ )} + + {(config?.ionic ?? 0) > 0 && ( + <> + + + + )} + + {config?.turtle && asset === 'STONE' && ( + + )} + + {config?.etherfi && ( + + )} + + {config?.kelp && ( + <> + + + + )} + + {config?.eigenlayer && ( + + )} + + {config?.spice && ( + + )} + + {type === 'supply' && ( + <> + {(config?.anzen ?? 0) > 0 && ( + + )} + + {config?.nektar && ( + + )} + + )} +
+
+
+ ); +} diff --git a/packages/ui/app/_components/markets/BorrowPopover.tsx b/packages/ui/app/_components/markets/BorrowPopover.tsx deleted file mode 100644 index 89697147c..000000000 --- a/packages/ui/app/_components/markets/BorrowPopover.tsx +++ /dev/null @@ -1,215 +0,0 @@ -/* eslint-disable @next/next/no-img-element */ -import dynamic from 'next/dynamic'; - -import { pools } from '@ui/constants/index'; -import { useRewardsBadge } from '@ui/hooks/useRewardsBadge'; -import { multipliers } from '@ui/utils/multipliers'; - -import type { Address } from 'viem'; - -import type { FlywheelReward } from '@ionicprotocol/types'; - -const Rewards = dynamic(() => import('./FlyWheelRewards'), { - ssr: false -}); - -export type BorrowPopoverProps = { - dropdownSelectedChain: number; - borrowAPR?: number; - rewardsAPR?: number; - selectedPoolId: string; - asset: string; - cToken: Address; - pool: Address; - rewards?: FlywheelReward[]; -}; -export default function BorrowPopover({ - dropdownSelectedChain, - borrowAPR, - rewards, - selectedPoolId, - asset, - cToken, - pool -}: BorrowPopoverProps) { - const isModeMarket = - dropdownSelectedChain === 34443 && (asset === 'USDC' || asset === 'WETH'); - - const borrowConfig = - multipliers[+dropdownSelectedChain]?.[selectedPoolId]?.[asset]?.borrow; - - const showRewardsBadge = useRewardsBadge( - dropdownSelectedChain, - selectedPoolId, - asset, - 'borrow', - rewards - ); - - return ( - <> - - + ION APR i - - - {showRewardsBadge && !isModeMarket && ( - - + REWARDS i - - )} - - {borrowConfig?.turtle && !isModeMarket && ( - - - + TURTLE{' '} - external-link - - - )} -
-
- Base APR:{' '} - {typeof borrowAPR !== 'undefined' ? (borrowAPR > 0 ? '-' : '') : ''} - {borrowAPR - ? borrowAPR.toLocaleString('en-US', { maximumFractionDigits: 2 }) - : '-'} - % -
- {borrowConfig && ( - <> - {borrowConfig?.flywheel && ( - - )} - {borrowConfig?.ionic > 0 && ( - <> -
- {' '} - +{' '} - { - multipliers[dropdownSelectedChain]?.[selectedPoolId]?.[ - asset - ]?.borrow?.ionic - } - x Ionic Points -
-
- {' '} - + Turtle Ionic Points -
- - )} - {borrowConfig?.turtle && asset === 'STONE' && ( - <> -
- {' '} - + Stone Turtle Points -
- - )} - {borrowConfig?.etherfi && ( - <> -
- {' '} - +{' '} - { - multipliers[dropdownSelectedChain]?.[selectedPoolId]?.[ - asset - ]?.borrow?.etherfi - } - x ether.fi Points -
- - )} - {borrowConfig?.kelp && ( - <> -
- {' '} - +{' '} - { - multipliers[dropdownSelectedChain]?.[selectedPoolId]?.[ - asset - ]?.borrow?.kelp - } - x Kelp Miles -
-
- {' '} - + Turtle Kelp Points -
- - )} - {borrowConfig?.eigenlayer && ( -
- {' '} - + EigenLayer Points -
- )} - {borrowConfig?.spice && ( -
- {' '} - + Spice Points -
- )} - - )} -
- - ); -} diff --git a/packages/ui/app/_components/markets/FeaturedMarketTile.tsx b/packages/ui/app/_components/markets/FeaturedMarketTile.tsx index fc235a14a..c0e5e2ac0 100644 --- a/packages/ui/app/_components/markets/FeaturedMarketTile.tsx +++ b/packages/ui/app/_components/markets/FeaturedMarketTile.tsx @@ -6,17 +6,11 @@ import { useStore } from '@ui/store/Store'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; import WrapEthSwaps from './WrapEthSwaps'; -import { PopupMode } from '../popup/page'; import ResultHandler from '../ResultHandler'; -// import BorrowPopover from './BorrowPopover'; -// import SupplyPopover from './SupplyPopover'; - -// import { pools } from '@ui/constants/index'; - interface Iprop { selectedChain: number; - setPopupMode: Dispatch>; + setIsManageDialogOpen: Dispatch>; setSelectedSymbol: Dispatch>; isLoadingPoolData: boolean; dropdownSelectedChain: string; @@ -28,7 +22,7 @@ interface Iprop { export default function FeaturedMarketTile({ selectedChain, - setPopupMode, + setIsManageDialogOpen, setSelectedSymbol, isLoadingPoolData = true, dropdownSelectedChain, @@ -37,17 +31,6 @@ export default function FeaturedMarketTile({ setWrapWidgetOpen, wrapWidgetOpen }: Iprop) { - // const { - // asset, - // borrowAPR, - // rewardsAPR, - // dropdownSelectedChain, - // selectedPoolId, - // cToken, - // pool, - // rewards, - // loopPossible - // } = useStore((state) => state.featuredBorrow); const featuredSupply = useStore((state) => state.featuredSupply); const featuredSupply2 = useStore((state) => state.featuredSupply2); @@ -93,15 +76,6 @@ export default function FeaturedMarketTile({ }) ?? '-'} % - {/* */}
- {/* */}
)} diff --git a/packages/ui/app/_components/markets/FilterBar.tsx b/packages/ui/app/_components/markets/FilterBar.tsx new file mode 100644 index 000000000..47c21e505 --- /dev/null +++ b/packages/ui/app/_components/markets/FilterBar.tsx @@ -0,0 +1,44 @@ +'use client'; + +import dynamic from 'next/dynamic'; + +import { type MarketRowData } from '@ui/hooks/market/useMarketData'; + +import MarketSearch from './MarketSearch'; + +const PoolToggle = dynamic( + () => import('../../_components/markets/PoolToggle'), + { + ssr: false + } +); +interface FilterBarProps { + chain: number; + pool: string; + marketData: MarketRowData[]; + onSearch: (filteredData: MarketRowData[]) => void; +} + +export default function FilterBar({ + chain, + pool, + marketData, + onSearch +}: FilterBarProps) { + return ( +
+
+ +
+
+ +
+
+ ); +} diff --git a/packages/ui/app/_components/markets/FlyWheelRewards.tsx b/packages/ui/app/_components/markets/FlyWheelRewards.tsx index d8ef6e565..fc2aa0353 100644 --- a/packages/ui/app/_components/markets/FlyWheelRewards.tsx +++ b/packages/ui/app/_components/markets/FlyWheelRewards.tsx @@ -7,14 +7,17 @@ import dynamic from 'next/dynamic'; import { formatEther, type Address } from 'viem'; import { useChainId } from 'wagmi'; +import { Button } from '@ui/components/ui/button'; +import { Card } from '@ui/components/ui/card'; import { REWARDS_TO_SYMBOL } from '@ui/constants/index'; import { useSdk } from '@ui/hooks/ionic/useSdk'; import { useFlywheelRewards } from '@ui/hooks/useFlyWheelRewards'; +import { cn } from '@ui/lib/utils'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; import ResultHandler from '../ResultHandler'; -import { type FlywheelReward } from '@ionicprotocol/types'; +import type { FlywheelReward } from '@ionicprotocol/types'; type FlyWheelRewardsProps = { cToken: Address; @@ -22,21 +25,46 @@ type FlyWheelRewardsProps = { poolChainId: number; type: 'borrow' | 'supply'; rewards?: FlywheelReward[]; - className?: string; + maxButtonWidth?: string; + isStandalone?: boolean; }; +const RewardRow = ({ + symbol, + value, + isStandalone +}: { + symbol: string; + value: string; + isStandalone?: boolean; +}) => ( +
+ + {value} +
+); + const FlyWheelRewards = ({ cToken, pool, poolChainId, type, - rewards, - className + rewards = [], + maxButtonWidth = 'max-w-[160px]', + isStandalone = false }: FlyWheelRewardsProps) => { - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); const chainId = useChainId(); const sdk = useSdk(poolChainId); - const { filteredRewards, totalRewards, combinedRewards } = useFlywheelRewards( poolChainId, cToken, @@ -44,76 +72,93 @@ const FlyWheelRewards = ({ type ); - const claimRewards = async () => { + const handleClaim = async () => { try { - const result = await handleSwitchOriginChain(poolChainId, chainId); - if (result) { - setIsLoading(true); - const tx = await sdk?.claimRewardsForMarket( - cToken, - filteredRewards?.map((r) => r.flywheel!) ?? [] - ); - setIsLoading(false); - console.warn('claim tx: ', tx); - } + const canSwitch = await handleSwitchOriginChain(poolChainId, chainId); + if (!canSwitch || !sdk) return; + + setIsLoading(true); + await sdk.claimRewardsForMarket( + cToken, + filteredRewards?.map((r) => r.flywheel!) ?? [] + ); } catch (err) { - setIsLoading(false); console.warn(err); } finally { setIsLoading(false); } }; + const rewardsSymbols = REWARDS_TO_SYMBOL[poolChainId] ?? {}; + return ( - <> - {rewards?.map((rewards, index) => ( -
- {REWARDS_TO_SYMBOL[poolChainId]?.[rewards?.token]} Rewards APR: + - {rewards.apy - ? rewards.apy.toLocaleString('en-US', { maximumFractionDigits: 2 }) - : '-'} - % -
+
+ {rewards.map((reward, index) => ( + ))} + {(totalRewards > 0 || combinedRewards.length > 0) && ( -
- {combinedRewards.map((rewards, index) => ( -
+ {combinedRewards.map((reward, index) => ( + - - +{' '} - {Number(formatEther(rewards.amount)).toLocaleString('en-US', { - maximumFractionDigits: 1 - })}{' '} - {REWARDS_TO_SYMBOL[poolChainId][rewards.rewardToken]} -
+ symbol={rewardsSymbols[reward.rewardToken]} + value={`+ ${Number(formatEther(reward.amount)).toLocaleString( + 'en-US', + { + maximumFractionDigits: 1 + } + )} ${rewardsSymbols[reward.rewardToken]}`} + isStandalone={isStandalone} + /> ))} + {totalRewards > 0n && ( -
- +
)} -
+ )} - +
); }; diff --git a/packages/ui/app/_components/markets/MarketInfo.tsx b/packages/ui/app/_components/markets/MarketInfo.tsx new file mode 100644 index 000000000..f491b6c00 --- /dev/null +++ b/packages/ui/app/_components/markets/MarketInfo.tsx @@ -0,0 +1,179 @@ +import React, { useState } from 'react'; + +import Image from 'next/image'; +import Link from 'next/link'; + +import { ArrowRight } from 'lucide-react'; +import { base, mode, optimism } from 'viem/chains'; + +import { Button } from '@ui/components/ui/button'; +import { + Card, + CardHeader, + CardTitle, + CardContent +} from '@ui/components/ui/card'; +import { Separator } from '@ui/components/ui/separator'; +import useSugarAPR from '@ui/hooks/useSugarAPR'; +import type { PoolData } from '@ui/types/TokensDataMap'; + +import WrapWidget from './WrapWidget'; +import { CHAIN_CONFIGS } from '../stake/RewardDisplay'; + +interface MarketInfoProps { + chain: number; + poolData?: PoolData | null; + isLoadingPoolData: boolean; + isLoadingLoopMarkets: boolean; +} + +const MarketInfo = ({ + chain, + poolData, + isLoadingPoolData, + isLoadingLoopMarkets +}: MarketInfoProps) => { + const chainConfig = CHAIN_CONFIGS[chain]?.defaultConfig; + const { apr } = useSugarAPR({ + sugarAddress: chainConfig?.sugarAddress ?? '0x', + poolIndex: chainConfig?.poolIndex ?? 0n, + chainId: chain, + selectedToken: 'eth', + isMode: chain === mode.id + }); + + const [wrapWidgetOpen, setWrapWidgetOpen] = useState(false); + const formatCurrency = (value?: number) => { + if (value === undefined) { + return '$0.00'; + } + if (value >= 1e9) { + return `$${(value / 1e9).toFixed(2)}B`; + } else if (value >= 1e6) { + return `$${(value / 1e6).toFixed(2)}M`; + } else { + return `$${value.toLocaleString('en-US', { + maximumFractionDigits: 2, + minimumFractionDigits: 2 + })}`; + } + }; + + const isLoading = isLoadingPoolData || isLoadingLoopMarkets; + + return ( + <> + + + +
+ M +
+ Mode Market +
+
+ Staking + ion logo +
+
+ + +
+ {/* Left group */} +
+
+

TOTAL MARKET SIZE

+

+ {isLoading + ? 'Loading...' + : formatCurrency( + poolData + ? poolData.totalSuppliedFiat + + poolData.totalBorrowedFiat + : 0 + )} +

+
+
+

TOTAL AVAILABLE

+

+ {isLoading + ? 'Loading...' + : formatCurrency(poolData?.totalSuppliedFiat)} +

+
+
+

TOTAL BORROWS

+

+ {isLoading + ? 'Loading...' + : formatCurrency(poolData?.totalBorrowedFiat)} +

+
+
+ + {/* Right group */} +
+
+

APR

+

{apr}

+
+
+

IONIC DISTRIBUTED

+

$2,452,751.00

+
+
+
+ + + +
+ + + + Stake + +
+
+
+ + setWrapWidgetOpen(false)} + open={wrapWidgetOpen} + chain={+chain} + /> + + ); +}; + +export default MarketInfo; diff --git a/packages/ui/app/_components/markets/MarketSearch.tsx b/packages/ui/app/_components/markets/MarketSearch.tsx new file mode 100644 index 000000000..1559ed386 --- /dev/null +++ b/packages/ui/app/_components/markets/MarketSearch.tsx @@ -0,0 +1,81 @@ +import React, { useCallback, useEffect, useState } from 'react'; + +import { Search } from 'lucide-react'; +import { isAddress } from 'viem'; + +import { Input } from '@ui/components/ui/input'; +import type { MarketRowData } from '@ui/hooks/market/useMarketData'; + +const useDebounce = (value: T, delay: number = 300): T => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +}; + +interface MarketSearchProps { + data: MarketRowData[]; + onSearch: (filtered: MarketRowData[]) => void; +} + +const MarketSearch = ({ data, onSearch }: MarketSearchProps) => { + const [searchTerm, setSearchTerm] = useState(''); + const debouncedSearchTerm = useDebounce(searchTerm); + + const filterMarkets = useCallback( + (term: string) => { + if (!term.trim()) { + onSearch(data); + return; + } + + const lowercaseSearch = term.toLowerCase(); + const isAddressSearch = isAddress(term); + + const filtered = data.filter((market) => { + if (isAddressSearch) { + return ( + market.cTokenAddress.toLowerCase() === lowercaseSearch || + market.underlyingToken.toLowerCase() === lowercaseSearch + ); + } + + return ( + market.asset.toLowerCase().includes(lowercaseSearch) || + market.underlyingSymbol.toLowerCase().includes(lowercaseSearch) + ); + }); + + onSearch(filtered); + }, + [data, onSearch] + ); + + useEffect(() => { + filterMarkets(debouncedSearchTerm); + }, [debouncedSearchTerm, filterMarkets]); + + return ( +
+
+ +
+ setSearchTerm(e.target.value)} + className="h-9 pl-10 pr-4 rounded-lg text-sm border-white/5 hover:border-white/10 focus-visible:ring-1 focus-visible:ring-accent/50 focus-visible:border-accent transition-colors placeholder:text-white/30" + /> +
+ ); +}; + +export default MarketSearch; diff --git a/packages/ui/app/_components/markets/NetworkSelector.tsx b/packages/ui/app/_components/markets/NetworkSelector.tsx index 56a0592ff..5c6da0c54 100644 --- a/packages/ui/app/_components/markets/NetworkSelector.tsx +++ b/packages/ui/app/_components/markets/NetworkSelector.tsx @@ -20,6 +20,7 @@ interface INetworkSelector { enabledChains?: number[]; upcomingChains?: string[]; } + const NETWORK_ORDER = ['Mode', 'Base', 'Optimism', 'Fraxtal', 'Lisk', 'BoB']; function NetworkSelector({ @@ -33,7 +34,6 @@ function NetworkSelector({ const setDropChain = useStore((state) => state.setDropChain); const orderedNetworks = NETWORK_ORDER.map((networkName) => - // eslint-disable-next-line @typescript-eslint/no-unused-vars Object.entries(pools).find(([_, pool]) => pool.name === networkName) ).filter( (entry): entry is [string, any] => @@ -43,13 +43,11 @@ function NetworkSelector({ const getUrlWithParams = (chainId: string) => { const params = new URLSearchParams(searchParams.toString()); + // Always reset pool to 0 when changing chains unless nopool is true params.set('chain', chainId); - if (!nopool && !params.has('pool')) { + if (!nopool) { params.set('pool', '0'); } - if (nopool && params.has('pool')) { - params.delete('pool'); - } return `${pathname}?${params.toString()}`; }; diff --git a/packages/ui/app/_components/markets/PoolRows.tsx b/packages/ui/app/_components/markets/PoolRows.tsx deleted file mode 100644 index 4ae486771..000000000 --- a/packages/ui/app/_components/markets/PoolRows.tsx +++ /dev/null @@ -1,407 +0,0 @@ -/* eslint-disable @next/next/no-img-element */ -'use client'; -import { useEffect, useMemo, type Dispatch, type SetStateAction } from 'react'; - -import Link from 'next/link'; - -import { - FLYWHEEL_TYPE_MAP, - pools, - shouldGetFeatured -} from '@ui/constants/index'; -import { useMultiIonic } from '@ui/context/MultiIonicContext'; -import { useBorrowCapsDataForAsset } from '@ui/hooks/ionic/useBorrowCapsDataForAsset'; -import type { LoopMarketData } from '@ui/hooks/useLoopMarkets'; -import { useMerklApr } from '@ui/hooks/useMerklApr'; -import { useStore } from '@ui/store/Store'; -import type { MarketData } from '@ui/types/TokensDataMap'; -import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; - -import BorrowPopover from './BorrowPopover'; -import SupplyPopover from './SupplyPopover'; -import { PopupMode } from '../popup/page'; - -import type { Address } from 'viem'; - -import type { FlywheelReward } from '@ionicprotocol/types'; - -interface IRows { - asset: string; - borrowAPR?: number; - borrowBalance: string; - chain: string; - collateralFactor: number; - comptrollerAddress: Address; - cTokenAddress: Address; - dropdownSelectedChain: number; - logo: string; - loopMarkets?: LoopMarketData | undefined; - loopPossible: boolean; - membership: boolean; - pool: string; - selectedChain: number; - rewards?: FlywheelReward[]; - selectedMarketData: MarketData | undefined; - selectedPoolId: string; - selectedSymbol: string; - setPopupMode: Dispatch>; - setSelectedSymbol: Dispatch>; - supplyAPR?: number; - supplyBalance: string; - totalBorrowing: string; - totalSupplied: string; -} - -const PoolRows = ({ - asset, - supplyBalance, - totalSupplied, - borrowBalance, - chain, - collateralFactor, - cTokenAddress, - dropdownSelectedChain, - membership, - totalBorrowing, - supplyAPR, - borrowAPR, - logo, - loopPossible, - pool, - setSelectedSymbol, - setPopupMode, - selectedChain, - selectedPoolId, - comptrollerAddress, - rewards -}: IRows) => { - const { address } = useMultiIonic(); - const { data: merklApr } = useMerklApr(); - - const merklAprForToken = merklApr?.find( - (a) => Object.keys(a)[0].toLowerCase() === cTokenAddress.toLowerCase() - )?.[cTokenAddress]; - - const supplyRewards = useMemo( - () => - rewards?.filter((reward) => - FLYWHEEL_TYPE_MAP[dropdownSelectedChain]?.supply?.includes( - (reward as FlywheelReward).flywheel - ) - ), - [dropdownSelectedChain, rewards] - ); - const totalSupplyRewardsAPR = useMemo( - () => - (supplyRewards?.reduce((acc, reward) => acc + (reward.apy ?? 0), 0) ?? - 0) + (merklAprForToken ?? 0), - [supplyRewards, merklAprForToken] - ); - - const borrowRewards = useMemo( - () => - rewards?.filter((reward) => - FLYWHEEL_TYPE_MAP[dropdownSelectedChain]?.borrow?.includes( - (reward as FlywheelReward).flywheel - ) - ), - [dropdownSelectedChain, rewards] - ); - const totalBorrowRewardsAPR = useMemo( - () => - borrowRewards?.reduce((acc, reward) => acc + (reward.apy ?? 0), 0) ?? 0, - [borrowRewards] - ); - - const { data: borrowCapsData } = useBorrowCapsDataForAsset( - cTokenAddress, - dropdownSelectedChain - ); - - const borrowAPRTotal = - typeof borrowAPR !== 'undefined' - ? 0 - borrowAPR + totalBorrowRewardsAPR - : undefined; - const supplyAPRTotal = - typeof supplyAPR !== 'undefined' - ? supplyAPR + totalSupplyRewardsAPR - : undefined; - - //type the asset name to get it featured - // shouldGetFeatured - // const setFeaturedBorrow = useStore((state) => state.setFeaturedBorrow); - const setFeaturedSupply = useStore((state) => state.setFeaturedSupply); - const setFeaturedSupply2 = useStore((state) => state.setFeaturedSupply2); - - useEffect(() => { - if ( - shouldGetFeatured.featuredSupply2[+dropdownSelectedChain][ - pool - ].toLowerCase() === asset.toLowerCase() - ) { - // setFeaturedBorrow({ - // dropdownSelectedChain, - // borrowAPR, - // rewardsAPR: totalBorrowRewardsAPR, - // selectedPoolId, - // cToken: cTokenAddress, - // pool: comptrollerAddress, - // rewards: borrowRewards, - // asset, - // loopPossible - // }); - setFeaturedSupply2({ - asset: asset, - supplyAPR: supplyAPR, - supplyAPRTotal: supplyAPRTotal, - rewards: supplyRewards, - dropdownSelectedChain: dropdownSelectedChain, - selectedPoolId: selectedPoolId, - cToken: cTokenAddress, - pool: comptrollerAddress - }); - } - if ( - shouldGetFeatured.featuredSupply[+dropdownSelectedChain][ - pool - ].toLowerCase() === asset.toLowerCase() - ) { - setFeaturedSupply({ - asset: asset, - supplyAPR: supplyAPR, - supplyAPRTotal: supplyAPRTotal, - rewards: supplyRewards, - dropdownSelectedChain: dropdownSelectedChain, - selectedPoolId: selectedPoolId, - cToken: cTokenAddress, - pool: comptrollerAddress - }); - } - }, [ - asset, - cTokenAddress, - comptrollerAddress, - dropdownSelectedChain, - pool, - selectedPoolId, - setFeaturedSupply, - setFeaturedSupply2, - supplyAPR, - supplyAPRTotal, - supplyRewards - ]); - // console.log(borrowCapAsNumber, asset); - return ( -
- {membership && ( - - Collateral - - )} - sendPassedData()} - > -
- {asset} -

{asset}

-
-

- - SUPPLY BALANCE: - - {supplyBalance} -

-

- - TOTAL SUPPLIED: - - {totalSupplied} -

-

- - BORROW BALANCE: - - {borrowBalance} -

-

- - TOTAL BORROWING: - - {totalBorrowing} -

- - -

- - SUPPLY APR: - -
- - + - {supplyAPRTotal?.toLocaleString('en-US', { - maximumFractionDigits: 2 - }) ?? '-'} - %{' '} - {/* {multipliers[dropdownSelectedChain]?.[selectedPoolId]?.[asset] - ?.supply?.underlyingAPR && - '(+' + - (multipliers[dropdownSelectedChain]?.[selectedPoolId]?.[ - asset - ]?.supply?.underlyingAPR).toLocaleString('en-US', { - maximumFractionDigits: 2 - }) + - '%)'} */} - - - -
-

-

- - BORROW APR: - -
- - {borrowAPRTotal ? (borrowAPRTotal > 0 ? '+' : '') : ''} - {borrowAPRTotal?.toLocaleString('en-US', { - maximumFractionDigits: 1 - }) ?? '-'} - % - - - -
-

-

- - COLLATERAL FACTOR: - - {Math.round(collateralFactor)}% -

-
-
- {/* */} - - -
- {/* {!address && ( -
- -
- )} */} -
-
- ); -}; - -export default PoolRows; diff --git a/packages/ui/app/_components/markets/PoolToggle.tsx b/packages/ui/app/_components/markets/PoolToggle.tsx index 7421db837..09f98c7c7 100644 --- a/packages/ui/app/_components/markets/PoolToggle.tsx +++ b/packages/ui/app/_components/markets/PoolToggle.tsx @@ -4,31 +4,44 @@ import dynamic from 'next/dynamic'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; +import { Globe, Diamond } from 'lucide-react'; + import { pools } from '@ui/constants/index'; const PoolToggle = ({ chain, pool }: { chain: number; pool: string }) => { const pathname = usePathname(); + const poolsData = pools[+chain].pools; + return ( -
- {pools[+chain].pools.map((poolx, idx) => { - return ( - - {poolx.name} - - ); - })} +
+
+ {poolsData.map((poolx, idx) => { + const isActive = pool === poolx.id; + const isMain = poolx.name.toLowerCase().includes('main'); + + return ( + + {isMain ? ( + + ) : ( + + )} + {isMain ? 'Main' : 'Native'} + + ); + })} +
); }; diff --git a/packages/ui/app/_components/markets/RewardsIcon.tsx b/packages/ui/app/_components/markets/RewardsIcon.tsx new file mode 100644 index 000000000..9fc9096d7 --- /dev/null +++ b/packages/ui/app/_components/markets/RewardsIcon.tsx @@ -0,0 +1,45 @@ +import Image from 'next/image'; + +type RewardIconsProps = { + rewards: string[]; // Array of reward types like 'op', 'ionic', 'turtle', 'kelp', etc. +}; + +const iconMap = { + op: '/images/op-logo-red.svg', + ionic: '/img/ionic-sq.png', + turtle: '/images/turtle-ionic.png', + stone: '/img/symbols/32/color/stone.png', + etherfi: '/images/etherfi.png', + kelp: '/images/kelpmiles.png', + eigen: '/images/eigen.png', + spice: '/img/symbols/32/color/bob.png', + anzen: '/img/symbols/32/color/usdz.png', + nektar: '/img/symbols/32/color/nektar.png' +}; + +export const RewardIcons = ({ rewards }: RewardIconsProps) => { + return ( +
+ {rewards.map((reward, index) => ( +
+
+ {reward} +
+
+ ))} +
+ ); +}; diff --git a/packages/ui/app/_components/markets/StakingTile.tsx b/packages/ui/app/_components/markets/StakingTile.tsx index beb4d4f03..96c800fd0 100644 --- a/packages/ui/app/_components/markets/StakingTile.tsx +++ b/packages/ui/app/_components/markets/StakingTile.tsx @@ -20,7 +20,7 @@ export default function StakingTile({ chain }: Iprop) { {+chain === mode.id || +chain === base.id ? ( ) : ( diff --git a/packages/ui/app/_components/markets/SupplyPopover.tsx b/packages/ui/app/_components/markets/SupplyPopover.tsx deleted file mode 100644 index ceac85cd5..000000000 --- a/packages/ui/app/_components/markets/SupplyPopover.tsx +++ /dev/null @@ -1,301 +0,0 @@ -import dynamic from 'next/dynamic'; -import Link from 'next/link'; - -import { pools } from '@ui/constants/index'; -import { useMerklApr } from '@ui/hooks/useMerklApr'; -import { useRewardsBadge } from '@ui/hooks/useRewardsBadge'; -import { multipliers } from '@ui/utils/multipliers'; - -import type { Address } from 'viem'; - -import type { FlywheelReward } from '@ionicprotocol/types'; - -const Rewards = dynamic(() => import('./FlyWheelRewards'), { - ssr: false -}); - -export type SupplyPopoverProps = { - asset: string; - cToken: Address; - dropdownSelectedChain: number; - pool: Address; - selectedPoolId: string; - supplyAPR?: number; - rewards?: FlywheelReward[]; -}; - -export default function SupplyPopover({ - asset, - cToken, - dropdownSelectedChain, - pool, - selectedPoolId, - supplyAPR, - rewards -}: SupplyPopoverProps) { - const isMainModeMarket = - dropdownSelectedChain === 34443 && - (asset === 'USDC' || asset === 'WETH') && - selectedPoolId === '0'; - - const { data: merklApr } = useMerklApr(); - - const merklAprForToken = merklApr?.find( - (a) => Object.keys(a)[0].toLowerCase() === cToken.toLowerCase() - )?.[cToken]; - - const supplyConfig = - multipliers[+dropdownSelectedChain]?.[selectedPoolId]?.[asset]?.supply; - - const showRewardsBadge = useRewardsBadge( - dropdownSelectedChain, - selectedPoolId, - asset, - 'supply', - rewards - ); - - return ( - <> - - + ION APR i - - - {/* Rewards Badge */} - {showRewardsBadge && ( - - {merklAprForToken ? ( - <> - +{' '} - OP{' '} - REWARDS{' '} - - ) : ( - '+ REWARDS ' - )} - i - - )} - - {supplyConfig?.turtle && !isMainModeMarket && ( - - - + TURTLE{' '} - external-link - - - )} -
-
- - Base APR: + - {typeof supplyAPR !== 'undefined' - ? supplyAPR.toLocaleString('en-US', { maximumFractionDigits: 2 }) - : '-'} - % - -
- {merklAprForToken && ( -
- OP - - + OP Rewards:{' '} - {merklAprForToken?.toLocaleString('en-US', { - maximumFractionDigits: 2 - })} - % - -
- )} -

- {supplyConfig?.underlyingAPR && - `Native Asset Yield: +${multipliers[dropdownSelectedChain]?.[ - selectedPoolId - ]?.[asset]?.supply?.underlyingAPR?.toLocaleString('en-US', { - maximumFractionDigits: 2 - })}%`} -

- {supplyConfig?.flywheel && ( - - )} - {(supplyConfig?.ionic ?? 0) > 0 && ( - <> -
- {' '} - +{' '} - { - multipliers[dropdownSelectedChain]?.[selectedPoolId]?.[asset] - ?.supply?.ionic - } - x Ionic Points -
-
- {' '} - + Turtle Ionic Points -
- - )} - {(supplyConfig?.anzen ?? 0) > 0 && ( -
- {' '} - +{' '} - { - multipliers[dropdownSelectedChain]?.[selectedPoolId]?.[asset] - ?.supply?.anzen - } - x Anzen Points -
- )} - {supplyConfig?.turtle && asset === 'STONE' && ( - <> -
- {' '} - + Stone Turtle Points -
- - )} - {supplyConfig?.etherfi && ( - <> -
- {' '} - +{' '} - { - multipliers[dropdownSelectedChain]?.[selectedPoolId]?.[asset] - ?.supply?.etherfi - } - x ether.fi Points -
- - )} - {supplyConfig?.renzo && ( - <> -
- {' '} - +{' '} - { - multipliers[dropdownSelectedChain]?.[selectedPoolId]?.[asset] - ?.supply?.renzo - } - x Renzo Points -
-
- {' '} - + Turtle Renzo Points -
- - )} - {supplyConfig?.kelp && ( - <> -
- {' '} - +{' '} - { - multipliers[dropdownSelectedChain]?.[selectedPoolId]?.[asset] - ?.supply?.kelp - } - x Kelp Miles -
-
- {' '} - + Turtle Kelp Points -
- - )} - {supplyConfig?.eigenlayer && ( -
- {' '} - + EigenLayer Points -
- )} - {supplyConfig?.spice && ( -
- {' '} - + Spice Points -
- )} -
- - ); -} diff --git a/packages/ui/app/_components/markets/SwapWidget.tsx b/packages/ui/app/_components/markets/SwapWidget.tsx index 37128b85d..6883e5a50 100644 --- a/packages/ui/app/_components/markets/SwapWidget.tsx +++ b/packages/ui/app/_components/markets/SwapWidget.tsx @@ -1,12 +1,10 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -'use client'; - -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { LiFiWidget, useWidgetEvents, WidgetEvent } from '@lifi/widget'; import { type Address, zeroAddress } from 'viem'; import { mode } from 'viem/chains'; +import { Dialog, DialogContent } from '@ui/components/ui/dialog'; import { pools } from '@ui/constants/index'; import { getToken } from '@ui/utils/getStakingTokens'; @@ -22,7 +20,7 @@ interface IProps { onRouteExecutionCompleted?: (route: Route) => void; } -export default function Widget({ +export default function SwapWidget({ close, open, toChain, @@ -38,10 +36,9 @@ export default function Widget({ WidgetEvent.RouteExecutionCompleted, onRouteExecutionCompleted ); - return () => widgetEvents.all.clear(); }, [onRouteExecutionCompleted, widgetEvents]); - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const widgetConfig: WidgetConfig = { toChain, fromChain: fromChain ?? toChain, @@ -60,50 +57,29 @@ export default function Widget({ }, sdkConfig: { routeOptions: { - maxPriceImpact: 0.4, // increases threshold to 40% + maxPriceImpact: 0.4, slippage: 0.005 } }, fee: 0.01, - // theme : { palette : "grey"}, integrator: 'ionic', appearance: 'dark' }; - const newRef = useRef(null!); - - useEffect(() => { - const handleOutsideClick = (e: any) => { - //@ts-ignore - if (newRef.current && !newRef.current?.contains(e?.target)) { - close(); - } - }; - document.addEventListener('mousedown', handleOutsideClick); - return () => { - document.removeEventListener('mousedown', handleOutsideClick); - }; - }, [close]); - - // ... - return ( -
-
-
-
+ + ); } - -// export default dynamic(() => Promise.resolve(Widget), { ssr: false }); diff --git a/packages/ui/app/_components/popup/Amount.tsx b/packages/ui/app/_components/popup/Amount.tsx deleted file mode 100644 index eb1e360ec..000000000 --- a/packages/ui/app/_components/popup/Amount.tsx +++ /dev/null @@ -1,186 +0,0 @@ -/* eslint-disable @next/next/no-img-element */ -'use client'; -import React, { useState } from 'react'; - -import dynamic from 'next/dynamic'; - -// import { useSearchParams } from 'next/navigation'; -import { parseUnits } from 'viem'; - -import type { MarketData } from '@ui/types/TokensDataMap'; - -import ResultHandler from '../ResultHandler'; - -interface IAmount { - amount?: string; - availableAssets?: MarketData[]; - handleInput: (val?: string) => void; - hintText?: string; - isLoading?: boolean; - mainText?: string; - max?: string; - readonly?: boolean; - selectedMarketData: MarketData; - setSelectedAsset?: (asset: MarketData) => void; - symbol: string; -} - -const Amount = ({ - selectedMarketData, - handleInput, - amount, - availableAssets, - hintText = 'Wallet Balance', - mainText = 'Amount', - max = '0', - symbol, - isLoading = false, - readonly, - setSelectedAsset -}: IAmount) => { - const [availableAssetsOpen, setAvailableAssetsOpen] = - useState(false); - - function handlInpData(e: React.ChangeEvent) { - const currentValue = e.target.value.trim(); - let newAmount = currentValue === '' ? undefined : currentValue; - const numbersBeforeSeparator = new RegExp(/[0-9]\./gm).test( - currentValue ?? '' - ) - ? 1 - : 0; - - if ( - newAmount && - newAmount.length > 1 && - newAmount[0] === '0' && - newAmount[1] !== '.' - ) { - newAmount = newAmount.slice(1, newAmount.length); - } - - if ( - newAmount && - newAmount.length > - selectedMarketData.underlyingDecimals + 1 + numbersBeforeSeparator - ) { - return; - } - - if ( - newAmount && - parseUnits(max, selectedMarketData.underlyingDecimals) < - parseUnits(newAmount, selectedMarketData.underlyingDecimals) - ) { - handleInput(max); - - return; - } - - handleInput(newAmount); - } - function handleMax(val: string) { - handleInput(val); - } - - return ( -
-
- {mainText} - - {!readonly && ( -
- - <> - - {hintText} {max} - - - - -
- )} -
-
- - -
setAvailableAssetsOpen(!availableAssetsOpen)} - > - link - {symbol} - - {availableAssets && ( - link - )} -
- - {availableAssets && ( -
- {availableAssets.map((asset) => ( -
{ - setSelectedAsset && setSelectedAsset(asset); - setAvailableAssetsOpen(false); - }} - > - link - - {asset.underlyingSymbol} - -
- ))} -
- )} -
-
- ); -}; - -// export default Amount -export default dynamic(() => Promise.resolve(Amount), { ssr: false }); -{ - /*
*/ -} diff --git a/packages/ui/app/_components/popup/Loop.tsx b/packages/ui/app/_components/popup/Loop.tsx deleted file mode 100644 index 23b21d780..000000000 --- a/packages/ui/app/_components/popup/Loop.tsx +++ /dev/null @@ -1,1281 +0,0 @@ -import type { Dispatch, SetStateAction } from 'react'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; - -import dynamic from 'next/dynamic'; -import Image from 'next/image'; - -import { useQueryClient } from '@tanstack/react-query'; -import millify from 'millify'; -import { - type Address, - formatEther, - formatUnits, - parseEther, - parseUnits -} from 'viem'; -import { useBalance, useChainId } from 'wagmi'; - -import { INFO_MESSAGES } from '@ui/constants/index'; -import { useMultiIonic } from '@ui/context/MultiIonicContext'; -import { useCurrentLeverageRatio } from '@ui/hooks/leverage/useCurrentLeverageRatio'; -import { useGetNetApy } from '@ui/hooks/leverage/useGetNetApy'; -import { useGetPositionBorrowApr } from '@ui/hooks/leverage/useGetPositionBorrowApr'; -import { usePositionInfo } from '@ui/hooks/leverage/usePositionInfo'; -import { usePositionsQuery } from '@ui/hooks/leverage/usePositions'; -import { usePositionsSupplyApy } from '@ui/hooks/leverage/usePositionsSupplyApy'; -import { useUsdPrice } from '@ui/hooks/useAllUsdPrices'; -import { useFusePoolData } from '@ui/hooks/useFusePoolData'; -import { useMaxSupplyAmount } from '@ui/hooks/useMaxSupplyAmount'; -import type { MarketData } from '@ui/types/TokensDataMap'; -import { getScanUrlByChainId } from '@ui/utils/networkData'; - -import Amount from './Amount'; -import SliderComponent from './Slider'; -import TransactionStepsHandler, { - useTransactionSteps -} from './TransactionStepsHandler'; -import Modal from '../Modal'; -import Range from '../Range'; -import ResultHandler from '../ResultHandler'; - -import type { OpenPosition } from '@ionicprotocol/types'; - -const SwapWidget = dynamic(() => import('../markets/SwapWidget'), { - ssr: false -}); - -export type LoopProps = { - borrowableAssets: Address[]; - closeLoop: () => void; - comptrollerAddress: Address; - currentBorrowAsset?: MarketData; - isOpen: boolean; - selectedCollateralAsset: MarketData; -}; - -type LoopHealthRatioDisplayProps = { - currentValue: string; - healthRatio: number; - liquidationValue: string; - projectedHealthRatio?: number; -}; - -type LoopInfoDisplayProps = { - aprPercentage?: string; - aprText?: string; - isLoading: boolean; - nativeAmount: string; - symbol: string; - title: string; - usdAmount: string; -}; - -type SupplyActionsProps = { - amount?: string; - comptrollerAddress: LoopProps['comptrollerAddress']; - handleClosePosition: () => void; - isClosingPosition: boolean; - selectedCollateralAsset: LoopProps['selectedCollateralAsset']; - selectedCollateralAssetUSDPrice: number; - setAmount: React.Dispatch>; -}; - -type BorrowActionsProps = { - borrowAmount?: string; - borrowableAssets: LoopProps['borrowableAssets']; - currentLeverage: number; - currentPositionLeverage?: number; - selectedBorrowAsset?: MarketData; - selectedBorrowAssetUSDPrice: number; - setCurrentLeverage: Dispatch>; - setSelectedBorrowAsset: React.Dispatch< - React.SetStateAction - >; -}; - -enum SupplyActionsMode { - DEPOSIT, - WITHDRAW -} - -function LoopHealthRatioDisplay({ - currentValue, - healthRatio, - liquidationValue, - projectedHealthRatio -}: LoopHealthRatioDisplayProps) { - const healthRatioPosition = useCallback((value: number): number => { - if (value < 0) { - return 0; - } - - if (value > 1) { - return 100; - } - - return value * 100; - }, []); - - return ( -
-
- Health Ratio -
- -
-
- -
-
- -
-
- ${liquidationValue} - Liquidation -
- -
- ${currentValue} - Current value -
-
-
- ); -} - -function LoopInfoDisplay({ - aprText, - aprPercentage, - isLoading, - nativeAmount, - symbol, - title, - usdAmount -}: LoopInfoDisplayProps) { - return ( -
-
{title}
- -
-
- - {nativeAmount} $ - {usdAmount} - -
- -
- - - {symbol} -
-
- - {aprText && aprPercentage && ( -
- {aprText} - - - {aprPercentage} - -
- )} -
- ); -} - -function SupplyActions({ - amount, - comptrollerAddress, - handleClosePosition, - isClosingPosition, - selectedCollateralAsset, - selectedCollateralAssetUSDPrice, - setAmount -}: SupplyActionsProps) { - const chainId = useChainId(); - const [mode, setMode] = useState( - SupplyActionsMode.DEPOSIT - ); - const [utilization, setUtilization] = useState(0); - const { data: maxSupplyAmount, isLoading: isLoadingMaxSupply } = - useMaxSupplyAmount(selectedCollateralAsset, comptrollerAddress, chainId); - - const handleSupplyUtilization = (utilizationPercentage: number) => { - if (utilizationPercentage >= 100) { - setAmount( - formatUnits( - maxSupplyAmount?.bigNumber ?? 0n, - parseInt(selectedCollateralAsset.underlyingDecimals.toString()) - ) - ); - - return; - } - - setAmount( - ((utilizationPercentage / 100) * (maxSupplyAmount?.number ?? 0)).toFixed( - parseInt(selectedCollateralAsset.underlyingDecimals.toString()) - ) - ); - }; - - useEffect(() => { - switch (mode) { - case SupplyActionsMode.DEPOSIT: - setUtilization( - Math.round( - (parseFloat(amount ?? '0') / - (maxSupplyAmount && maxSupplyAmount.number > 0 - ? maxSupplyAmount.number - : 1)) * - 100 - ) - ); - - break; - - case SupplyActionsMode.WITHDRAW: - break; - } - }, [amount, maxSupplyAmount, mode]); - - return ( -
-
-
setMode(SupplyActionsMode.DEPOSIT)} - > - Deposit -
- -
setMode(SupplyActionsMode.WITHDRAW)} - > - Withdraw -
-
- - {mode === SupplyActionsMode.DEPOSIT && ( - <> - setAmount(val)} - hintText="Available:" - isLoading={isLoadingMaxSupply} - mainText="AMOUNT TO DEPOSIT" - max={formatUnits( - maxSupplyAmount?.bigNumber ?? 0n, - selectedCollateralAsset.underlyingDecimals - )} - selectedMarketData={selectedCollateralAsset} - symbol={selectedCollateralAsset.underlyingSymbol} - /> - -
- $ - {( - selectedCollateralAssetUSDPrice * parseFloat(amount ?? '0') - ).toFixed(2)} -
- - - - )} - - {mode === SupplyActionsMode.WITHDRAW && ( -
-

- Click the button to withdraw your funds -

- - -
- )} -
- ); -} - -function BorrowActions({ - borrowAmount, - borrowableAssets, - currentLeverage, - currentPositionLeverage, - selectedBorrowAsset, - selectedBorrowAssetUSDPrice, - setCurrentLeverage, - setSelectedBorrowAsset -}: BorrowActionsProps) { - const chainId = useChainId(); - const { data: marketData, isLoading: isLoadingMarketData } = useFusePoolData( - '0', - chainId, - true - ); - const maxLoop = 2; - - return ( - - {selectedBorrowAsset && ( -
-
- - borrowableAssets.find( - (borrowableAsset) => borrowableAsset === asset.cToken - ) - )} - handleInput={() => {}} - hintText="Available:" - isLoading={false} - mainText="AMOUNT TO BORROW" - max={''} - readonly - selectedMarketData={selectedBorrowAsset} - setSelectedAsset={(asset: MarketData) => - setSelectedBorrowAsset(asset) - } - symbol={selectedBorrowAsset.underlyingSymbol} - /> -
- -
- $ - {( - selectedBorrowAssetUSDPrice * parseFloat(borrowAmount ?? '0') - ).toFixed(2)} -
- -
-
- LOOP -
- {(currentLeverage - 1).toFixed(1)} -
-
- -
-
- {[ - '0x', - '1x', - '2x', - '3x', - '4x', - '5x', - '6x', - '7x', - '8x', - '9x', - '10x' - ].map((label, i) => ( - maxLoop && 'text-white/20'} ${ - currentLeverage === i + 1 && '!text-accent' - } `} - key={`label-${label}`} - onClick={() => - setCurrentLeverage(i > maxLoop ? maxLoop + 1 : i + 1) - } - style={{ left: `${(i / 10) * 100}%` }} - > - {label} - - ))} -
- - - setCurrentLeverage(val > maxLoop ? maxLoop + 1 : val + 1) - } - step={1} - /> - -
- {'<'} Repay - - Borrow {'>'} -
-
-
-
- )} -
- ); -} - -export default function Loop({ - borrowableAssets, - closeLoop, - comptrollerAddress, - currentBorrowAsset, - selectedCollateralAsset, - isOpen -}: LoopProps) { - const chainId = useChainId(); - const [amount, setAmount] = useState(); - const amountAsBInt = useMemo( - () => parseUnits(amount ?? '0', selectedCollateralAsset.underlyingDecimals), - [amount, selectedCollateralAsset] - ); - const [swapWidgetOpen, setSwapWidgetOpen] = useState(false); - const { data: marketData } = useFusePoolData('0', chainId, true); - const { data: usdPrice } = useUsdPrice(chainId.toString()); - const [selectedBorrowAsset, setSelectedBorrowAsset] = useState< - MarketData | undefined - >(currentBorrowAsset); - const { data: positions, refetch: refetchPositions } = - usePositionsQuery(chainId); - const currentPosition = useMemo(() => { - return positions?.openPositions.find( - (position) => - position.borrowable.underlyingToken === - selectedBorrowAsset?.underlyingToken && - position.collateral.underlyingToken === - selectedCollateralAsset.underlyingToken && - !position.isClosed - ); - }, [positions, selectedBorrowAsset, selectedCollateralAsset]); - const { data: currentPositionLeverageRatio } = useCurrentLeverageRatio( - currentPosition?.address ?? ('' as Address), - chainId - ); - const collateralsAPR = usePositionsSupplyApy( - positions?.openPositions.map((position) => position.collateral) ?? [], - positions?.openPositions.map((position) => position.chainId) ?? [] - ); - const { - data: positionInfo, - isFetching: isFetchingPositionInfo, - refetch: refetchPositionInfo - } = usePositionInfo( - currentPosition?.address ?? ('' as Address), - collateralsAPR && - collateralsAPR[selectedCollateralAsset.cToken] !== undefined - ? parseEther( - collateralsAPR[selectedCollateralAsset.cToken].totalApy.toFixed(18) - ) - : undefined, - chainId - ); - const { data: positionNetApy, isFetching: isFetchingPositionNetApy } = - useGetNetApy( - selectedCollateralAsset.cToken, - selectedBorrowAsset?.cToken ?? ('' as Address), - positionInfo?.equityAmount, - currentPositionLeverageRatio, - collateralsAPR && - collateralsAPR[selectedCollateralAsset.cToken] !== undefined - ? parseEther( - collateralsAPR[selectedCollateralAsset.cToken].totalApy.toFixed(18) - ) - : undefined, - chainId - ); - const [currentLeverage, setCurrentLeverage] = useState(1); - const { data: borrowApr } = useGetPositionBorrowApr({ - amount: amountAsBInt, - borrowMarket: selectedBorrowAsset?.cToken ?? ('' as Address), - collateralMarket: selectedCollateralAsset.cToken, - leverage: parseEther(currentLeverage.toString()) - }); - - const { - borrowedAssetAmount, - borrowedToCollateralRatio, - positionValueMillified, - projectedHealthRatio, - liquidationValue, - healthRatio, - projectedCollateral, - projectedBorrowAmount, - projectedCollateralValue, - selectedBorrowAssetUSDPrice, - selectedCollateralAssetUSDPrice - } = useMemo(() => { - const selectedCollateralAssetUSDPrice = - (usdPrice ?? 0) * - parseFloat(formatEther(selectedCollateralAsset.underlyingPrice)); - const selectedBorrowAssetUSDPrice = - usdPrice && selectedBorrowAsset - ? (usdPrice ?? 0) * - parseFloat(formatEther(selectedBorrowAsset.underlyingPrice)) - : 0; - const positionValue = - Number(formatEther(positionInfo?.positionSupplyAmount ?? 0n)) * - (selectedCollateralAssetUSDPrice ?? 0); - const liquidationValue = - positionValue * Number(formatEther(positionInfo?.safetyBuffer ?? 0n)); - const healthRatio = positionValue / liquidationValue - 1; - const borrowedToCollateralRatio = - selectedBorrowAssetUSDPrice / selectedCollateralAssetUSDPrice; - const borrowedAssetAmount = Number( - formatUnits( - positionInfo?.debtAmount ?? 0n, - currentPosition?.borrowable.underlyingDecimals ?? 18 - ) - ); - const projectedCollateral = formatUnits( - positionInfo?.equityAmount ?? 0n + amountAsBInt, - selectedCollateralAsset.underlyingDecimals - ); - const projectedCollateralValue = - Number(projectedCollateral) * selectedCollateralAssetUSDPrice; - const projectedBorrowAmount = - (Number(projectedCollateral) / borrowedToCollateralRatio) * - (currentLeverage - 1); - const projectedHealthRatio = currentPosition - ? (projectedCollateralValue + - projectedBorrowAmount * selectedBorrowAssetUSDPrice) / - liquidationValue - - 1 - : undefined; - - return { - borrowedAssetAmount, - borrowedToCollateralRatio, - healthRatio, - liquidationValue, - positionValue, - positionValueMillified: `${millify(positionValue)}`, - projectedBorrowAmount, - projectedCollateral, - projectedCollateralValue, - projectedHealthRatio, - selectedBorrowAssetUSDPrice, - selectedCollateralAssetUSDPrice - }; - }, [ - amountAsBInt, - currentLeverage, - currentPosition, - selectedBorrowAsset, - selectedCollateralAsset, - positionInfo, - usdPrice - ]); - const { currentSdk, address } = useMultiIonic(); - const { addStepsForAction, transactionSteps, upsertTransactionStep } = - useTransactionSteps(); - const { refetch: refetchBalance } = useBalance({ - address, - token: selectedCollateralAsset.underlyingToken as `0x${string}` - }); - const queryClient = useQueryClient(); - - /** - * Force new borrow asset - * when currentBorrowAsset - * is present - */ - useEffect(() => { - if (currentBorrowAsset && isOpen) { - setSelectedBorrowAsset(currentBorrowAsset); - } - }, [currentBorrowAsset, isOpen]); - - /** - * Update selected borrow asset - * when market data loads - */ - useEffect(() => { - if (!selectedBorrowAsset && marketData) { - setSelectedBorrowAsset( - marketData.assets.filter((asset) => - borrowableAssets.find( - (borrowableAsset) => borrowableAsset === asset.cToken - ) - )[0] - ); - } - }, [borrowableAssets, marketData, selectedBorrowAsset]); - - /** - * Reset neccessary queries after actions - */ - const resetQueries = async (): Promise => { - queryClient.invalidateQueries({ queryKey: ['useCurrentLeverageRatio'] }); - queryClient.invalidateQueries({ queryKey: ['useGetNetApy'] }); - queryClient.invalidateQueries({ queryKey: ['usePositionInfo'] }); - queryClient.invalidateQueries({ queryKey: ['positions'] }); - queryClient.invalidateQueries({ queryKey: ['useMaxSupplyAmount'] }); - await refetchBalance(); - await refetchPositionInfo(); - await refetchPositions(); - }; - - /** - * Handle position opening - */ - const handleOpenPosition = async (): Promise => { - if (!currentSdk || !address) { - return; - } - - let currentTransactionStep = 0; - - addStepsForAction([ - { - error: false, - message: INFO_MESSAGES.OPEN_POSITION.APPROVE, - success: false - }, - { - error: false, - message: INFO_MESSAGES.OPEN_POSITION.OPENING, - success: false - } - ]); - - try { - const token = currentSdk.getEIP20TokenInstance( - selectedCollateralAsset.underlyingToken, - currentSdk.publicClient as any - ); - const factory = currentSdk.createLeveredPositionFactory(); - const hasApprovedEnough = - (await token.read.allowance([address, factory.address])) >= - amountAsBInt; - - if (!hasApprovedEnough) { - const tx = await currentSdk.approve( - factory.address, - selectedCollateralAsset.underlyingToken, - amountAsBInt - ); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - txHash: tx - } - }); - - await currentSdk.publicClient.waitForTransactionReceipt({ hash: tx }); - } - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - - currentTransactionStep++; - - const tx = await currentSdk.createAndFundPositionAtRatio( - selectedCollateralAsset.cToken, - selectedBorrowAsset?.cToken ?? ('' as Address), - selectedCollateralAsset.underlyingToken, - amountAsBInt, - parseEther(currentLeverage.toString()) - ); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - txHash: tx - } - }); - - await currentSdk.publicClient.waitForTransactionReceipt({ hash: tx }); - await refetchPositions(); - setAmount('0'); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - } catch (error) { - console.error(error); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - error: true - } - }); - } - }; - - /** - * Handle leverage adjustment - */ - const handleLeverageAdjustment = async (): Promise => { - const currentTransactionStep = 0; - - addStepsForAction([ - { - error: false, - message: INFO_MESSAGES.ADJUST_LEVERAGE.ADJUSTING, - success: false - } - ]); - - try { - const tx = await currentSdk?.adjustLeverageRatio( - currentPosition?.address ?? ('' as Address), - currentLeverage - ); - - if (!tx) { - throw new Error('Error while adjusting leverage'); - } - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - txHash: tx - } - }); - - await currentSdk?.publicClient.waitForTransactionReceipt({ hash: tx }); - await refetchPositions(); - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - } catch (error) { - console.error(error); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - error: true - } - }); - } - }; - - /** - * Handle position funding - */ - const handlePositionFunding = async (): Promise => { - if (!currentSdk || !address || !currentPosition) { - return; - } - - let currentTransactionStep = 0; - - addStepsForAction([ - { - error: false, - message: INFO_MESSAGES.FUNDING_POSITION.APPROVE, - success: false - }, - { - error: false, - message: INFO_MESSAGES.FUNDING_POSITION.FUNDING, - success: false - } - ]); - - try { - const token = currentSdk.getEIP20TokenInstance( - selectedCollateralAsset.underlyingToken, - currentSdk.walletClient as any - ); - const hasApprovedEnough = - (await token.read.allowance([address, currentPosition.address])) >= - amountAsBInt; - - if (!hasApprovedEnough) { - const tx = await currentSdk.approve( - currentPosition.address, - selectedCollateralAsset.underlyingToken, - amountAsBInt - ); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - txHash: tx - } - }); - - await currentSdk.publicClient.waitForTransactionReceipt({ hash: tx }); - } - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - - currentTransactionStep++; - - const tx = await currentSdk.fundPosition( - currentPosition?.address ?? '', - selectedCollateralAsset.underlyingToken, - amountAsBInt - ); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - txHash: tx - } - }); - - await currentSdk.publicClient.waitForTransactionReceipt({ hash: tx }); - - setAmount('0'); - await refetchPositions(); - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - } catch (error) { - console.error(error); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - error: true - } - }); - } - }; - - /** - * Handle position closing - */ - const handleClosePosition = async (): Promise => { - const currentTransactionStep = 0; - - addStepsForAction([ - { - error: false, - message: INFO_MESSAGES.CLOSE_POSITION.CLOSING, - success: false - } - ]); - - try { - const tx = await currentSdk?.closeLeveredPosition( - currentPosition?.address ?? ('' as Address) - ); - - if (!tx) { - throw new Error('Error while closing position'); - } - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - txHash: tx - } - }); - - await currentSdk?.publicClient.waitForTransactionReceipt({ hash: tx }); - - await refetchPositions(); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - } catch (error) { - console.error(error); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - error: true - } - }); - } - }; - - /** - * Handle transaction steps reset - */ - const handleTransactionStepsReset = async (): Promise => { - resetQueries(); - upsertTransactionStep(undefined); - }; - - return ( - <> - {isOpen && ( - <> - setSwapWidgetOpen(false)} - open={swapWidgetOpen} - fromChain={chainId} - toChain={chainId} - toToken={selectedCollateralAsset.underlyingToken} - /> - closeLoop()}> -
- - - {selectedCollateralAsset.underlyingSymbol} -
- -
- {currentPosition - ? `Loop Position Found: ` - : 'No Loop Position Found, Create a New One'} - {currentPosition && ( - - 0x{currentPosition.address.slice(2, 4)}... - {currentPosition.address.slice(-6)} - - )} -
- - -
-
-
- Position Value - - - ${positionValueMillified} - - -
-
- Net APR - - - {positionNetApy?.toFixed(2) ?? '0.00'}% - - -
- - {/*
- Annual yield - - TODO - -
*/} -
- -
- -
- - 0 || - (!!currentPositionLeverageRatio && - Math.round(currentPositionLeverageRatio) !== - currentLeverage) - ? projectedHealthRatio - : undefined - } - /> -
- -
- -
- - -
- -
- - -
- -
- - {currentPosition && ( - <> -
- - -
- -
- - -
- -
- - )} - -
- - -
- -
- - -
- -
- - <> - {transactionSteps.length > 0 ? ( -
- -
- ) : ( - <> - {currentPosition ? ( -
- - - -
- ) : ( - - )} - - )} - -
-
- - - )} - - ); -} diff --git a/packages/ui/app/_components/popup/Tab.tsx b/packages/ui/app/_components/popup/Tab.tsx deleted file mode 100644 index c71fe30ef..000000000 --- a/packages/ui/app/_components/popup/Tab.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { PopupMode } from './page'; -interface IMode { - active: PopupMode; - loopPossible: boolean; - borrowPossible: boolean; - mode: PopupMode; - setActive: (val: PopupMode) => void; -} -const Tab = ({ - loopPossible, - mode, - setActive, - active, - borrowPossible -}: IMode) => { - return ( -
- {(mode === PopupMode.SUPPLY || mode === PopupMode.WITHDRAW) && ( - <> -

setActive(PopupMode.SUPPLY)} - > - COLLATERAL -

-

setActive(PopupMode.WITHDRAW)} - > - WITHDRAW -

- - )} - {(mode === PopupMode.BORROW || - mode === PopupMode.REPAY || - mode === PopupMode.LOOP) && ( - <> - {borrowPossible && ( - <> -

setActive(PopupMode.BORROW)} - > - BORROW -

-

setActive(PopupMode.REPAY)} - > - REPAY -

- - )} - - {loopPossible && ( -

setActive(PopupMode.LOOP)} - > - LOOP -

- )} - - )} -
- ); -}; - -export default Tab; diff --git a/packages/ui/app/_components/popup/page.tsx b/packages/ui/app/_components/popup/page.tsx deleted file mode 100644 index a86dfc724..000000000 --- a/packages/ui/app/_components/popup/page.tsx +++ /dev/null @@ -1,1866 +0,0 @@ -'use client'; -/* eslint-disable @next/next/no-img-element */ -// import { useSearchParams } from 'next/navigation'; -import { useEffect, useMemo, useReducer, useRef, useState } from 'react'; - -import dynamic from 'next/dynamic'; - -import { useQueryClient } from '@tanstack/react-query'; -import millify from 'millify'; -import toast from 'react-hot-toast'; -import { - type Address, - formatEther, - formatUnits, - maxUint256, - parseEther, - parseUnits -} from 'viem'; -import { useChainId } from 'wagmi'; - -import { INFO_MESSAGES } from '@ui/constants/index'; -import { useMultiIonic } from '@ui/context/MultiIonicContext'; -import { useBorrowCapsDataForAsset } from '@ui/hooks/ionic/useBorrowCapsDataForAsset'; -import { useSupplyCapsDataForAsset } from '@ui/hooks/ionic/useSupplyCapsDataForPool'; -import useUpdatedUserAssets from '@ui/hooks/ionic/useUpdatedUserAssets'; -import { - useHealthFactor, - useHealthFactorPrediction -} from '@ui/hooks/pools/useHealthFactor'; -import { useUsdPrice } from '@ui/hooks/useAllUsdPrices'; -import { useBorrowMinimum } from '@ui/hooks/useBorrowMinimum'; -import type { LoopMarketData } from '@ui/hooks/useLoopMarkets'; -import { useMaxBorrowAmount } from '@ui/hooks/useMaxBorrowAmount'; -import { useMaxRepayAmount } from '@ui/hooks/useMaxRepayAmount'; -import { useMaxSupplyAmount } from '@ui/hooks/useMaxSupplyAmount'; -import { useMaxWithdrawAmount } from '@ui/hooks/useMaxWithdrawAmount'; -import { useTotalSupplyAPYs } from '@ui/hooks/useTotalSupplyAPYs'; -import type { MarketData } from '@ui/types/TokensDataMap'; -import { errorCodeToMessage } from '@ui/utils/errorCodeToMessage'; -import { getBlockTimePerMinuteByChainId } from '@ui/utils/networkData'; - -import Amount from './Amount'; -import MemoizedDonutChart from './DonutChart'; -import Loop from './Loop'; -import SliderComponent from './Slider'; -import Tab from './Tab'; -import TransactionStepsHandler, { - useTransactionSteps -} from './TransactionStepsHandler'; -import ResultHandler from '../ResultHandler'; - -import { FundOperationMode } from '@ionicprotocol/types'; - -const SwapWidget = dynamic(() => import('../markets/SwapWidget'), { - ssr: false -}); - -export enum PopupMode { - SUPPLY = 1, - WITHDRAW, - BORROW, - REPAY, - LOOP -} - -export enum HFPStatus { - CRITICAL = 'CRITICAL', - NORMAL = 'NORMAL', - UNKNOWN = 'UNKNOWN', - WARNING = 'WARNING' -} - -interface IPopup { - closePopup: () => void; - comptrollerAddress: Address; - loopMarkets?: LoopMarketData; - mode?: PopupMode; - selectedMarketData: MarketData; -} -const Popup = ({ - mode = PopupMode.SUPPLY, - loopMarkets, - selectedMarketData, - closePopup, - comptrollerAddress -}: IPopup) => { - const { addStepsForAction, transactionSteps, upsertTransactionStep } = - useTransactionSteps(); - const { currentSdk, address } = useMultiIonic(); - const chainId = useChainId(); - const { data: usdPrice } = useUsdPrice(chainId.toString()); - const pricePerSingleAsset = useMemo( - () => - parseFloat(formatEther(selectedMarketData.underlyingPrice)) * - (usdPrice ?? 0), - [selectedMarketData, usdPrice] - ); - const { data: supplyCap } = useSupplyCapsDataForAsset( - comptrollerAddress, - selectedMarketData.cToken, - chainId - ); - const supplyCapAsNumber = useMemo( - () => - parseFloat( - formatUnits( - supplyCap?.supplyCaps ?? 0n, - selectedMarketData.underlyingDecimals - ) - ), - [supplyCap, selectedMarketData.underlyingDecimals] - ); - const supplyCapAsFiat = useMemo( - () => pricePerSingleAsset * supplyCapAsNumber, - [pricePerSingleAsset, supplyCapAsNumber] - ); - const totalSupplyAsNumber = useMemo( - () => - parseFloat( - formatUnits( - selectedMarketData.totalSupply, - selectedMarketData.underlyingDecimals - ) - ), - [selectedMarketData.totalSupply, selectedMarketData.underlyingDecimals] - ); - const { data: borrowCap } = useBorrowCapsDataForAsset( - selectedMarketData.cToken, - chainId - ); - const borrowCapAsNumber = useMemo( - () => - parseFloat( - formatUnits( - borrowCap?.totalBorrowCap ?? 0n, - selectedMarketData.underlyingDecimals - ) - ), - [borrowCap, selectedMarketData.underlyingDecimals] - ); - const borrowCapAsFiat = useMemo( - () => pricePerSingleAsset * borrowCapAsNumber, - [pricePerSingleAsset, borrowCapAsNumber] - ); - const totalBorrowAsNumber = useMemo( - () => - parseFloat( - formatUnits( - selectedMarketData.totalBorrow, - selectedMarketData.underlyingDecimals - ) - ), - [selectedMarketData.totalBorrow, selectedMarketData.underlyingDecimals] - ); - const { data: minBorrowAmount } = useBorrowMinimum( - selectedMarketData, - chainId - ); - const { - data: maxSupplyAmount, - isLoading: isLoadingMaxSupply, - refetch: refetchMaxSupplyAmount - } = useMaxSupplyAmount(selectedMarketData, comptrollerAddress, chainId); - const { data: assetsSupplyAprData } = useTotalSupplyAPYs( - [selectedMarketData], - chainId - ); - const collateralApr = useMemo(() => { - // Todo: add the market rewards to this calculation - if (assetsSupplyAprData) { - return `${assetsSupplyAprData[selectedMarketData.cToken].apy.toFixed( - 2 - )}%`; - } - - return '0.00%'; - }, [assetsSupplyAprData, selectedMarketData.cToken]); - const [active, setActive] = useState(mode); - const slide = useRef(null!); - const [amount, setAmount] = useReducer( - (_: string | undefined, value: string | undefined): string | undefined => - value, - '0' - ); - const { data: maxRepayAmount, isLoading: isLoadingMaxRepayAmount } = - useMaxRepayAmount(selectedMarketData, chainId); - const amountAsBInt = useMemo( - () => - parseUnits( - amount?.toString() ?? '0', - selectedMarketData.underlyingDecimals - ), - [amount, selectedMarketData.underlyingDecimals] - ); - const { data: maxBorrowAmount, isLoading: isLoadingMaxBorrowAmount } = - useMaxBorrowAmount(selectedMarketData, comptrollerAddress, chainId); - - // const setBorrow = useStore((state) => state.setBorrowAmount); - - const { data: healthFactor } = useHealthFactor(comptrollerAddress, chainId); - const { - data: _predictedHealthFactor, - isLoading: isLoadingPredictedHealthFactor - } = useHealthFactorPrediction( - comptrollerAddress, - address ?? ('' as Address), - selectedMarketData.cToken, - active === PopupMode.WITHDRAW - ? (amountAsBInt * BigInt(1e18)) / selectedMarketData.exchangeRate - : parseUnits('0', selectedMarketData.underlyingDecimals), - active === PopupMode.BORROW - ? amountAsBInt - : parseUnits('0', selectedMarketData.underlyingDecimals), - active === PopupMode.REPAY - ? (amountAsBInt * BigInt(1e18)) / selectedMarketData.exchangeRate - : parseUnits('0', selectedMarketData.underlyingDecimals) - ); - - const currentBorrowAmountAsFloat = useMemo( - () => parseFloat(selectedMarketData.borrowBalance.toString()), - [selectedMarketData] - ); - const [currentUtilizationPercentage, setCurrentUtilizationPercentage] = - useState(0); - const [currentFundOperation, setCurrentFundOperation] = - useState(FundOperationMode.SUPPLY); - const { data: updatedAssets, isLoading: isLoadingUpdatedAssets } = - useUpdatedUserAssets({ - amount: amountAsBInt, - assets: [selectedMarketData], - index: 0, - mode: currentFundOperation, - poolChainId: chainId - }); - const updatedAsset = updatedAssets ? updatedAssets[0] : undefined; - const { data: maxWithdrawAmount, isLoading: isLoadingMaxWithdrawAmount } = - useMaxWithdrawAmount(selectedMarketData, chainId); - const { - supplyAPY, - borrowAPR, - updatedSupplyAPY, - updatedBorrowAPR, - supplyBalanceFrom, - supplyBalanceTo, - borrowBalanceFrom, - borrowBalanceTo - } = useMemo(() => { - const blocksPerMinute = getBlockTimePerMinuteByChainId(chainId); - - if (currentSdk) { - return { - borrowAPR: currentSdk.ratePerBlockToAPY( - selectedMarketData.borrowRatePerBlock, - blocksPerMinute - ), - borrowBalanceFrom: Number( - formatUnits( - selectedMarketData.borrowBalance, - selectedMarketData.underlyingDecimals - ) - ).toLocaleString('en-US', { maximumFractionDigits: 2 }), - borrowBalanceTo: updatedAsset - ? Number( - formatUnits( - updatedAsset.borrowBalance, - updatedAsset.underlyingDecimals - ) - ).toLocaleString('en-US', { maximumFractionDigits: 2 }) - : undefined, - supplyAPY: currentSdk.ratePerBlockToAPY( - selectedMarketData.supplyRatePerBlock, - blocksPerMinute - ), - supplyBalanceFrom: Number( - formatUnits( - selectedMarketData.supplyBalance, - selectedMarketData.underlyingDecimals - ) - ).toLocaleString('en-US', { maximumFractionDigits: 2 }), - supplyBalanceTo: updatedAsset - ? Math.abs( - Number( - formatUnits( - updatedAsset.supplyBalance, - updatedAsset.underlyingDecimals - ) - ) - ).toLocaleString('en-US', { maximumFractionDigits: 2 }) - : undefined, - totalBorrows: updatedAssets?.reduce( - (acc, cur) => acc + cur.borrowBalanceFiat, - 0 - ), - updatedBorrowAPR: updatedAsset - ? currentSdk.ratePerBlockToAPY( - updatedAsset.borrowRatePerBlock, - blocksPerMinute - ) - : undefined, - updatedSupplyAPY: updatedAsset - ? currentSdk.ratePerBlockToAPY( - updatedAsset.supplyRatePerBlock, - blocksPerMinute - ) - : undefined, - updatedTotalBorrows: updatedAssets - ? updatedAssets.reduce((acc, cur) => acc + cur.borrowBalanceFiat, 0) - : undefined - }; - } - - return {}; - }, [chainId, updatedAsset, selectedMarketData, updatedAssets, currentSdk]); - const [enableCollateral, setEnableCollateral] = useState( - selectedMarketData.membership && selectedMarketData.supplyBalance > 0n - ); - const [isMounted, setIsMounted] = useState(false); - const [loopOpen, setLoopOpen] = useState(false); - const [swapWidgetOpen, setSwapWidgetOpen] = useState(false); - const predictedHealthFactor = useMemo(() => { - if (updatedAsset && updatedAsset?.supplyBalanceFiat < 0.01) { - return maxUint256; - } - - if (amountAsBInt === 0n) { - return parseEther(healthFactor ?? '0'); - } - - return _predictedHealthFactor; - }, [_predictedHealthFactor, updatedAsset, amountAsBInt, healthFactor]); - - const hfpStatus = useMemo(() => { - if (!predictedHealthFactor) { - return HFPStatus.UNKNOWN; - } - - if (predictedHealthFactor === maxUint256) { - return HFPStatus.NORMAL; - } - - if (updatedAsset && updatedAsset?.supplyBalanceFiat < 0.01) { - return HFPStatus.NORMAL; - } - - const predictedHealthFactorNumber = Number( - formatEther(predictedHealthFactor) - ); - - if (predictedHealthFactorNumber <= 1.1) { - return HFPStatus.CRITICAL; - } - - if (predictedHealthFactorNumber <= 1.2) { - return HFPStatus.WARNING; - } - - return HFPStatus.NORMAL; - }, [predictedHealthFactor, updatedAsset]); - const queryClient = useQueryClient(); - - /** - * Fade in animation - */ - useEffect(() => { - setIsMounted(true); - }, []); - - useEffect(() => { - let closeTimer: ReturnType; - - if (!isMounted) { - closeTimer = setTimeout(() => { - closePopup(); - }, 301); - } - - return () => { - clearTimeout(closeTimer); - }; - }, [isMounted, closePopup]); - - /** - * Update utilization percentage when amount changes - */ - useEffect(() => { - switch (active) { - case PopupMode.SUPPLY: { - const div = - Number(formatEther(amountAsBInt)) / - (maxSupplyAmount?.bigNumber && maxSupplyAmount.number > 0 - ? Number(formatEther(maxSupplyAmount?.bigNumber)) - : 1); - setCurrentUtilizationPercentage(Math.round(div * 100)); - - break; - } - - case PopupMode.WITHDRAW: { - const div = - Number(formatEther(amountAsBInt)) / - (maxWithdrawAmount && maxWithdrawAmount > 0n - ? Number(formatEther(maxWithdrawAmount)) - : 1); - setCurrentUtilizationPercentage(Math.round(div * 100)); - - break; - } - - case PopupMode.BORROW: { - const div = - Number(formatEther(amountAsBInt)) / - (maxBorrowAmount?.bigNumber && maxBorrowAmount.number > 0 - ? Number(formatEther(maxBorrowAmount?.bigNumber)) - : 1); - setCurrentUtilizationPercentage(Math.round(div * 100)); - // setBorrow( - // maxBorrowAmount?.bigNumber && maxBorrowAmount.number > 0 - // ? formatEther(maxBorrowAmount?.bigNumber) - // : '' - // ); - break; - } - - case PopupMode.REPAY: { - const div = - Number(formatEther(amountAsBInt)) / - (maxRepayAmount && maxRepayAmount > 0n - ? Number(formatEther(maxRepayAmount)) - : 1); - setCurrentUtilizationPercentage(Math.round(div * 100)); - - break; - } - - case PopupMode.LOOP: { - setLoopOpen(true); - - break; - } - } - }, [ - amountAsBInt, - active, - maxBorrowAmount, - maxRepayAmount, - maxSupplyAmount, - maxWithdrawAmount - // setBorrow - ]); - - useEffect(() => { - setAmount('0'); - setCurrentUtilizationPercentage(0); - upsertTransactionStep(undefined); - - switch (active) { - case PopupMode.SUPPLY: - slide.current.style.transform = 'translateX(0%)'; - - setCurrentFundOperation(FundOperationMode.SUPPLY); - - break; - - case PopupMode.WITHDRAW: - slide.current.style.transform = 'translateX(-100%)'; - - setCurrentFundOperation(FundOperationMode.WITHDRAW); - - break; - - case PopupMode.BORROW: - slide.current.style.transform = 'translateX(0%)'; - - setCurrentFundOperation(FundOperationMode.BORROW); - - break; - - case PopupMode.REPAY: - slide.current.style.transform = 'translateX(-100%)'; - - setCurrentFundOperation(FundOperationMode.REPAY); - - break; - } - }, [active, mode, upsertTransactionStep]); - - const initiateCloseAnimation = () => setIsMounted(false); - - const handleSupplyUtilization = (utilizationPercentage: number) => { - if (utilizationPercentage >= 100) { - setAmount( - formatUnits( - maxSupplyAmount?.bigNumber ?? 0n, - parseInt(selectedMarketData.underlyingDecimals.toString()) - ) - ); - - return; - } - - setAmount( - ((utilizationPercentage / 100) * (maxSupplyAmount?.number ?? 0)).toFixed( - parseInt(selectedMarketData.underlyingDecimals.toString()) - ) - ); - }; - - const handleWithdrawUtilization = (utilizationPercentage: number) => { - if (utilizationPercentage >= 100) { - setAmount( - formatUnits( - maxWithdrawAmount ?? 0n, - parseInt(selectedMarketData.underlyingDecimals.toString()) - ) - ); - - return; - } - - setAmount( - ( - (utilizationPercentage / 100) * - parseFloat( - formatUnits( - maxWithdrawAmount ?? 0n, - selectedMarketData.underlyingDecimals - ) ?? '0.0' - ) - ).toFixed(parseInt(selectedMarketData.underlyingDecimals.toString())) - ); - }; - - const handleBorrowUtilization = (utilizationPercentage: number) => { - if (utilizationPercentage >= 100) { - setAmount( - formatUnits( - maxBorrowAmount?.bigNumber ?? 0n, - parseInt(selectedMarketData.underlyingDecimals.toString()) - ) - ); - - return; - } - - setAmount( - ((utilizationPercentage / 100) * (maxBorrowAmount?.number ?? 0)).toFixed( - parseInt(selectedMarketData.underlyingDecimals.toString()) - ) - ); - }; - - const handleRepayUtilization = (utilizationPercentage: number) => { - if (utilizationPercentage >= 100) { - setAmount( - formatUnits( - maxRepayAmount ?? 0n, - parseInt(selectedMarketData.underlyingDecimals.toString()) - ) - ); - - return; - } - - setAmount( - ( - (utilizationPercentage / 100) * - parseFloat( - formatUnits( - maxRepayAmount ?? 0n, - selectedMarketData.underlyingDecimals - ) ?? '0.0' - ) - ).toFixed(parseInt(selectedMarketData.underlyingDecimals.toString())) - ); - }; - - const resetTransactionSteps = () => { - refetchUsedQueries(); - upsertTransactionStep(undefined); - initiateCloseAnimation(); - }; - - const refetchUsedQueries = async () => { - queryClient.invalidateQueries({ queryKey: ['useFusePoolData'] }); - queryClient.invalidateQueries({ queryKey: ['useBorrowMinimum'] }); - queryClient.invalidateQueries({ queryKey: ['useUsdPrice'] }); - queryClient.invalidateQueries({ queryKey: ['useAllUsdPrices'] }); - queryClient.invalidateQueries({ queryKey: ['useTotalSupplyAPYs'] }); - queryClient.invalidateQueries({ queryKey: ['useUpdatedUserAssets'] }); - queryClient.invalidateQueries({ queryKey: ['useMaxSupplyAmount'] }); - queryClient.invalidateQueries({ queryKey: ['useMaxWithdrawAmount'] }); - queryClient.invalidateQueries({ queryKey: ['useMaxBorrowAmount'] }); - queryClient.invalidateQueries({ queryKey: ['useMaxRepayAmount'] }); - queryClient.invalidateQueries({ - queryKey: ['useSupplyCapsDataForPool'] - }); - queryClient.invalidateQueries({ - queryKey: ['useBorrowCapsDataForAsset'] - }); - }; - - const supplyAmount = async () => { - if ( - !transactionSteps.length && - currentSdk && - address && - amount && - amountAsBInt > 0n && - maxSupplyAmount && - amountAsBInt <= maxSupplyAmount.bigNumber - ) { - let currentTransactionStep = 0; - addStepsForAction([ - { - error: false, - message: INFO_MESSAGES.SUPPLY.APPROVE, - success: false - }, - ...(enableCollateral && !selectedMarketData.membership - ? [ - { - error: false, - message: INFO_MESSAGES.SUPPLY.COLLATERAL, - success: false - } - ] - : []), - { - error: false, - message: INFO_MESSAGES.SUPPLY.SUPPLYING, - success: false - } - ]); - - try { - const token = currentSdk.getEIP20TokenInstance( - selectedMarketData.underlyingToken, - currentSdk.publicClient as any - ); - const hasApprovedEnough = - (await token.read.allowance([address, selectedMarketData.cToken])) >= - amountAsBInt; - - if (!hasApprovedEnough) { - const tx = await currentSdk.approve( - selectedMarketData.cToken, - selectedMarketData.underlyingToken, - (amountAsBInt * 105n) / 100n - ); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - txHash: tx - } - }); - - await currentSdk.publicClient.waitForTransactionReceipt({ - hash: tx, - confirmations: 2 - }); - - // wait for 5 seconds to resolve timing issue - await new Promise((resolve) => setTimeout(resolve, 5000)); - } - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - - currentTransactionStep++; - - if (enableCollateral && !selectedMarketData.membership) { - const tx = await currentSdk.enterMarkets( - selectedMarketData.cToken, - comptrollerAddress - ); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - txHash: tx - } - }); - - await currentSdk.publicClient.waitForTransactionReceipt({ hash: tx }); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - - currentTransactionStep++; - } - - const { tx, errorCode } = await currentSdk.mint( - selectedMarketData.cToken, - amountAsBInt - ); - - if (errorCode) { - console.error(errorCode); - - throw new Error('Error during supplying!'); - } - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - txHash: tx - } - }); - - tx && - (await currentSdk.publicClient.waitForTransactionReceipt({ - hash: tx - })); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - - toast.success( - `Supplied ${amount} ${selectedMarketData.underlyingSymbol}` - ); - } catch (error) { - toast.error('Error while supplying!'); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - error: true - } - }); - } - } - }; - - const withdrawAmount = async () => { - if ( - !transactionSteps.length && - currentSdk && - address && - amount && - amountAsBInt > 0n && - maxWithdrawAmount - ) { - const currentTransactionStep = 0; - addStepsForAction([ - { - error: false, - message: INFO_MESSAGES.WITHDRAW.WITHDRAWING, - success: false - } - ]); - - try { - const amountToWithdraw = amountAsBInt; - - console.warn( - 'Withdraw params:', - selectedMarketData.cToken, - amountToWithdraw.toString() - ); - let isMax = false; - if (amountToWithdraw === maxWithdrawAmount) { - isMax = true; - } - - const { tx, errorCode } = await currentSdk.withdraw( - selectedMarketData.cToken, - amountToWithdraw, - isMax - ); - - if (errorCode) { - console.error(errorCode); - - throw new Error('Error during withdrawing!'); - } - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - txHash: tx - } - }); - - tx && - (await currentSdk.publicClient.waitForTransactionReceipt({ - hash: tx - })); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - - toast.success( - `Withdrawn ${amount} ${selectedMarketData.underlyingSymbol}` - ); - } catch (error) { - console.error(error); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - error: true - } - }); - - toast.error('Error while withdrawing!'); - } - } - }; - - const borrowAmount = async () => { - if ( - !transactionSteps.length && - currentSdk && - address && - amount && - amountAsBInt > 0n && - minBorrowAmount && - amountAsBInt >= (minBorrowAmount.minBorrowAsset ?? 0n) && - maxBorrowAmount && - amountAsBInt <= maxBorrowAmount.bigNumber - ) { - const currentTransactionStep = 0; - addStepsForAction([ - { - error: false, - message: INFO_MESSAGES.BORROW.BORROWING, - success: false - } - ]); - - try { - const { tx, errorCode } = await currentSdk.borrow( - selectedMarketData.cToken, - amountAsBInt - ); - - if (errorCode) { - console.error(errorCode); - - throw new Error('Error during borrowing!'); - } - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - txHash: tx - } - }); - - tx && - (await currentSdk.publicClient.waitForTransactionReceipt({ - hash: tx - })); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - - toast.success( - `Borrowed ${amount} ${selectedMarketData.underlyingSymbol}` - ); - } catch (error) { - console.error(error); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - error: true - } - }); - - toast.error('Error while borrowing!'); - } - } - }; - - const repayAmount = async () => { - if ( - !transactionSteps.length && - currentSdk && - address && - amount && - amountAsBInt > 0n && - currentBorrowAmountAsFloat - ) { - let currentTransactionStep = 0; - addStepsForAction([ - { - error: false, - message: INFO_MESSAGES.REPAY.APPROVE, - success: false - }, - { - error: false, - message: INFO_MESSAGES.REPAY.REPAYING, - success: false - } - ]); - - try { - const token = currentSdk.getEIP20TokenInstance( - selectedMarketData.underlyingToken, - currentSdk.publicClient as any - ); - const hasApprovedEnough = - (await token.read.allowance([address, selectedMarketData.cToken])) >= - amountAsBInt; - - if (!hasApprovedEnough) { - const tx = await currentSdk.approve( - selectedMarketData.cToken, - selectedMarketData.underlyingToken, - (amountAsBInt * 105n) / 100n - ); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - txHash: tx - } - }); - - await currentSdk.publicClient.waitForTransactionReceipt({ - hash: tx, - confirmations: 2 - }); - - // wait for 5 seconds to resolve timing issue - await new Promise((resolve) => setTimeout(resolve, 5000)); - } - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - - currentTransactionStep++; - - const isRepayingMax = - amountAsBInt >= (selectedMarketData.borrowBalance ?? 0n); - console.warn( - 'Repay params:', - selectedMarketData.cToken, - isRepayingMax, - isRepayingMax - ? selectedMarketData.borrowBalance.toString() - : amountAsBInt.toString() - ); - const { tx, errorCode } = await currentSdk.repay( - selectedMarketData.cToken, - isRepayingMax, - isRepayingMax ? selectedMarketData.borrowBalance : amountAsBInt - ); - - if (errorCode) { - console.error(errorCode); - - throw new Error('Error during repaying!'); - } - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - txHash: tx - } - }); - - tx && - (await currentSdk.publicClient.waitForTransactionReceipt({ - hash: tx - })); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - } catch (error) { - console.error(error); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - error: true - } - }); - - toast.error('Error while repaying!'); - } - } - }; - - const handleCollateralToggle = async () => { - if (!transactionSteps.length) { - if (currentSdk && selectedMarketData.supplyBalance > 0n) { - const currentTransactionStep = 0; - - try { - let tx; - - switch (enableCollateral) { - case true: { - const comptrollerContract = currentSdk.createComptroller( - comptrollerAddress, - currentSdk.publicClient - ); - - const exitCode = ( - await comptrollerContract.simulate.exitMarket( - [selectedMarketData.cToken], - { account: currentSdk.walletClient!.account!.address } - ) - ).result; - - if (exitCode !== 0n) { - toast.error(errorCodeToMessage(Number(exitCode))); - - return; - } - - addStepsForAction([ - { - error: false, - message: INFO_MESSAGES.COLLATERAL.DISABLE, - success: false - } - ]); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - error: false, - message: INFO_MESSAGES.COLLATERAL.DISABLE, - success: false - } - }); - - tx = await comptrollerContract.write.exitMarket( - [selectedMarketData.cToken], - { - account: currentSdk.walletClient!.account!.address, - chain: currentSdk.publicClient.chain - } - ); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - txHash: tx - } - }); - - await currentSdk.publicClient.waitForTransactionReceipt({ - hash: tx - }); - - setEnableCollateral(false); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - - break; - } - - case false: { - addStepsForAction([ - { - error: false, - message: INFO_MESSAGES.COLLATERAL.ENABLE, - success: false - } - ]); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - error: false, - message: INFO_MESSAGES.COLLATERAL.ENABLE, - success: false - } - }); - - tx = await currentSdk.enterMarkets( - selectedMarketData.cToken, - comptrollerAddress - ); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - txHash: tx - } - }); - - await currentSdk.publicClient.waitForTransactionReceipt({ - hash: tx - }); - - setEnableCollateral(true); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - - break; - } - } - - refetchUsedQueries(); - - return; - } catch (error) { - console.error(error); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - error: true - } - }); - } - } - - setEnableCollateral(!enableCollateral); - } - }; - - const normalizedHealthFactor = useMemo(() => { - return healthFactor - ? healthFactor === '-1' - ? '∞' - : Number(healthFactor).toFixed(2) - : undefined; - }, [healthFactor]); - - const normalizedPredictedHealthFactor = useMemo(() => { - return predictedHealthFactor === maxUint256 - ? '∞' - : Number(formatEther(predictedHealthFactor ?? 0n)).toFixed(2); - }, [predictedHealthFactor]); - return ( - <> -
- setSwapWidgetOpen(false)} - open={swapWidgetOpen} - fromChain={chainId} - toChain={chainId} - toToken={selectedMarketData.underlyingToken} - onRouteExecutionCompleted={() => refetchMaxSupplyAmount()} - /> -
- close -
- modlogo -
- 0 - : false - } - mode={mode} - setActive={setActive} - borrowPossible={borrowCap ? borrowCap?.totalBorrowCap > 1n : true} - /> - {/* all the respective slides */} - -
- {(mode === PopupMode.SUPPLY || mode === PopupMode.WITHDRAW) && ( - <> - {/* ---------------------------------------------------------------------------- */} - {/* SUPPLY-Collateral section */} - {/* ---------------------------------------------------------------------------- */} -
- - {/*
*/} - {/*
*/} - setAmount(val)} - isLoading={isLoadingMaxSupply} - max={formatUnits( - maxSupplyAmount?.bigNumber ?? 0n, - selectedMarketData.underlyingDecimals - )} - selectedMarketData={selectedMarketData} - symbol={selectedMarketData.underlyingSymbol} - /> - - -
-
- COLLATERAL APR - - {collateralApr} - {/* to do: add the rewards to the calculation */} - -
-
-
- Market Supply Balance - - {supplyBalanceFrom} - {`->`} - - {supplyBalanceTo} - - {/* this will be dynamic */} - -
-
- Market Supply APR - - {`${supplyAPY?.toFixed(2)}%`} - {`->`} - - {updatedSupplyAPY?.toFixed(2)}% - - -
- {/*
- Health Factor - - {`${Number(healthFactor).toFixed(2)}`} - {`->`} - - {Number( - formatEther(predictedHealthFactor ?? 0n) - ).toFixed(2)} - - -
*/} -
- -
- -
- -
- -
-
Total Supplied:
- -
- - {millify(Math.round(totalSupplyAsNumber))} of{' '} - {millify(Math.round(supplyCapAsNumber))}{' '} - {selectedMarketData.underlyingSymbol} - -
- -
- $ - {millify( - Math.round(selectedMarketData.totalSupplyFiat) - )}{' '} - of ${millify(Math.round(supplyCapAsFiat))} -
-
-
-
- -
-
- Enable collateral -
- -
-
-
- {transactionSteps.length > 0 ? ( - - ) : ( - <> - - - )} -
- {/* */} -
-
- {/* ---------------------------------------------------------------------------- */} - {/* SUPPLY-Withdraw section */} - {/* ---------------------------------------------------------------------------- */} - setAmount(val)} - hintText="Max Withdraw" - isLoading={isLoadingMaxWithdrawAmount} - max={formatUnits( - maxWithdrawAmount ?? 0n, - selectedMarketData.underlyingDecimals - )} - selectedMarketData={selectedMarketData} - symbol={selectedMarketData.underlyingSymbol} - /> - - - {hfpStatus === HFPStatus.WARNING && ( -
- Warning: You are close to the liquidation threshold and - will need to manage your health factor. -
- )} - - {hfpStatus === HFPStatus.CRITICAL && ( -
- Health factor too low. -
- )} - -
- -
- Market Supply Balance - - {supplyBalanceFrom} - {`->`} - - {supplyBalanceTo} - - {/* this will be dynamic */} - -
-
- Market Supply APR - - {`${supplyAPY?.toFixed(2)}%`} - {`->`} - - {updatedSupplyAPY?.toFixed(2)}% - - -
-
- Health Factor - - {`${normalizedHealthFactor}`} - {`->`} - - {normalizedPredictedHealthFactor} - - -
-
- {transactionSteps.length > 0 ? ( - - ) : ( - - )} -
- {/* */} -
- - )} - {(mode === PopupMode.BORROW || mode === PopupMode.REPAY) && ( - <> -
- {/* ---------------------------------------------------------------------------- */} - {/* SUPPLY-borrow section */} - {/* ---------------------------------------------------------------------------- */} - setAmount(val)} - hintText="Max Borrow Amount" - isLoading={isLoadingMaxBorrowAmount} - max={formatUnits( - maxBorrowAmount?.bigNumber ?? 0n, - selectedMarketData.underlyingDecimals - )} - selectedMarketData={selectedMarketData} - symbol={selectedMarketData.underlyingSymbol} - /> - - - {hfpStatus === HFPStatus.WARNING && ( -
- Warning: You are close to the liquidation threshold and - will need to manage your health factor. -
- )} - - {hfpStatus === HFPStatus.CRITICAL && ( -
- Health factor too low. -
- )} - -
-
- MIN BORROW - - {formatUnits( - minBorrowAmount?.minBorrowAsset ?? 0n, - selectedMarketData.underlyingDecimals - )} - {/* this will be dynamic */} - -
-
- MAX BORROW - - {maxBorrowAmount?.number?.toFixed( - parseInt( - selectedMarketData.underlyingDecimals.toString() - ) - ) ?? '0.00'} - {/* this will be dynamic */} - -
-
- CURRENTLY BORROWING - - {`${borrowBalanceFrom}`} - {`->`} - - {borrowBalanceTo} - - -
-
- Market Borrow Apr - - {`${borrowAPR?.toFixed(2)}%`} - {`->`} - - {updatedBorrowAPR?.toFixed(2)}% - - -
-
- Health Factor - - {`${normalizedHealthFactor}`} - {`->`} - - {normalizedPredictedHealthFactor} - - -
-
- -
- -
- -
- -
-
Total Borrowed:
- -
- - {millify(Math.round(totalBorrowAsNumber))} of{' '} - {millify(Math.round(borrowCapAsNumber))}{' '} - {selectedMarketData.underlyingSymbol} - -
- -
- $ - {millify( - Math.round(selectedMarketData.totalBorrowFiat) - )}{' '} - of ${millify(Math.round(borrowCapAsFiat))} -
-
-
-
- -
-
- {transactionSteps.length > 0 ? ( - - ) : ( - - )} -
-
-
- {/* ---------------------------------------------------------------------------- */} - {/* SUPPLY-repay section */} - {/* ---------------------------------------------------------------------------- */} - setAmount(val)} - hintText={'Max Repay Amount'} - isLoading={isLoadingMaxRepayAmount} - max={formatUnits( - maxRepayAmount ?? 0n, - selectedMarketData.underlyingDecimals - )} - selectedMarketData={selectedMarketData} - symbol={selectedMarketData.underlyingSymbol} - /> - -
-
- CURRENTLY BORROWING - - - {`${borrowBalanceFrom}`} - - {`->`} - - {borrowBalanceTo} - - -
-
- Market Borrow Apr - - {`${borrowAPR?.toFixed(2)}%`} - {`->`} - - {updatedBorrowAPR?.toFixed(2)}% - - -
-
- Health Factor - - {`${normalizedHealthFactor}`} - {`->`} - - {normalizedPredictedHealthFactor} - - -
-
-
- {transactionSteps.length > 0 ? ( - - ) : ( - - )} -
-
- - )} -
-
-
- - { - setLoopOpen(false); - setActive(PopupMode.BORROW); - }} - comptrollerAddress={comptrollerAddress} - isOpen={loopOpen} - selectedCollateralAsset={selectedMarketData} - /> - - ); -}; - -export default Popup; - -/*mode should be of -supply consist of collateral , withdraw - borrow ( borrow repay) -manage collateral withdraw borrow repay - default -*/ - -/*

-

colleteralT , borrowingT , lendingT , cAPR , lAPR , bAPR} */ diff --git a/packages/ui/app/_components/stake/BaseBreakdown.tsx b/packages/ui/app/_components/stake/BaseBreakdown.tsx deleted file mode 100644 index 6c2a93dde..000000000 --- a/packages/ui/app/_components/stake/BaseBreakdown.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import Image from 'next/image'; - -import { base } from 'viem/chains'; - -import { BaseSugarAddress } from '@ui/constants/baselp'; -import useSugarAPR from '@ui/hooks/useSugarAPR'; - -type BaseBreakdownProps = { - step3Toggle: string; -}; - -const ION_POOL_INDEX = 1489n; - -export default function BaseBreakdown({ step3Toggle }: BaseBreakdownProps) { - const { apr } = useSugarAPR({ - sugarAddress: BaseSugarAddress, - poolIndex: ION_POOL_INDEX, - chainId: base.id - }); - - return ( -
- AERO logo - Aerodrome APR - - {apr} - -
- ); -} diff --git a/packages/ui/app/_components/stake/ModeBreakdown.tsx b/packages/ui/app/_components/stake/ModeBreakdown.tsx deleted file mode 100644 index e8b70a12d..000000000 --- a/packages/ui/app/_components/stake/ModeBreakdown.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import Image from 'next/image'; - -import { mode } from 'viem/chains'; - -import { ModeSugarAddress } from '@ui/constants/lp'; -import useSugarAPR from '@ui/hooks/useSugarAPR'; - -type ModeBreakdownProps = { - step3Toggle: string; - selectedToken: 'eth' | 'mode' | 'weth'; -}; - -export default function ModeBreakdown({ - step3Toggle, - selectedToken -}: ModeBreakdownProps) { - const ION_WETH_POOL_INDEX = 6n; - const ION_MODE_POOL_INDEX = 26n; - - const { apr: apy } = useSugarAPR({ - sugarAddress: ModeSugarAddress, - poolIndex: - selectedToken === 'mode' ? ION_MODE_POOL_INDEX : ION_WETH_POOL_INDEX, - chainId: mode.id, - selectedToken, - isMode: true - }); - - return ( -
- VELO logo - Velodrome APR - - {apy} - -
- ); -} diff --git a/packages/ui/app/_components/stake/OPBreakdown.tsx b/packages/ui/app/_components/stake/OPBreakdown.tsx deleted file mode 100644 index 5be4668e2..000000000 --- a/packages/ui/app/_components/stake/OPBreakdown.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import Image from 'next/image'; - -import { optimism } from 'viem/chains'; - -import { OPSugarAddress } from '@ui/constants/oplp'; -import useSugarAPR from '@ui/hooks/useSugarAPR'; - -type OPBreakdownProps = { - step3Toggle: string; -}; - -const POOL_INDEX = 910n; - -export default function OPBreakdown({ step3Toggle }: OPBreakdownProps) { - const { apr } = useSugarAPR({ - sugarAddress: OPSugarAddress, - poolIndex: POOL_INDEX, - chainId: optimism.id - }); - - return ( -
- VELO logo - Velodrome APR - - {apr} - -
- ); -} diff --git a/packages/ui/app/_components/stake/RewardDisplay.tsx b/packages/ui/app/_components/stake/RewardDisplay.tsx index cdb3e55f7..cb4b084a2 100644 --- a/packages/ui/app/_components/stake/RewardDisplay.tsx +++ b/packages/ui/app/_components/stake/RewardDisplay.tsx @@ -84,12 +84,14 @@ export default function RewardDisplay({ const config = chainConfig.tokenConfigs?.[selectedToken] || chainConfig.defaultConfig; + // Always call the hook at the top level const { apr } = useSugarAPR({ sugarAddress: config.sugarAddress, poolIndex: config.poolIndex, chainId, selectedToken, isMode: chainId === mode.id + // Add enabled flag to control when the hook should actually fetch data }); if (!chainConfig) return null; diff --git a/packages/ui/app/dashboard/page.tsx b/packages/ui/app/dashboard/page.tsx index 9cc55bccd..a392100bb 100644 --- a/packages/ui/app/dashboard/page.tsx +++ b/packages/ui/app/dashboard/page.tsx @@ -33,12 +33,12 @@ import ClaimRewardPopover from '../_components/dashboards/ClaimRewardPopover'; import CollateralSwapPopup from '../_components/dashboards/CollateralSwapPopup'; import InfoRows, { InfoMode } from '../_components/dashboards/InfoRows'; import LoopRewards from '../_components/dashboards/LoopRewards'; +import Loop from '../_components/dialogs/loop'; +import ManageDialog from '../_components/dialogs/manage'; import NetworkSelector from '../_components/markets/NetworkSelector'; -import Loop from '../_components/popup/Loop'; -import Popup from '../_components/popup/page'; import ResultHandler from '../_components/ResultHandler'; -import type { PopupMode } from '../_components/popup/page'; +import type { ActiveTab } from '../_components/dialogs/manage'; import type { FlywheelReward, @@ -58,7 +58,8 @@ export default function Dashboard() { const chain = querychain ? querychain : 34443; const pool = querypool ? querypool : '0'; const [selectedSymbol, setSelectedSymbol] = useState('WETH'); - const [popupMode, setPopupMode] = useState(); + const [activeTab, setActiveTab] = useState(); + const [isManageDialogOpen, setIsManageDialogOpen] = useState(false); const [collateralSwapFromAsset, setCollateralSwapFromAsset] = useState(); const walletChain = useChainId(); @@ -173,7 +174,7 @@ export default function Dashboard() { ); const [selectedLoopBorrowData, setSelectedLoopBorrowData] = useState(); - const [loopOpen, setLoopOpen] = useState(false); + const [isLoopDialogOpen, setIsLoopDialogOpen] = useState(false); const { data: healthData, isLoading: isLoadingHealthData } = useHealthFactor( marketData?.comptroller, +chain @@ -238,7 +239,6 @@ export default function Dashboard() { toggle: swapToggle } = useOutsideClick(); - // console.log(suppliedAssets); return ( <> {swapOpen && marketData?.comptroller && ( @@ -554,7 +554,8 @@ export default function Dashboard() { })) as FlywheelReward[]) ?? [] } selectedChain={+chain} - setPopupMode={setPopupMode} + setActiveTab={setActiveTab} + setIsManageDialogOpen={setIsManageDialogOpen} setSelectedSymbol={setSelectedSymbol} // utilization={utilizations[i]} toggler={async () => { @@ -656,7 +657,8 @@ export default function Dashboard() { membership={asset.membership} mode={InfoMode.BORROW} selectedChain={+chain} - setPopupMode={setPopupMode} + setIsManageDialogOpen={setIsManageDialogOpen} + setActiveTab={setActiveTab} setSelectedSymbol={setSelectedSymbol} // utilization={utilizations[i]} utilization="0.00%" @@ -718,7 +720,7 @@ export default function Dashboard() { usdPrice={usdPrice ?? undefined} setSelectedLoopBorrowData={setSelectedLoopBorrowData} setSelectedSymbol={setSelectedSymbol} - setLoopOpen={setLoopOpen} + setLoopOpen={setIsLoopDialogOpen} chain={+chain} /> ); @@ -737,21 +739,20 @@ export default function Dashboard() { {selectedMarketData && ( { - setLoopOpen(false); - }} + isOpen={isLoopDialogOpen} + setIsOpen={setIsLoopDialogOpen} comptrollerAddress={marketData?.comptroller ?? ('' as Address)} currentBorrowAsset={selectedLoopBorrowData} - isOpen={loopOpen} selectedCollateralAsset={selectedMarketData} /> )} - {popupMode && selectedMarketData && marketData && ( - setPopupMode(undefined)} + {selectedMarketData && marketData && ( + )} diff --git a/packages/ui/app/globals.css b/packages/ui/app/globals.css index 781608232..6a26d593c 100644 --- a/packages/ui/app/globals.css +++ b/packages/ui/app/globals.css @@ -8,6 +8,13 @@ --background-start-rgb: 214, 219, 220; --background-end-rgb: 255, 255, 255; --gasbot-primary: #38fe89; + --border-mode-color: #dffd04; + --border-lime-color: #dffe00; + --border-base-color: rgb(37 99 235); + --border-optimism-color: #df1515; + --border-bob-color: #e56016; + --border-fraxtal-color: #504F57; + --border-lisk-color: #4071f4; } .popover-hint { diff --git a/packages/ui/app/layout.tsx b/packages/ui/app/layout.tsx index a336db2d3..7d4e1742d 100644 --- a/packages/ui/app/layout.tsx +++ b/packages/ui/app/layout.tsx @@ -22,6 +22,7 @@ import { AppProgressBar as ProgressBar } from 'next-nprogress-bar'; import { Toaster } from 'react-hot-toast'; import { WagmiProvider } from 'wagmi'; +import { TooltipProvider } from '@ui/components/ui/tooltip'; import { MultiIonicProvider } from '@ui/context/MultiIonicContext'; import Navbar from './_components/Navbar'; @@ -104,220 +105,222 @@ export default function RootLayout({ - }> - - -
- -
{children}
-
+
-
- logo +
+

Resources

+ + +
+ +
+

Tools

+ + +
+ +
+ logo +
-
- + - -
- + }} + /> +
+ + diff --git a/packages/ui/app/market/details/[asset]/page.tsx b/packages/ui/app/market/details/[asset]/page.tsx index 36e9c5fb8..2f5b64a70 100644 --- a/packages/ui/app/market/details/[asset]/page.tsx +++ b/packages/ui/app/market/details/[asset]/page.tsx @@ -38,8 +38,6 @@ ChartJS.register( //-------------------------components----------- import { - chartoptions, - getChartData, donutoptions, getDonutData, chartoptions2 @@ -53,9 +51,9 @@ import { // ]; // const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042']; import { INFO } from '@ui/constants/index'; -import Popup, { PopupMode } from '@ui/app/_components/popup/page'; + +import Swap from '@ui/app/_components/dialogs/manage/Swap'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; -import Swap from '@ui/app/_components/popup/Swap'; import { MarketData } from '@ui/types/TokensDataMap'; import { useFusePoolData } from '@ui/hooks/useFusePoolData'; import { useLoopMarkets } from '@ui/hooks/useLoopMarkets'; @@ -65,6 +63,7 @@ import { useBorrowCapsDataForAsset } from '@ui/hooks/fuse/useBorrowCapsDataForAs import { useUsdPrice } from '@ui/hooks/useAllUsdPrices'; import { useSupplyCapsDataForAsset } from '@ui/hooks/fuse/useSupplyCapsDataForPool'; import BorrowAmount from '@ui/app/_components/markets/BorrowAmount'; +import ManageDialog from '@ui/app/_components/dialogs/manage'; import { useAssetChartData } from '@ui/hooks/useAssetChartData'; import ChartWithDateRange from '@ui/app/_components/markets/ChartWithDateRange'; import ResultHandler from '@ui/app/_components/ResultHandler'; @@ -76,6 +75,7 @@ interface IGraph { supplyAtY: number[]; valAtX: string[]; } +type ActiveTab = 'borrow' | 'repay' | 'supply' | 'withdraw'; const supabase = createClient( 'https://uoagtjstsdrjypxlkuzr.supabase.co/', @@ -90,6 +90,8 @@ const Asset = () => { }); const [info, setInfo] = useState(INFO.BORROW); const searchParams = useSearchParams(); + const [isManageDialogOpen, setIsManageDialogOpen] = useState(false); + const [activeTab, setActiveTab] = useState(); //URL passed Data ---------------------------- const dropdownSelectedChain = searchParams.get('dropdownSelectedChain'); @@ -103,7 +105,6 @@ const Asset = () => { const availableAPR = searchParams.get('supplyAPR'); //-------------------------------------------------------- - const [popupMode, setPopupMode] = useState(); const [swapOpen, setSwapOpen] = useState(false); // const [selectedPool, setSelectedPool] = useState(pool ? pool : pools[0].id); const [selectedMarketData, setSelectedMarketData] = useState< @@ -528,7 +529,8 @@ const Asset = () => { Number(selectedChain) ); if (result) { - setPopupMode(PopupMode.SUPPLY); + setIsManageDialogOpen(true); + setActiveTab('supply'); } }} > @@ -570,7 +572,8 @@ const Asset = () => { Number(selectedChain) ); if (result) { - setPopupMode(PopupMode.BORROW); + setIsManageDialogOpen(true); + setActiveTab('borrow'); } }} > @@ -660,13 +663,13 @@ const Asset = () => {
- {popupMode && selectedMarketData && poolData && ( - setPopupMode(undefined)} + {selectedMarketData && poolData && ( + )} diff --git a/packages/ui/app/market/page.tsx b/packages/ui/app/market/page.tsx index c94c72cc6..d97835746 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -1,146 +1,261 @@ -/* eslint-disable @next/next/no-img-element */ 'use client'; -// import { Listbox, Transition } from '@headlessui/react'; -// import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'; - -// import Link from 'next/link'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import dynamic from 'next/dynamic'; +import Image from 'next/image'; +import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; -import { type Address, formatEther, formatUnits } from 'viem'; import { mode } from 'viem/chains'; import { useChainId } from 'wagmi'; -// import Dropdown from '../_components/Dropdown'; -// import NetworkSelector from '../_components/markets/NetworkSelector'; -const PoolToggle = dynamic(() => import('../_components/markets/PoolToggle'), { - ssr: false -}); - -import { pools } from '@ui/constants/index'; -// import { useAllTvlAcrossChain } from '@ui/hooks/useAllTvlAcrossChain'; -import { useBorrowAPYs } from '@ui/hooks/useBorrowAPYs'; -import { useFusePoolData } from '@ui/hooks/useFusePoolData'; -import { useLoopMarkets } from '@ui/hooks/useLoopMarkets'; -import { useRewards } from '@ui/hooks/useRewards'; -import { useSupplyAPYs } from '@ui/hooks/useSupplyAPYs'; -import type { MarketData } from '@ui/types/TokensDataMap'; +import { pools } from '@ui/constants'; +import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import type { MarketRowData } from '@ui/hooks/market/useMarketData'; +import { useMarketData } from '@ui/hooks/market/useMarketData'; +import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; +import CommonTable from '../_components/CommonTable'; +import Loop from '../_components/dialogs/loop'; +import ManageDialog from '../_components/dialogs/manage'; +import Swap from '../_components/dialogs/manage/Swap'; +import APRCell from '../_components/markets/APRCell'; import FeaturedMarketTile from '../_components/markets/FeaturedMarketTile'; -import PoolRows from '../_components/markets/PoolRows'; +import FilterBar from '../_components/markets/FilterBar'; import StakingTile from '../_components/markets/StakingTile'; import TotalTvlTile from '../_components/markets/TotalTvlTile'; import TvlTile from '../_components/markets/TvlTile'; -import Popup from '../_components/popup/page'; -import Swap from '../_components/popup/Swap'; -import ResultHandler from '../_components/ResultHandler'; -import type { PopupMode } from '../_components/popup/page'; - -import { type FlywheelReward } from '@ionicprotocol/types'; -// import SwapWidget from '../_components/markets/SwapWidget'; +import type { EnhancedColumnDef } from '../_components/CommonTable'; +import type { Row } from '@tanstack/react-table'; const NetworkSelector = dynamic( () => import('../_components/markets/NetworkSelector'), { ssr: false } ); +interface MarketCellProps { + row: Row; + getValue: () => any; +} + export default function Market() { const searchParams = useSearchParams(); + const chainId = useChainId(); + const { address } = useMultiIonic(); + const querychain = searchParams.get('chain'); const querypool = searchParams.get('pool'); + const selectedPool = querypool ?? '0'; + const chain = querychain ? querychain : mode.id.toString(); + const [swapOpen, setSwapOpen] = useState(false); const [swapWidgetOpen, setSwapWidgetOpen] = useState(false); const [wrapWidgetOpen, setWrapWidgetOpen] = useState(false); - const [dropdownSelectedChain, setDropdownSelectedChain] = useState( - mode.id - ); - - const [popupMode, setPopupMode] = useState(); - const chainId = useChainId(); - - const selectedPool = querypool ?? '0'; - const chain = querychain ? querychain : mode.id; - const { data: poolData, isLoading: isLoadingPoolData } = useFusePoolData( - selectedPool, - +chain + const [isManageDialogOpen, setIsManageDialogOpen] = useState(false); + const [isLoopDialogOpen, setIsLoopDialogOpen] = useState(false); + const [selectedSymbol, setSelectedSymbol] = useState(); + const [isBorrowDisabled, setIsBorrowDisabled] = useState(false); + const [filteredMarketData, setFilteredMarketData] = useState( + [] ); + const { marketData, isLoading, poolData, selectedMarketData, loopProps } = + useMarketData(selectedPool, chain, selectedSymbol); useEffect(() => { - if (!chain) return; - setDropdownSelectedChain(+chain); - }, [chain]); - - const assets = useMemo( - () => poolData?.assets, - [poolData] - ); - - const { data: borrowRates } = useBorrowAPYs( - assets ?? [], - dropdownSelectedChain - ); - - const { data: supplyRates } = useSupplyAPYs( - assets ?? [], - dropdownSelectedChain - ); + setFilteredMarketData(marketData); + }, [marketData]); - const [selectedSymbol, setSelectedSymbol] = useState(); - const selectedMarketData = useMemo( - () => - poolData?.assets.find( - (_asset) => _asset.underlyingSymbol === selectedSymbol - ), - [selectedSymbol, poolData] - ); - const { data: loopMarkets, isLoading: isLoadingLoopMarkets } = useLoopMarkets( - poolData?.assets.map((asset) => asset.cToken) ?? [], - +chain - ); - - const { data: rewards } = useRewards({ - chainId: dropdownSelectedChain, - poolId: selectedPool - }); + const columns: EnhancedColumnDef[] = [ + { + id: 'asset', + header:
ASSETS
, + sortingFn: 'alphabetical', + cell: ({ row }: MarketCellProps) => ( + + {row.original.asset} +
+ {row.original.asset} +
+ + Supplied: ${row.original.supply.totalUSD.split(' ')[0]} + + + Borrowed: ${row.original.borrow.totalUSD.split(' ')[0]} + +
+
+ + ) + }, + { + id: 'supplyAPRTotal', + header: 'SUPPLY APR', + sortingFn: 'numerical', + accessorFn: (row) => row.supplyAPR, + cell: ({ row }: MarketCellProps) => ( + + ) + }, + { + id: 'borrowAPRTotal', + header: 'BORROW APR', + sortingFn: 'numerical', + accessorFn: (row) => row.borrowAPR, + cell: ({ row }: MarketCellProps) => ( + + ) + }, + { + id: 'supplyBalance', + header: 'SUPPLY BALANCE', + sortingFn: 'numerical', + cell: ({ row }: MarketCellProps) => ( +
+ {row.original.supply.balance} + + ${row.original.supply.balanceUSD} + +
+ ) + }, + { + id: 'borrowBalance', + header: 'BORROW BALANCE', + sortingFn: 'numerical', + cell: ({ row }: MarketCellProps) => ( +
+ {row.original.borrow.balance} + + ${row.original.borrow.balanceUSD} + +
+ ) + }, + { + id: 'collateralFactor', + header: 'COLLATERAL FACTOR', + sortingFn: 'percentage', + cell: ({ row }: MarketCellProps) => ( + {row.original.collateralFactor}% + ) + }, + { + id: 'actions', + header: 'ACTIONS', + enableSorting: false, + cell: ({ row }: MarketCellProps) => ( +
+ + {row.original.loopPossible && ( + + )} +
+ ) + } + ]; - // const { data: alltvl } = useAllTvlAcrossChain(); - // console.log(alltvl); return ( <> -
- {/* //........ */} -
-
+
+
+
- {/* //............................................ */} -
+ +
-
-
- -
- - - <> - {assets && - pools[dropdownSelectedChain].pools[+selectedPool].assets.map( - (symbol: string, idx: number) => { - const val = assets.find( - (asset) => asset.underlyingSymbol === symbol - ); - if (!val) return <>; - return ( - 0 - : false - } - membership={val?.membership ?? false} - pool={selectedPool} - rewards={ - (rewards?.[val?.cToken]?.map((r) => ({ - ...r, - apy: - typeof r.apy !== 'undefined' - ? r.apy * 100 - : undefined - })) as FlywheelReward[]) ?? [] - } - selectedChain={chainId} - selectedMarketData={selectedMarketData} - selectedPoolId={selectedPool} - selectedSymbol={selectedSymbol as string} - setPopupMode={setPopupMode} - setSelectedSymbol={setSelectedSymbol} - supplyAPR={ - typeof supplyRates?.[val.cToken] !== 'undefined' - ? supplyRates?.[val.cToken] * 100 - : undefined - } - supplyBalance={`${ - typeof val.supplyBalance === 'bigint' - ? parseFloat( - formatUnits( - val.supplyBalance, - val.underlyingDecimals - ) - ).toLocaleString('en-US', { - maximumFractionDigits: 2 - }) - : '-' - } ${val.underlyingSymbol} / $${val.supplyBalanceFiat.toLocaleString( - 'en-US', - { - maximumFractionDigits: 2 - } - )}`} - totalBorrowing={`${ - val.totalBorrowNative - ? parseFloat( - formatUnits( - val.totalBorrow, - val.underlyingDecimals - ) - ).toLocaleString('en-US', { - maximumFractionDigits: 2 - }) - : '0' - } ${val.underlyingSymbol} / $${val.totalBorrowFiat.toLocaleString( - 'en-US', - { - maximumFractionDigits: 2 - } - )}`} - totalSupplied={`${ - val.totalSupplyNative - ? parseFloat( - formatUnits( - val.totalSupply, - val.underlyingDecimals - ) - ).toLocaleString('en-US', { - maximumFractionDigits: 2 - }) - : '0' - } ${val.underlyingSymbol} / $${val.totalSupplyFiat.toLocaleString( - 'en-US', - { - maximumFractionDigits: 2 - } - )}`} - /> - ); - } - )} - - +
+ + + ({ + badge: row.original.membership + ? { text: 'Collateral' } + : undefined, + borderClassName: row.original.membership + ? pools[+chain]?.border + : undefined + })} + />
- {popupMode && selectedMarketData && poolData && ( - setPopupMode(undefined)} + + {selectedMarketData && poolData && ( + )} + {loopProps && ( + + )} + {swapOpen && ( setSwapOpen(false)} - dropdownSelectedChain={dropdownSelectedChain} + dropdownSelectedChain={+chain} selectedChain={chainId} /> )} diff --git a/packages/ui/app/stake/page.tsx b/packages/ui/app/stake/page.tsx index ea7b3cc90..680b4cf7e 100644 --- a/packages/ui/app/stake/page.tsx +++ b/packages/ui/app/stake/page.tsx @@ -38,8 +38,8 @@ import { } from '@ui/utils/getStakingTokens'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; +import SliderComponent from '../_components/dialogs/manage/Slider'; import MaxDeposit from '../_components/MaxDeposit'; -import SliderComponent from '../_components/popup/Slider'; import ResultHandler from '../_components/ResultHandler'; import ClaimRewards from '../_components/stake/ClaimRewards'; import RewardDisplay from '../_components/stake/RewardDisplay'; diff --git a/packages/ui/components/ui/alert.tsx b/packages/ui/components/ui/alert.tsx new file mode 100644 index 000000000..76bd2db23 --- /dev/null +++ b/packages/ui/components/ui/alert.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; + +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@ui/lib/utils'; + +const alertVariants = cva( + 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', + { + variants: { + variant: { + default: 'bg-background text-foreground', + destructive: + 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive' + } + }, + defaultVariants: { + variant: 'default' + } + } +); + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)); +Alert.displayName = 'Alert'; + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = 'AlertTitle'; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = 'AlertDescription'; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/packages/ui/components/ui/dialog.tsx b/packages/ui/components/ui/dialog.tsx index 7f05f0333..856dc77f4 100644 --- a/packages/ui/components/ui/dialog.tsx +++ b/packages/ui/components/ui/dialog.tsx @@ -5,6 +5,106 @@ import { X } from 'lucide-react'; import { cn } from '@ui/lib/utils'; +const dialogSizeVariants = { + sm: 'max-w-sm', + md: 'max-w-md', + lg: 'max-w-lg', + xl: 'max-w-xl', + '2xl': 'max-w-2xl', + '3xl': 'max-w-3xl', + '4xl': 'max-w-4xl', + '5xl': 'max-w-5xl', + full: 'max-w-full', + fitContent: 'max-w-fit', + percentage: { + 80: 'w-[80%]', + 90: 'w-[90%]', + 95: 'w-[95%]' + } +} as const; + +type DialogSize = + | keyof typeof dialogSizeVariants + | keyof typeof dialogSizeVariants.percentage; + +interface DialogContentProps + extends React.ComponentPropsWithoutRef { + hideCloseButton?: boolean; + size?: DialogSize; + minWidth?: string; + maxWidth?: string; + fullWidth?: boolean; +} + +const DialogContent = React.forwardRef< + React.ElementRef, + DialogContentProps +>( + ( + { + className, + children, + hideCloseButton = false, + size = '2xl', + minWidth, + maxWidth, + fullWidth = false, + style, + ...props + }, + ref + ) => { + // Build the style object with min and max width + const combinedStyle = React.useMemo( + () => ({ + ...style, + ...(minWidth && { minWidth }), + ...(maxWidth && { maxWidth }), + width: fullWidth ? '100%' : undefined + }), + [style, minWidth, maxWidth, fullWidth] + ); + + // Get the base size class + const getSizeClass = () => { + if (size in dialogSizeVariants) { + return dialogSizeVariants[size as keyof typeof dialogSizeVariants]; + } + if (size in dialogSizeVariants.percentage) { + return dialogSizeVariants.percentage[ + size as keyof typeof dialogSizeVariants.percentage + ]; + } + return dialogSizeVariants['2xl']; + }; + + return ( + + + + {children} + {!hideCloseButton && ( + + + Close + + )} + + + ); + } +); +DialogContent.displayName = DialogPrimitive.Content.displayName; + const Dialog = DialogPrimitive.Root; const DialogTrigger = DialogPrimitive.Trigger; const DialogPortal = DialogPrimitive.Portal; @@ -24,38 +124,7 @@ const DialogOverlay = React.forwardRef< /> )); DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; - -interface DialogContentProps - extends React.ComponentPropsWithoutRef { - hideCloseButton?: boolean; -} - -const DialogContent = React.forwardRef< - React.ElementRef, - DialogContentProps ->(({ className, children, hideCloseButton = false, ...props }, ref) => ( - - - - {children} - {!hideCloseButton && ( - - - Close - - )} - - -)); -DialogContent.displayName = DialogPrimitive.Content.displayName; - +// Rest of the components remain unchanged const DialogHeader = ({ className, ...props @@ -85,7 +154,6 @@ const DialogTitle = React.forwardRef< )); DialogTitle.displayName = DialogPrimitive.Title.displayName; -// Rest of the components remain unchanged const DialogFooter = ({ className, ...props @@ -113,6 +181,7 @@ const DialogDescription = React.forwardRef< DialogDescription.displayName = DialogPrimitive.Description.displayName; export { + type DialogSize, Dialog, DialogPortal, DialogOverlay, diff --git a/packages/ui/components/ui/hover-card.tsx b/packages/ui/components/ui/hover-card.tsx new file mode 100644 index 000000000..a9ea05040 --- /dev/null +++ b/packages/ui/components/ui/hover-card.tsx @@ -0,0 +1,40 @@ +'use client'; + +import * as React from 'react'; + +import * as HoverCardPrimitive from '@radix-ui/react-hover-card'; + +import { cn } from '@ui/lib/utils'; + +const HoverCard = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ openDelay = 0, closeDelay = 0, ...props }, ref) => ( + +)); +HoverCard.displayName = HoverCardPrimitive.Root.displayName; + +const HoverCardTrigger = HoverCardPrimitive.Trigger; + +const HoverCardContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( + +)); +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; + +export { HoverCard, HoverCardTrigger, HoverCardContent }; diff --git a/packages/ui/components/ui/slider.tsx b/packages/ui/components/ui/slider.tsx index 46f7c3cc7..89b291432 100644 --- a/packages/ui/components/ui/slider.tsx +++ b/packages/ui/components/ui/slider.tsx @@ -6,24 +6,88 @@ import * as SliderPrimitive from '@radix-ui/react-slider'; import { cn } from '@ui/lib/utils'; +type Mark = { + value: number; + label: string; + isDisabled?: boolean; +}; + +interface SliderProps + extends React.ComponentPropsWithoutRef { + marks?: Mark[]; + onMarkClick?: (value: number) => void; + currentPosition?: number; +} + const Slider = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - - -)); + SliderProps +>( + ( + { className, value, marks, onMarkClick, currentPosition, ...props }, + ref + ) => { + const percentage = value ? value[0] : 0; + const getColor = () => (percentage <= 50 ? 'bg-accent' : 'bg-lime'); + + return ( +
+ {marks && ( +
+ {marks.map((mark) => { + const position = + ((mark.value - (props.min || 0)) / + ((props.max || 100) - (props.min || 0))) * + 100; + return ( + { + if (!mark.isDisabled && onMarkClick) { + onMarkClick(mark.value); + } + }} + > + {mark.label} + + ); + })} +
+ )} + + + + + + +
+ ); + } +); Slider.displayName = SliderPrimitive.Root.displayName; export { Slider }; diff --git a/packages/ui/components/ui/switch.tsx b/packages/ui/components/ui/switch.tsx index c1450f98f..86e4fccb8 100644 --- a/packages/ui/components/ui/switch.tsx +++ b/packages/ui/components/ui/switch.tsx @@ -12,7 +12,7 @@ const Switch = React.forwardRef< >(({ className, ...props }, ref) => ( ->(({ className, ...props }, ref) => { - const { compact } = React.useContext(TableContext); - - return ( - - ); -}); +>(({ className, ...props }, ref) => ( + +)); TableBody.displayName = 'TableBody'; const TableFooter = React.forwardRef< @@ -68,29 +60,58 @@ const TableFooter = React.forwardRef< )); TableFooter.displayName = 'TableFooter'; -const TableRow = React.forwardRef< - HTMLTableRowElement, - React.HTMLAttributes & { - transparent?: boolean; - } ->(({ className, transparent = false, ...props }, ref) => { - const { compact } = React.useContext(TableContext); +interface TableRowProps extends React.HTMLAttributes { + transparent?: boolean; + badge?: { + text: string; + className?: string; + }; + borderClassName?: string; +} - return ( - - ); -}); +const TableRow = React.forwardRef( + ( + { className, transparent = false, badge, borderClassName, ...props }, + ref + ) => { + const { compact } = React.useContext(TableContext); + + return ( + + {props.children} + {badge && ( +
+ + {badge.text} + +
+ )} + + ); + } +); const TableHead = React.forwardRef< HTMLTableCellElement, diff --git a/packages/ui/components/ui/tabs.tsx b/packages/ui/components/ui/tabs.tsx index 97a291a48..b81879228 100644 --- a/packages/ui/components/ui/tabs.tsx +++ b/packages/ui/components/ui/tabs.tsx @@ -1,6 +1,5 @@ -'use client'; - import * as React from 'react'; +import { useRef, useState } from 'react'; import * as TabsPrimitive from '@radix-ui/react-tabs'; @@ -41,16 +40,29 @@ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; const TabsContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); +>(({ className, ...props }, ref) => { + const contentRef = useRef(null); + const [height, setHeight] = useState(null); + + return ( + { + if (contentRef.current) { + setHeight(contentRef.current.scrollHeight); + } + }} + className={cn( + 'transition-[height] duration-200 overflow-hidden', + className + )} + style={{ height: height ? `${height}px` : 'auto' }} + {...props} + > +
{props.children}
+
+ ); +}); TabsContent.displayName = TabsPrimitive.Content.displayName; export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/packages/ui/components/ui/tooltip.tsx b/packages/ui/components/ui/tooltip.tsx index c57c81fbb..8625906f8 100644 --- a/packages/ui/components/ui/tooltip.tsx +++ b/packages/ui/components/ui/tooltip.tsx @@ -8,7 +8,12 @@ import { cn } from '@ui/lib/utils'; const TooltipProvider = TooltipPrimitive.Provider; -const Tooltip = TooltipPrimitive.Root; +const Tooltip = ({ delayDuration = 50, ...props }) => ( + +); const TooltipTrigger = TooltipPrimitive.Trigger; diff --git a/packages/ui/context/ManageDialogContext.tsx b/packages/ui/context/ManageDialogContext.tsx new file mode 100644 index 000000000..2ae312213 --- /dev/null +++ b/packages/ui/context/ManageDialogContext.tsx @@ -0,0 +1,354 @@ +'use client'; +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState +} from 'react'; + +import { useQueryClient } from '@tanstack/react-query'; +import { type Address, formatUnits } from 'viem'; +import { useChainId } from 'wagmi'; + +import type { TransactionStep } from '@ui/app/_components/dialogs/manage/TransactionStepsHandler'; +import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import useUpdatedUserAssets from '@ui/hooks/ionic/useUpdatedUserAssets'; +import type { MarketData } from '@ui/types/TokensDataMap'; +import { getBlockTimePerMinuteByChainId } from '@ui/utils/networkData'; + +import { FundOperationMode } from '@ionicprotocol/types'; + +type ActiveTab = 'borrow' | 'repay' | 'supply' | 'withdraw'; +type FundOperation = + | FundOperationMode.BORROW + | FundOperationMode.REPAY + | FundOperationMode.SUPPLY + | FundOperationMode.WITHDRAW; + +export enum HFPStatus { + CRITICAL = 'CRITICAL', + NORMAL = 'NORMAL', + UNKNOWN = 'UNKNOWN', + WARNING = 'WARNING' +} + +export enum TransactionType { + SUPPLY = 'SUPPLY', + COLLATERAL = 'COLLATERAL', + BORROW = 'BORROW', + REPAY = 'REPAY', + WITHDRAW = 'WITHDRAW' +} + +interface TransactionStepsState { + [TransactionType.SUPPLY]: TransactionStep[]; + [TransactionType.COLLATERAL]: TransactionStep[]; + [TransactionType.BORROW]: TransactionStep[]; + [TransactionType.REPAY]: TransactionStep[]; + [TransactionType.WITHDRAW]: TransactionStep[]; +} + +interface ManageDialogContextType { + active: ActiveTab; + setActive: (tab: ActiveTab) => void; + resetTransactionSteps: () => void; + refetchUsedQueries: () => Promise; + selectedMarketData: MarketData; + chainId: number; + comptrollerAddress: Address; + updatedValues: { + borrowAPR: number | undefined; + borrowBalanceFrom: string; + borrowBalanceTo: string | undefined; + supplyAPY: number | undefined; + supplyBalanceFrom: string; + supplyBalanceTo: number | string; + totalBorrows: number | undefined; + updatedBorrowAPR: number | undefined; + updatedSupplyAPY: number | undefined; + updatedTotalBorrows: number | undefined; + }; + isLoadingUpdatedAssets: boolean; + setPredictionAmount: (amount: bigint) => void; + transactionSteps: TransactionStepsState; + addStepsForType: (type: TransactionType, steps: TransactionStep[]) => void; + upsertStepForType: ( + type: TransactionType, + update: + | { + index: number; + transactionStep: TransactionStep; + } + | undefined + ) => void; + getStepsForTypes: (...types: TransactionType[]) => TransactionStep[]; +} + +const formatBalance = (value: bigint | undefined, decimals: number): string => { + const formatted = Number(formatUnits(value ?? 0n, decimals)); + return formatted === 0 ? '0' : formatted.toFixed(2); +}; + +const ManageDialogContext = createContext( + undefined +); + +export const ManageDialogProvider: React.FC<{ + selectedMarketData: MarketData; + comptrollerAddress: Address; + children: React.ReactNode; +}> = ({ selectedMarketData, comptrollerAddress, children }) => { + const { currentSdk } = useMultiIonic(); + const chainId = useChainId(); + const [active, setActive] = useState('supply'); + const operationMap = useMemo>( + () => ({ + supply: FundOperationMode.SUPPLY, + withdraw: FundOperationMode.WITHDRAW, + borrow: FundOperationMode.BORROW, + repay: FundOperationMode.REPAY + }), + [] + ); + const [predictionAmount, setPredictionAmount] = useState(0n); + const [transactionSteps, setTransactionSteps] = + useState({ + [TransactionType.SUPPLY]: [], + [TransactionType.COLLATERAL]: [], + [TransactionType.BORROW]: [], + [TransactionType.REPAY]: [], + [TransactionType.WITHDRAW]: [] + }); + + useEffect(() => { + setCurrentFundOperation(operationMap[active]); + }, [active, operationMap]); + + const [currentFundOperation, setCurrentFundOperation] = + useState(FundOperationMode.SUPPLY); + + const { data: updatedAssets, isLoading: isLoadingUpdatedAssets } = + useUpdatedUserAssets({ + amount: predictionAmount, + assets: [selectedMarketData], + index: 0, + mode: currentFundOperation, + poolChainId: chainId + }); + + const updatedAsset = updatedAssets ? updatedAssets[0] : undefined; + + const queryClient = useQueryClient(); + + const refetchUsedQueries = useCallback(async () => { + const queryKeys = [ + 'useFusePoolData', + 'useBorrowMinimum', + 'useUsdPrice', + 'useAllUsdPrices', + 'useTotalSupplyAPYs', + 'useUpdatedUserAssets', + 'useMaxSupplyAmount', + 'useMaxWithdrawAmount', + 'useMaxBorrowAmount', + 'useMaxRepayAmount', + 'useSupplyCapsDataForPool', + 'useBorrowCapsDataForAsset' + ]; + + queryKeys.forEach((key) => { + queryClient.invalidateQueries({ queryKey: [key] }); + }); + }, [queryClient]); + + const updatedValues = useMemo(() => { + const blocksPerMinute = getBlockTimePerMinuteByChainId(chainId); + + if (currentSdk) { + const formatBalanceValue = (value: bigint, decimals: number) => { + const formatted = Number(formatUnits(value, decimals)); + return isNaN(formatted) + ? '0' + : formatted.toLocaleString('en-US', { maximumFractionDigits: 2 }); + }; + + return { + borrowAPR: currentSdk.ratePerBlockToAPY( + selectedMarketData.borrowRatePerBlock, + blocksPerMinute + ), + borrowBalanceFrom: formatBalanceValue( + selectedMarketData.borrowBalance, + selectedMarketData.underlyingDecimals + ), + borrowBalanceTo: updatedAsset + ? formatBalanceValue( + updatedAsset.borrowBalance, + updatedAsset.underlyingDecimals + ) + : '0', + supplyAPY: currentSdk.ratePerBlockToAPY( + selectedMarketData.supplyRatePerBlock, + blocksPerMinute + ), + supplyBalanceFrom: formatBalance( + selectedMarketData.supplyBalance, + selectedMarketData.underlyingDecimals + ), + supplyBalanceTo: updatedAsset + ? formatBalance( + updatedAsset.supplyBalance, + updatedAsset.underlyingDecimals + ) + : '0', + totalBorrows: + updatedAssets?.reduce( + (acc, cur) => + acc + (isNaN(cur.borrowBalanceFiat) ? 0 : cur.borrowBalanceFiat), + 0 + ) ?? 0, + updatedBorrowAPR: updatedAsset + ? currentSdk.ratePerBlockToAPY( + updatedAsset.borrowRatePerBlock, + blocksPerMinute + ) + : undefined, + updatedSupplyAPY: updatedAsset + ? currentSdk.ratePerBlockToAPY( + updatedAsset.supplyRatePerBlock, + blocksPerMinute + ) + : undefined, + updatedTotalBorrows: updatedAssets + ? updatedAssets.reduce( + (acc, cur) => + acc + + (isNaN(cur.borrowBalanceFiat) ? 0 : cur.borrowBalanceFiat), + 0 + ) + : 0 + }; + } + + // Default values when currentSdk is not available + return { + borrowAPR: 0, + borrowBalanceFrom: '0', + borrowBalanceTo: '0', + supplyAPY: 0, + supplyBalanceFrom: '0', + supplyBalanceTo: '0', + totalBorrows: 0, + updatedBorrowAPR: 0, + updatedSupplyAPY: 0, + updatedTotalBorrows: 0 + }; + }, [chainId, updatedAsset, selectedMarketData, updatedAssets, currentSdk]); + + const upsertStepForType = useCallback( + ( + type: TransactionType, + updatedStep: + | { index: number; transactionStep: TransactionStep } + | undefined + ) => { + if (!updatedStep) { + setTransactionSteps((prev) => ({ + ...prev, + [type]: [] + })); + return; + } + + setTransactionSteps((prev) => { + const currentSteps = prev[type].slice(); + currentSteps[updatedStep.index] = { + ...currentSteps[updatedStep.index], + ...updatedStep.transactionStep + }; + + if ( + updatedStep.transactionStep.error && + updatedStep.index + 1 < currentSteps.length + ) { + for (let i = updatedStep.index + 1; i < currentSteps.length; i++) { + currentSteps[i] = { + ...currentSteps[i], + error: true + }; + } + } + + return { + ...prev, + [type]: currentSteps + }; + }); + }, + [] + ); + + const addStepsForType = useCallback( + (type: TransactionType, steps: TransactionStep[]) => { + steps.forEach((step, i) => + upsertStepForType(type, { index: i, transactionStep: step }) + ); + }, + [upsertStepForType] + ); + + const getStepsForTypes = useCallback( + (...types: TransactionType[]) => { + return types.reduce((acc, type) => { + return [...acc, ...transactionSteps[type]]; + }, []); + }, + [transactionSteps] + ); + + const resetTransactionSteps = useCallback(() => { + refetchUsedQueries(); + setTransactionSteps({ + [TransactionType.SUPPLY]: [], + [TransactionType.COLLATERAL]: [], + [TransactionType.BORROW]: [], + [TransactionType.REPAY]: [], + [TransactionType.WITHDRAW]: [] + }); + }, [refetchUsedQueries]); + + return ( + + {children} + + ); +}; + +export const useManageDialogContext = (): ManageDialogContextType => { + const context = useContext(ManageDialogContext); + if (!context) { + throw new Error( + 'useManageDialogContext must be used within a ManageDialogProvider' + ); + } + return context; +}; diff --git a/packages/ui/hooks/market/useBorrow.ts b/packages/ui/hooks/market/useBorrow.ts new file mode 100644 index 000000000..937b91e9d --- /dev/null +++ b/packages/ui/hooks/market/useBorrow.ts @@ -0,0 +1,210 @@ +// useBorrow.ts +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { toast } from 'react-hot-toast'; +import { type Address, formatUnits, parseUnits } from 'viem'; + +import { useTransactionSteps } from '@ui/app/_components/dialogs/manage/TransactionStepsHandler'; +import { INFO_MESSAGES } from '@ui/constants'; +import { + TransactionType, + useManageDialogContext +} from '@ui/context/ManageDialogContext'; +import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import type { MarketData } from '@ui/types/TokensDataMap'; + +import { useBalancePolling } from '../useBalancePolling'; +import { useBorrowMinimum } from '../useBorrowMinimum'; +import { useMaxBorrowAmount } from '../useMaxBorrowAmount'; + +interface UseBorrowProps { + selectedMarketData: MarketData; + chainId: number; + comptrollerAddress: Address; +} + +export const useBorrow = ({ + selectedMarketData, + chainId, + comptrollerAddress +}: UseBorrowProps) => { + const [txHash, setTxHash] = useState
(); + const [isWaitingForIndexing, setIsWaitingForIndexing] = useState(false); + const [amount, setAmount] = useState('0'); + const [utilizationPercentage, setUtilizationPercentage] = useState(0); + const { data: minBorrowAmount } = useBorrowMinimum( + selectedMarketData, + chainId + ); + const { addStepsForType, upsertStepForType } = useManageDialogContext(); + + const { refetch: refetchMaxBorrow, data: maxBorrowAmount } = + useMaxBorrowAmount(selectedMarketData, comptrollerAddress, chainId); + + const { addStepsForAction, transactionSteps, upsertTransactionStep } = + useTransactionSteps(); + const { currentSdk, address } = useMultiIonic(); + + const amountAsBInt = useMemo( + () => + parseUnits( + amount?.toString() ?? '0', + selectedMarketData.underlyingDecimals + ), + [amount, selectedMarketData.underlyingDecimals] + ); + + const handleUtilization = useCallback( + (newUtilizationPercentage: number) => { + const maxAmountNumber = maxBorrowAmount?.number ?? 0; + + const calculatedAmount = ( + (newUtilizationPercentage / 100) * + maxAmountNumber + ).toFixed(parseInt(selectedMarketData.underlyingDecimals.toString())); + + setAmount(calculatedAmount); + setUtilizationPercentage(newUtilizationPercentage); + }, + [maxBorrowAmount?.number, selectedMarketData.underlyingDecimals] + ); + + // Update utilization percentage when amount changes + useEffect(() => { + if (amount === '0' || !amount || !maxBorrowAmount?.bigNumber) { + setUtilizationPercentage(0); + return; + } + + const utilization = + (Number(amountAsBInt) * 100) / Number(maxBorrowAmount.bigNumber); + setUtilizationPercentage(Math.min(Math.round(utilization), 100)); + }, [amountAsBInt, maxBorrowAmount?.bigNumber, amount]); + + const borrowLimits = { + min: formatUnits( + minBorrowAmount?.minBorrowAsset ?? 0n, + selectedMarketData.underlyingDecimals + ), + max: + maxBorrowAmount?.number?.toFixed( + parseInt(selectedMarketData.underlyingDecimals.toString()) + ) ?? '0.00' + }; + + const isUnderMinBorrow = + amount && + borrowLimits.min && + parseFloat(amount) < parseFloat(borrowLimits.min); + + const borrowAmount = async () => { + if ( + currentSdk && + address && + amount && + amountAsBInt > 0n && + minBorrowAmount && + amountAsBInt >= (minBorrowAmount.minBorrowAsset ?? 0n) && + maxBorrowAmount && + amountAsBInt <= maxBorrowAmount.bigNumber + ) { + const currentTransactionStep = 0; + addStepsForType(TransactionType.BORROW, [ + { + error: false, + message: INFO_MESSAGES.BORROW.BORROWING, + success: false + } + ]); + + try { + const { tx, errorCode } = await currentSdk.borrow( + selectedMarketData.cToken, + amountAsBInt + ); + + if (errorCode) { + throw new Error('Error during borrowing!'); + } + + upsertStepForType(TransactionType.BORROW, { + index: currentTransactionStep, + transactionStep: { + error: false, + message: INFO_MESSAGES.BORROW.BORROWING, + txHash: tx, + success: false + } + }); + + if (tx) { + await currentSdk.publicClient.waitForTransactionReceipt({ + hash: tx + }); + + setTxHash(tx); + setIsWaitingForIndexing(true); + + upsertStepForType(TransactionType.BORROW, { + index: currentTransactionStep, + transactionStep: { + error: false, + message: INFO_MESSAGES.BORROW.BORROWING, + txHash: tx, + success: true + } + }); + + toast.success( + `Borrowed ${amount} ${selectedMarketData.underlyingSymbol}` + ); + } + } catch (error) { + console.error(error); + setIsWaitingForIndexing(false); + setTxHash(undefined); + + upsertStepForType(TransactionType.BORROW, { + index: currentTransactionStep, + transactionStep: { + error: true, + message: INFO_MESSAGES.BORROW.BORROWING, + success: false + } + }); + + toast.error('Error while borrowing!'); + } + } + }; + + const { isPolling } = useBalancePolling({ + address, + chainId, + txHash, + enabled: isWaitingForIndexing, + onSuccess: () => { + setIsWaitingForIndexing(false); + setTxHash(undefined); + refetchMaxBorrow(); + setAmount('0'); + setUtilizationPercentage(0); + toast.success( + `Borrowed ${amount} ${selectedMarketData.underlyingSymbol}` + ); + } + }); + + return { + isWaitingForIndexing, + borrowAmount, + isPolling, + borrowLimits, + isUnderMinBorrow, + amount, + setAmount, + utilizationPercentage, + handleUtilization, + amountAsBInt + }; +}; diff --git a/packages/ui/hooks/market/useCollateralToggle.ts b/packages/ui/hooks/market/useCollateralToggle.ts new file mode 100644 index 000000000..e997454d8 --- /dev/null +++ b/packages/ui/hooks/market/useCollateralToggle.ts @@ -0,0 +1,177 @@ +import { useState } from 'react'; + +import { toast } from 'react-hot-toast'; + +import { useTransactionSteps } from '@ui/app/_components/dialogs/manage/TransactionStepsHandler'; +import { INFO_MESSAGES } from '@ui/constants'; +import { + TransactionType, + useManageDialogContext +} from '@ui/context/ManageDialogContext'; +import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import type { MarketData } from '@ui/types/TokensDataMap'; +import { errorCodeToMessage } from '@ui/utils/errorCodeToMessage'; + +import type { Address } from 'viem'; + +interface UseCollateralToggleProps { + selectedMarketData: MarketData; + comptrollerAddress: Address; + onSuccess: () => Promise; +} + +export const useCollateralToggle = ({ + selectedMarketData, + comptrollerAddress, + onSuccess +}: UseCollateralToggleProps) => { + const [enableCollateral, setEnableCollateral] = useState( + selectedMarketData.membership && selectedMarketData.supplyBalance > 0n + ); + + const { currentSdk } = useMultiIonic(); + const { addStepsForType, upsertStepForType } = useManageDialogContext(); + const { transactionSteps } = useTransactionSteps(); + + const handleCollateralToggle = async () => { + if (!transactionSteps.length) { + if (currentSdk && selectedMarketData.supplyBalance > 0n) { + const currentTransactionStep = 0; + + try { + let tx; + + if (enableCollateral) { + const comptrollerContract = currentSdk.createComptroller( + comptrollerAddress, + currentSdk.publicClient + ); + + const exitCode = ( + await comptrollerContract.simulate.exitMarket( + [selectedMarketData.cToken], + { account: currentSdk.walletClient!.account!.address } + ) + ).result; + + if (exitCode !== 0n) { + toast.error(errorCodeToMessage(Number(exitCode))); + return; + } + + addStepsForType(TransactionType.COLLATERAL, [ + { + error: false, + message: INFO_MESSAGES.COLLATERAL.DISABLE, + success: false + } + ]); + + tx = await comptrollerContract.write.exitMarket( + [selectedMarketData.cToken], + { + account: currentSdk.walletClient!.account!.address, + chain: currentSdk.publicClient.chain + } + ); + + upsertStepForType(TransactionType.COLLATERAL, { + index: currentTransactionStep, + transactionStep: { + error: false, + message: INFO_MESSAGES.COLLATERAL.DISABLE, + txHash: tx, + success: false + } + }); + + await currentSdk.publicClient.waitForTransactionReceipt({ + hash: tx + }); + + setEnableCollateral(false); + + upsertStepForType(TransactionType.COLLATERAL, { + index: currentTransactionStep, + transactionStep: { + error: false, + message: INFO_MESSAGES.COLLATERAL.DISABLE, + txHash: tx, + success: true + } + }); + } else { + addStepsForType(TransactionType.COLLATERAL, [ + { + error: false, + message: INFO_MESSAGES.COLLATERAL.ENABLE, + success: false + } + ]); + + tx = await currentSdk.enterMarkets( + selectedMarketData.cToken, + comptrollerAddress + ); + + upsertStepForType(TransactionType.COLLATERAL, { + index: currentTransactionStep, + transactionStep: { + error: false, + message: INFO_MESSAGES.COLLATERAL.ENABLE, + txHash: tx, + success: false + } + }); + + await currentSdk.publicClient.waitForTransactionReceipt({ + hash: tx + }); + + setEnableCollateral(true); + + upsertStepForType(TransactionType.COLLATERAL, { + index: currentTransactionStep, + transactionStep: { + error: false, + message: INFO_MESSAGES.COLLATERAL.ENABLE, + txHash: tx, + success: true + } + }); + } + + await onSuccess(); + return; + } catch (error) { + console.error(error); + + upsertStepForType(TransactionType.COLLATERAL, { + index: currentTransactionStep, + transactionStep: { + error: true, + message: enableCollateral + ? INFO_MESSAGES.COLLATERAL.DISABLE + : INFO_MESSAGES.COLLATERAL.ENABLE, + success: false + } + }); + + toast.error( + `Error while ${ + enableCollateral ? 'disabling' : 'enabling' + } collateral!` + ); + } + } + + setEnableCollateral(!enableCollateral); + } + }; + + return { + enableCollateral, + handleCollateralToggle, + transactionSteps + }; +}; diff --git a/packages/ui/hooks/market/useHealth.ts b/packages/ui/hooks/market/useHealth.ts new file mode 100644 index 000000000..c78855456 --- /dev/null +++ b/packages/ui/hooks/market/useHealth.ts @@ -0,0 +1,133 @@ +// useHealth.ts +import { useMemo } from 'react'; +import { Address, formatEther, maxUint256, parseEther, parseUnits } from 'viem'; +import { useChainId } from 'wagmi'; + +import { ActiveTab, HFPStatus } from '@ui/app/_components/dialogs/manage'; +import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import { + useHealthFactor, + useHealthFactorPrediction +} from '../pools/useHealthFactor'; + +interface UseHealthProps { + comptrollerAddress: Address; + cToken: Address; + activeTab: ActiveTab; + amount: bigint; + exchangeRate: bigint; + decimals: number; + updatedAsset?: { + supplyBalanceFiat: number; + }; +} + +export const useHealth = ({ + comptrollerAddress, + cToken, + activeTab, + amount, + exchangeRate, + decimals, + updatedAsset +}: UseHealthProps) => { + const { address } = useMultiIonic(); + const chainId = useChainId(); + + const { data: healthFactor } = useHealthFactor(comptrollerAddress, chainId); + + const { + data: _predictedHealthFactor, + isLoading: isLoadingPredictedHealthFactor + } = useHealthFactorPrediction( + comptrollerAddress, + address ?? ('' as Address), + cToken, + activeTab === 'withdraw' + ? (amount * BigInt(1e18)) / exchangeRate + : parseUnits('0', decimals), + activeTab === 'borrow' ? amount : parseUnits('0', decimals), + activeTab === 'repay' + ? (amount * BigInt(1e18)) / exchangeRate + : parseUnits('0', decimals) + ); + + const predictedHealthFactor = useMemo(() => { + if (updatedAsset && updatedAsset?.supplyBalanceFiat < 0.01) { + return maxUint256; + } + + if (amount === 0n) { + return parseEther(healthFactor ?? '0'); + } + + return _predictedHealthFactor; + }, [_predictedHealthFactor, updatedAsset, amount, healthFactor]); + + const hfpStatus = useMemo(() => { + // If we're loading but have a previous health factor, keep using it + if (isLoadingPredictedHealthFactor && healthFactor) { + return healthFactor === '-1' + ? HFPStatus.NORMAL + : Number(healthFactor) <= 1.1 + ? HFPStatus.CRITICAL + : Number(healthFactor) <= 1.2 + ? HFPStatus.WARNING + : HFPStatus.NORMAL; + } + + if (!predictedHealthFactor && !healthFactor) { + return HFPStatus.UNKNOWN; + } + + if (predictedHealthFactor === maxUint256) { + return HFPStatus.NORMAL; + } + + if (updatedAsset && updatedAsset?.supplyBalanceFiat < 0.01) { + return HFPStatus.NORMAL; + } + + const predictedHealthFactorNumber = Number( + formatEther(predictedHealthFactor ?? 0n) + ); + + if (predictedHealthFactorNumber <= 1.1) { + return HFPStatus.CRITICAL; + } + + if (predictedHealthFactorNumber <= 1.2) { + return HFPStatus.WARNING; + } + + return HFPStatus.NORMAL; + }, [ + predictedHealthFactor, + updatedAsset, + healthFactor, + isLoadingPredictedHealthFactor + ]); + + const normalizedHealthFactor = useMemo(() => { + return healthFactor + ? healthFactor === '-1' + ? '∞' + : Number(healthFactor).toFixed(2) + : undefined; + }, [healthFactor]); + + const normalizedPredictedHealthFactor = useMemo(() => { + return predictedHealthFactor === maxUint256 + ? '∞' + : Number(formatEther(predictedHealthFactor ?? 0n)).toFixed(2); + }, [predictedHealthFactor]); + + return { + hfpStatus, + healthFactor: { + current: normalizedHealthFactor ?? '0', + predicted: normalizedPredictedHealthFactor ?? '0' + }, + isLoadingPredictedHealthFactor + }; +}; diff --git a/packages/ui/hooks/market/useMarketData.ts b/packages/ui/hooks/market/useMarketData.ts new file mode 100644 index 000000000..ad0e215e8 --- /dev/null +++ b/packages/ui/hooks/market/useMarketData.ts @@ -0,0 +1,299 @@ +// hooks/useMarketData.ts +import { useEffect, useMemo } from 'react'; + +import { type Address, formatEther, formatUnits } from 'viem'; + +import { + FLYWHEEL_TYPE_MAP, + pools, + shouldGetFeatured +} from '@ui/constants/index'; +import { useBorrowCapsForAssets } from '@ui/hooks/ionic/useBorrowCapsDataForAsset'; +import { useBorrowAPYs } from '@ui/hooks/useBorrowAPYs'; +import { useFusePoolData } from '@ui/hooks/useFusePoolData'; +import { useLoopMarkets } from '@ui/hooks/useLoopMarkets'; +import { useMerklApr } from '@ui/hooks/useMerklApr'; +import { useRewards } from '@ui/hooks/useRewards'; +import { useSupplyAPYs } from '@ui/hooks/useSupplyAPYs'; +import { useStore } from '@ui/store/Store'; +import type { MarketData } from '@ui/types/TokensDataMap'; + +import type { FlywheelReward } from '@ionicprotocol/types'; + +export type MarketRowData = MarketData & { + asset: string; + logo: string; + supply: { + balance: string; + balanceUSD: string; + total: string; + totalUSD: string; + }; + borrow: { + balance: string; + balanceUSD: string; + total: string; + totalUSD: string; + }; + supplyAPR: number; + borrowAPR: number; + collateralFactor: number; + membership: boolean; + cTokenAddress: Address; + comptrollerAddress: Address; + underlyingDecimals: number; + loopPossible: boolean; + supplyRewards: FlywheelReward[]; + borrowRewards: FlywheelReward[]; + supplyAPRTotal: number | undefined; + borrowAPRTotal: number | undefined; + isBorrowDisabled: boolean; + underlyingSymbol: string; +}; + +export const useMarketData = ( + selectedPool: string, + chain: number | string, + selectedSymbol: string | undefined +) => { + const { data: poolData, isLoading: isLoadingPoolData } = useFusePoolData( + selectedPool, + +chain + ); + + const setFeaturedSupply = useStore((state) => state.setFeaturedSupply); + const setFeaturedSupply2 = useStore((state) => state.setFeaturedSupply2); + + const assets = useMemo( + () => poolData?.assets, + [poolData] + ); + + const { data: borrowRates } = useBorrowAPYs(assets ?? [], +chain); + const { data: supplyRates } = useSupplyAPYs(assets ?? [], +chain); + const { data: merklApr } = useMerklApr(); + const { data: loopMarkets, isLoading: isLoadingLoopMarkets } = useLoopMarkets( + poolData?.assets.map((asset) => asset.cToken) ?? [], + +chain + ); + + const { data: rewards } = useRewards({ + chainId: +chain, + poolId: selectedPool + }); + + // Get all cToken addresses for borrow caps query + const cTokenAddresses = useMemo( + () => assets?.map((asset) => asset.cToken) ?? [], + [assets] + ); + + // Query borrow caps for all assets at once + const { data: borrowCapsData } = useBorrowCapsForAssets( + cTokenAddresses, + +chain + ); + + const formatNumber = (value: bigint | number, decimals: number): string => { + const parsedValue = + typeof value === 'bigint' + ? parseFloat(formatUnits(value, decimals)) + : value; + + return parsedValue.toLocaleString('en-US', { maximumFractionDigits: 2 }); + }; + + const marketData = useMemo(() => { + if (!assets) return []; + + const transformedData = pools[+chain].pools[+selectedPool].assets + .map((symbol: string) => { + const asset = assets.find((a) => a.underlyingSymbol === symbol); + if (!asset) return null; + + const supplyRewards = rewards?.[asset.cToken] + ?.filter((reward) => + FLYWHEEL_TYPE_MAP[+chain]?.supply?.includes( + (reward as FlywheelReward).flywheel + ) + ) + .map((reward) => ({ + ...reward, + apy: (reward.apy ?? 0) * 100 + })); + + const borrowRewards = rewards?.[asset.cToken] + ?.filter((reward) => + FLYWHEEL_TYPE_MAP[+chain]?.borrow?.includes( + (reward as FlywheelReward).flywheel + ) + ) + .map((reward) => ({ + ...reward, + apy: (reward.apy ?? 0) * 100 + })); + + const merklAprForToken = merklApr?.find( + (a) => Object.keys(a)[0].toLowerCase() === asset.cToken.toLowerCase() + )?.[asset.cToken]; + + const totalSupplyRewardsAPR = + (supplyRewards?.reduce((acc, reward) => acc + (reward.apy ?? 0), 0) ?? + 0) + (merklAprForToken ?? 0); + + const totalBorrowRewardsAPR = + borrowRewards?.reduce((acc, reward) => acc + (reward.apy ?? 0), 0) ?? + 0; + + // Get borrow caps for this specific asset from the bulk query result + const assetBorrowCaps = borrowCapsData?.[asset.cToken]; + + const supply = { + balance: + typeof asset.supplyBalance === 'bigint' + ? `${formatNumber(asset.supplyBalance, asset.underlyingDecimals)} ${asset.underlyingSymbol}` + : `0 ${asset.underlyingSymbol}`, + balanceUSD: formatNumber(asset.supplyBalanceFiat, 0), + total: asset.totalSupplyNative + ? `${formatNumber(asset.totalSupply, asset.underlyingDecimals)} ${asset.underlyingSymbol}` + : `0 ${asset.underlyingSymbol}`, + totalUSD: formatNumber(asset.totalSupplyFiat, 0) + }; + + const borrow = { + balance: + typeof asset.borrowBalance === 'bigint' + ? `${formatNumber(asset.borrowBalance, asset.underlyingDecimals)} ${asset.underlyingSymbol}` + : `0 ${asset.underlyingSymbol}`, + balanceUSD: formatNumber(asset.borrowBalanceFiat, 0), + total: asset.totalBorrowNative + ? `${formatNumber(asset.totalBorrow, asset.underlyingDecimals)} ${asset.underlyingSymbol}` + : `0 ${asset.underlyingSymbol}`, + totalUSD: formatNumber(asset.totalBorrowFiat, 0) + }; + + return { + ...asset, + asset: asset.underlyingSymbol, + logo: `/img/symbols/32/color/${asset.underlyingSymbol.toLowerCase()}.png`, + supply, + borrow, + supplyAPR: supplyRates?.[asset.cToken] + ? supplyRates[asset.cToken] * 100 + : 0, + borrowAPR: borrowRates?.[asset.cToken] + ? borrowRates[asset.cToken] * 100 + : 0, + collateralFactor: Number(formatEther(asset.collateralFactor)) * 100, + membership: asset.membership, + cTokenAddress: asset.cToken, + comptrollerAddress: poolData?.comptroller, + underlyingDecimals: asset.underlyingDecimals, + loopPossible: loopMarkets + ? loopMarkets[asset.cToken].length > 0 + : false, + supplyRewards, + borrowRewards, + supplyAPRTotal: supplyRates?.[asset.cToken] + ? supplyRates[asset.cToken] * 100 + totalSupplyRewardsAPR + : undefined, + borrowAPRTotal: borrowRates?.[asset.cToken] + ? 0 - borrowRates[asset.cToken] * 100 + totalBorrowRewardsAPR + : undefined, + isBorrowDisabled: assetBorrowCaps + ? assetBorrowCaps.totalBorrowCap <= 1 + : false + }; + }) + .filter(Boolean) as MarketRowData[]; + + return transformedData; + }, [ + assets, + chain, + selectedPool, + rewards, + merklApr, + supplyRates, + borrowRates, + loopMarkets, + poolData?.comptroller, + borrowCapsData + ]); + + useEffect(() => { + if (!marketData.length) return; + + // Find and set featured supply assets based on shouldGetFeatured mapping + marketData.forEach((market) => { + // Check if this market is featured supply 1 + if ( + shouldGetFeatured.featuredSupply[+chain][ + selectedPool + ]?.toLowerCase() === market.asset.toLowerCase() + ) { + setFeaturedSupply({ + asset: market.asset, + supplyAPR: market.supplyAPR, + supplyAPRTotal: market.supplyAPRTotal, + rewards: market.supplyRewards, + dropdownSelectedChain: +chain, + selectedPoolId: selectedPool, + cToken: market.cTokenAddress, + pool: market.comptrollerAddress + }); + } + + // Check if this market is featured supply 2 + if ( + shouldGetFeatured.featuredSupply2[+chain][ + selectedPool + ]?.toLowerCase() === market.asset.toLowerCase() + ) { + setFeaturedSupply2({ + asset: market.asset, + supplyAPR: market.supplyAPR, + supplyAPRTotal: market.supplyAPRTotal, + rewards: market.supplyRewards, + dropdownSelectedChain: +chain, + selectedPoolId: selectedPool, + cToken: market.cTokenAddress, + pool: market.comptrollerAddress + }); + } + }); + }, [ + marketData, + chain, + selectedPool, + setFeaturedSupply, + setFeaturedSupply2, + poolData?.comptroller + ]); + + const selectedMarketData = useMemo(() => { + const found = assets?.find( + (asset) => asset.underlyingSymbol === selectedSymbol + ); + return found; + }, [assets, selectedSymbol]); + + const loopProps = useMemo(() => { + if (!selectedMarketData || !poolData) return null; + return { + borrowableAssets: loopMarkets + ? loopMarkets[selectedMarketData.cToken] + : [], + comptrollerAddress: poolData.comptroller, + selectedCollateralAsset: selectedMarketData + }; + }, [selectedMarketData, poolData, loopMarkets]); + + return { + marketData, + isLoading: isLoadingPoolData, + poolData, + selectedMarketData, + loopProps + }; +}; diff --git a/packages/ui/hooks/market/useRepay.ts b/packages/ui/hooks/market/useRepay.ts new file mode 100644 index 000000000..a1eb04893 --- /dev/null +++ b/packages/ui/hooks/market/useRepay.ts @@ -0,0 +1,285 @@ +import { useCallback, useState, useEffect, useMemo } from 'react'; + +import { toast } from 'react-hot-toast'; +import { + type Address, + formatUnits, + parseUnits, + getContract, + maxUint256 +} from 'viem'; + +import { useTransactionSteps } from '@ui/app/_components/dialogs/manage/TransactionStepsHandler'; +import { INFO_MESSAGES } from '@ui/constants'; +import { + TransactionType, + useManageDialogContext +} from '@ui/context/ManageDialogContext'; +import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import type { MarketData } from '@ui/types/TokensDataMap'; + +import { useBalancePolling } from '../useBalancePolling'; +import { useMaxRepayAmount } from '../useMaxRepayAmount'; + +import { icErc20Abi } from '@ionicprotocol/sdk'; + +interface UseRepayProps { + maxAmount: bigint; + selectedMarketData: MarketData; + chainId: number; +} + +export const useRepay = ({ + maxAmount, + selectedMarketData, + chainId +}: UseRepayProps) => { + const { address, currentSdk } = useMultiIonic(); + const [txHash, setTxHash] = useState
(); + const [isWaitingForIndexing, setIsWaitingForIndexing] = useState(false); + const [amount, setAmount] = useState('0'); + const [utilizationPercentage, setUtilizationPercentage] = useState(0); + const { addStepsForType, upsertStepForType } = useManageDialogContext(); + const { transactionSteps } = useTransactionSteps(); + + const amountAsBInt = useMemo( + () => + parseUnits( + amount?.toString() ?? '0', + selectedMarketData.underlyingDecimals + ), + [amount, selectedMarketData.underlyingDecimals] + ); + + const handleUtilization = useCallback( + (newUtilizationPercentage: number) => { + const maxAmountNumber = Number( + formatUnits(maxAmount ?? 0n, selectedMarketData.underlyingDecimals) + ); + const calculatedAmount = ( + (newUtilizationPercentage / 100) * + maxAmountNumber + ).toFixed(parseInt(selectedMarketData.underlyingDecimals.toString())); + + setAmount(calculatedAmount); + setUtilizationPercentage(newUtilizationPercentage); + }, + [maxAmount, selectedMarketData.underlyingDecimals] + ); + + useEffect(() => { + if (amount === '0' || !amount || maxAmount === 0n) { + setUtilizationPercentage(0); + return; + } + const utilization = (Number(amountAsBInt) * 100) / Number(maxAmount); + setUtilizationPercentage(Math.min(Math.round(utilization), 100)); + }, [amountAsBInt, maxAmount, amount]); + + const currentBorrowAmountAsFloat = useMemo( + () => parseFloat(selectedMarketData.borrowBalance.toString()), + [selectedMarketData] + ); + + const repayAmount = useCallback(async () => { + if ( + !currentSdk || + !address || + !amount || + amountAsBInt <= 0n || + !currentBorrowAmountAsFloat + ) + return; + + let currentTransactionStep = 0; + + addStepsForType(TransactionType.REPAY, [ + { + error: false, + message: INFO_MESSAGES.REPAY.APPROVE, + success: false + }, + { + error: false, + message: INFO_MESSAGES.REPAY.REPAYING, + success: false + } + ]); + + try { + // Get token instances + const token = currentSdk.getEIP20TokenInstance( + selectedMarketData.underlyingToken, + currentSdk.publicClient as any + ); + + const cToken = getContract({ + address: selectedMarketData.cToken, + abi: icErc20Abi, + client: currentSdk.walletClient! + }); + + const currentAllowance = await token.read.allowance([ + address, + selectedMarketData.cToken + ]); + + if (currentAllowance < amountAsBInt) { + const approveTx = await currentSdk.approve( + selectedMarketData.cToken, + selectedMarketData.underlyingToken, + amountAsBInt + ); + + upsertStepForType(TransactionType.REPAY, { + index: currentTransactionStep, + transactionStep: { + error: false, + message: INFO_MESSAGES.REPAY.APPROVE, + txHash: approveTx, + success: false + } + }); + + await currentSdk.publicClient.waitForTransactionReceipt({ + hash: approveTx, + confirmations: 2 + }); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + + currentTransactionStep++; + + // Check if we're repaying the max amount + const isRepayingMax = + amountAsBInt >= (selectedMarketData.borrowBalance ?? 0n); + const repayAmount = isRepayingMax ? maxUint256 : amountAsBInt; + + // Verify final allowance + const finalAllowance = await token.read.allowance([ + address, + selectedMarketData.cToken + ]); + if (finalAllowance < amountAsBInt) { + throw new Error('Insufficient allowance after approval'); + } + + upsertStepForType(TransactionType.REPAY, { + index: currentTransactionStep - 1, + transactionStep: { + error: false, + message: INFO_MESSAGES.REPAY.APPROVE, + success: true + } + }); + + if (!currentSdk || !currentSdk.walletClient) { + console.error('SDK or wallet client is not initialized'); + return; + } + + // Estimate gas first + const gasLimit = await cToken.estimateGas.repayBorrow([repayAmount], { + account: address + }); + + // Execute the repay with gas limit + const tx = await cToken.write.repayBorrow([repayAmount], { + gas: gasLimit, + account: address, + chain: currentSdk.walletClient.chain + }); + + upsertStepForType(TransactionType.REPAY, { + index: currentTransactionStep, + transactionStep: { + error: false, + message: INFO_MESSAGES.REPAY.REPAYING, + txHash: tx, + success: false + } + }); + + await currentSdk.publicClient.waitForTransactionReceipt({ + hash: tx, + confirmations: 1 + }); + + setTxHash(tx); + setIsWaitingForIndexing(true); + + upsertStepForType(TransactionType.REPAY, { + index: currentTransactionStep, + transactionStep: { + error: false, + message: INFO_MESSAGES.REPAY.REPAYING, + txHash: tx, + success: true + } + }); + + toast.success(`Repaid ${amount} ${selectedMarketData.underlyingSymbol}`); + } catch (error) { + console.error('Repay error:', error); + setIsWaitingForIndexing(false); + setTxHash(undefined); + + upsertStepForType(TransactionType.REPAY, { + index: currentTransactionStep, + transactionStep: { + error: true, + message: + currentTransactionStep === 0 + ? INFO_MESSAGES.REPAY.APPROVE + : INFO_MESSAGES.REPAY.REPAYING, + success: false + } + }); + + toast.error('Error while repaying!'); + } + }, [ + currentSdk, + address, + amount, + amountAsBInt, + currentBorrowAmountAsFloat, + selectedMarketData, + addStepsForType, + upsertStepForType + ]); + + const { refetch: refetchMaxRepay } = useMaxRepayAmount( + selectedMarketData, + chainId + ); + + const { isPolling } = useBalancePolling({ + address, + chainId, + txHash, + enabled: isWaitingForIndexing, + onSuccess: () => { + setIsWaitingForIndexing(false); + setTxHash(undefined); + refetchMaxRepay(); + setAmount('0'); + setUtilizationPercentage(0); + toast.success(`Repaid ${amount} ${selectedMarketData.underlyingSymbol}`); + } + }); + + return { + isWaitingForIndexing, + repayAmount, + transactionSteps, + isPolling, + currentBorrowAmountAsFloat, + amount, + setAmount, + utilizationPercentage, + handleUtilization, + amountAsBInt + }; +}; diff --git a/packages/ui/hooks/market/useSupply.ts b/packages/ui/hooks/market/useSupply.ts new file mode 100644 index 000000000..f31147db0 --- /dev/null +++ b/packages/ui/hooks/market/useSupply.ts @@ -0,0 +1,344 @@ +import { useCallback, useState, useEffect, useMemo } from 'react'; + +import { toast } from 'react-hot-toast'; +import { type Address, formatUnits, parseUnits } from 'viem'; +import { getContract } from 'viem'; + +import { useTransactionSteps } from '@ui/app/_components/dialogs/manage/TransactionStepsHandler'; +import { INFO_MESSAGES } from '@ui/constants'; +import { + TransactionType, + useManageDialogContext +} from '@ui/context/ManageDialogContext'; +import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import type { MarketData } from '@ui/types/TokensDataMap'; + +import { useBalancePolling } from '../useBalancePolling'; +import { useMaxSupplyAmount } from '../useMaxSupplyAmount'; + +import { icErc20Abi } from '@ionicprotocol/sdk'; + +interface UseSupplyProps { + maxAmount: bigint; + enableCollateral: boolean; + selectedMarketData: MarketData; + comptrollerAddress: Address; + chainId: number; +} + +export const useSupply = ({ + maxAmount, + enableCollateral, + selectedMarketData, + comptrollerAddress, + chainId +}: UseSupplyProps) => { + const { address, currentSdk } = useMultiIonic(); + const [txHash, setTxHash] = useState
(); + const [isWaitingForIndexing, setIsWaitingForIndexing] = useState(false); + const [amount, setAmount] = useState('0'); + const [utilizationPercentage, setUtilizationPercentage] = useState(0); + const { addStepsForType, upsertStepForType } = useManageDialogContext(); + + const { transactionSteps } = useTransactionSteps(); + + const amountAsBInt = useMemo( + () => + parseUnits( + amount?.toString() ?? '0', + selectedMarketData.underlyingDecimals + ), + [amount, selectedMarketData.underlyingDecimals] + ); + + const handleUtilization = useCallback( + (newUtilizationPercentage: number) => { + const maxAmountNumber = Number( + formatUnits(maxAmount ?? 0n, selectedMarketData.underlyingDecimals) + ); + const calculatedAmount = ( + (newUtilizationPercentage / 100) * + maxAmountNumber + ).toFixed(parseInt(selectedMarketData.underlyingDecimals.toString())); + + setAmount(calculatedAmount); + setUtilizationPercentage(newUtilizationPercentage); + }, + [maxAmount, selectedMarketData.underlyingDecimals] + ); + + useEffect(() => { + if (amount === '0' || !amount || maxAmount === 0n) { + setUtilizationPercentage(0); + return; + } + const utilization = (Number(amountAsBInt) * 100) / Number(maxAmount); + setUtilizationPercentage(Math.min(Math.round(utilization), 100)); + }, [amountAsBInt, maxAmount, amount]); + + const supplyAmount = useCallback(async () => { + if ( + !currentSdk || + !address || + !amount || + amountAsBInt <= 0n || + amountAsBInt > maxAmount + ) + return; + + let currentTransactionStep = 0; + + // Add steps for both supply and collateral if needed + addStepsForType(TransactionType.SUPPLY, [ + { + error: false, + message: INFO_MESSAGES.SUPPLY.APPROVE, + success: false + }, + { + error: false, + message: INFO_MESSAGES.SUPPLY.SUPPLYING, + success: false + } + ]); + + if (enableCollateral && !selectedMarketData.membership) { + addStepsForType(TransactionType.COLLATERAL, [ + { + error: false, + message: INFO_MESSAGES.SUPPLY.COLLATERAL, + success: false + } + ]); + } + + try { + const token = currentSdk.getEIP20TokenInstance( + selectedMarketData.underlyingToken, + currentSdk.publicClient as any + ); + + const cToken = getContract({ + address: selectedMarketData.cToken, + abi: icErc20Abi, + client: currentSdk.walletClient! + }); + + // Check and handle allowance + const currentAllowance = await token.read.allowance([ + address, + selectedMarketData.cToken + ]); + + if (currentAllowance < amountAsBInt) { + const approveTx = await currentSdk.approve( + selectedMarketData.cToken, + selectedMarketData.underlyingToken, + amountAsBInt + ); + + upsertStepForType(TransactionType.SUPPLY, { + index: currentTransactionStep, + transactionStep: { + error: false, + message: INFO_MESSAGES.SUPPLY.APPROVE, + txHash: approveTx, + success: false + } + }); + + await currentSdk.publicClient.waitForTransactionReceipt({ + hash: approveTx, + confirmations: 2 + }); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + + upsertStepForType(TransactionType.SUPPLY, { + index: currentTransactionStep, + transactionStep: { + error: false, + message: INFO_MESSAGES.SUPPLY.APPROVE, + success: true + } + }); + + currentTransactionStep++; + + // Handle collateral if needed + if (enableCollateral && !selectedMarketData.membership) { + const enterMarketsTx = await currentSdk.enterMarkets( + selectedMarketData.cToken, + comptrollerAddress + ); + + upsertStepForType(TransactionType.COLLATERAL, { + index: 0, + transactionStep: { + error: false, + message: INFO_MESSAGES.SUPPLY.COLLATERAL, + txHash: enterMarketsTx, + success: false + } + }); + + await currentSdk.publicClient.waitForTransactionReceipt({ + hash: enterMarketsTx + }); + + upsertStepForType(TransactionType.COLLATERAL, { + index: 0, + transactionStep: { + error: false, + message: INFO_MESSAGES.SUPPLY.COLLATERAL, + success: true + } + }); + } + + // Verify final allowance + const finalAllowance = await token.read.allowance([ + address, + selectedMarketData.cToken + ]); + if (finalAllowance < amountAsBInt) { + throw new Error('Insufficient allowance after approval'); + } + + // Execute mint + const gasLimit = await cToken.estimateGas.mint([amountAsBInt], { + account: address + }); + + if (!currentSdk || !currentSdk.walletClient) { + console.error('SDK or wallet client is not initialized'); + return; + } + + const tx = await cToken.write.mint([amountAsBInt], { + gas: gasLimit, + account: address, + chain: currentSdk.walletClient.chain + }); + + upsertStepForType(TransactionType.SUPPLY, { + index: currentTransactionStep, + transactionStep: { + error: false, + message: INFO_MESSAGES.SUPPLY.SUPPLYING, + txHash: tx, + success: false + } + }); + + await currentSdk.publicClient.waitForTransactionReceipt({ + hash: tx, + confirmations: 1 + }); + + setTxHash(tx); + setIsWaitingForIndexing(true); + + upsertStepForType(TransactionType.SUPPLY, { + index: currentTransactionStep, + transactionStep: { + error: false, + message: INFO_MESSAGES.SUPPLY.SUPPLYING, + txHash: tx, + success: true + } + }); + + toast.success( + `Supplied ${amount} ${selectedMarketData.underlyingSymbol}` + ); + } catch (error) { + console.error('Supply error:', error); + setIsWaitingForIndexing(false); + setTxHash(undefined); + + // Mark all remaining steps as error + upsertStepForType(TransactionType.SUPPLY, { + index: currentTransactionStep, + transactionStep: { + error: true, + message: INFO_MESSAGES.SUPPLY.SUPPLYING, + success: false + } + }); + + if (enableCollateral && !selectedMarketData.membership) { + upsertStepForType(TransactionType.COLLATERAL, { + index: 0, + transactionStep: { + error: true, + message: INFO_MESSAGES.SUPPLY.COLLATERAL, + success: false + } + }); + } + + toast.error('Error while supplying!'); + } + }, [ + currentSdk, + address, + amount, + amountAsBInt, + maxAmount, + selectedMarketData, + enableCollateral, + comptrollerAddress, + addStepsForType, + upsertStepForType + ]); + + const { refetch: refetchMaxSupply } = useMaxSupplyAmount( + selectedMarketData, + comptrollerAddress, + chainId + ); + + const { isPolling } = useBalancePolling({ + address, + chainId, + txHash, + enabled: isWaitingForIndexing, + onSuccess: () => { + setIsWaitingForIndexing(false); + setTxHash(undefined); + refetchMaxSupply(); + setAmount('0'); + setUtilizationPercentage(0); + toast.success( + `Supplied ${amount} ${selectedMarketData.underlyingSymbol}` + ); + } + }); + + async function resetAllowance(selectedMarketData: MarketData) { + const tx = await currentSdk?.approve( + selectedMarketData.cToken, + selectedMarketData.underlyingToken, + 0n // Set allowance to 0 + ); + + if (tx) { + await currentSdk?.publicClient.waitForTransactionReceipt({ hash: tx }); + } + } + + return { + isWaitingForIndexing, + supplyAmount, + transactionSteps, + isPolling, + amount, + setAmount, + utilizationPercentage, + handleUtilization, + amountAsBInt, + resetAllowance + }; +}; diff --git a/packages/ui/hooks/market/useWithdraw.ts b/packages/ui/hooks/market/useWithdraw.ts new file mode 100644 index 000000000..8ff70cb1e --- /dev/null +++ b/packages/ui/hooks/market/useWithdraw.ts @@ -0,0 +1,189 @@ +// useWithdraw.ts +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { toast } from 'react-hot-toast'; +import { type Address, formatUnits, parseUnits } from 'viem'; + +import { INFO_MESSAGES } from '@ui/constants'; +import { + TransactionType, + useManageDialogContext +} from '@ui/context/ManageDialogContext'; +import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import type { MarketData } from '@ui/types/TokensDataMap'; + +import { useBalancePolling } from '../useBalancePolling'; +import { useMaxWithdrawAmount } from '../useMaxWithdrawAmount'; + +interface UseWithdrawProps { + maxAmount: bigint; + selectedMarketData: MarketData; + chainId: number; +} + +export const useWithdraw = ({ + maxAmount, + selectedMarketData, + chainId +}: UseWithdrawProps) => { + const [txHash, setTxHash] = useState
(); + const [isWaitingForIndexing, setIsWaitingForIndexing] = useState(false); + const [amount, setAmount] = useState('0'); + const [utilizationPercentage, setUtilizationPercentage] = useState(0); + + const { addStepsForType, upsertStepForType } = useManageDialogContext(); + + const { currentSdk, address } = useMultiIonic(); + + const amountAsBInt = useMemo( + () => + parseUnits( + amount?.toString() ?? '0', + selectedMarketData.underlyingDecimals + ), + [amount, selectedMarketData.underlyingDecimals] + ); + + const { refetch: refetchMaxWithdraw } = useMaxWithdrawAmount( + selectedMarketData, + chainId + ); + + const handleUtilization = useCallback( + (newUtilizationPercentage: number) => { + const maxAmountNumber = Number( + formatUnits(maxAmount ?? 0n, selectedMarketData.underlyingDecimals) + ); + + const calculatedAmount = ( + (newUtilizationPercentage / 100) * + maxAmountNumber + ).toFixed(parseInt(selectedMarketData.underlyingDecimals.toString())); + + setAmount(calculatedAmount); + setUtilizationPercentage(newUtilizationPercentage); + }, + [maxAmount, selectedMarketData.underlyingDecimals] + ); + + // Update utilization percentage when amount changes + useEffect(() => { + if (amount === '0' || !amount) { + setUtilizationPercentage(0); + return; + } + + if (maxAmount === 0n) { + setUtilizationPercentage(0); + return; + } + + const utilization = (Number(amountAsBInt) * 100) / Number(maxAmount); + setUtilizationPercentage(Math.min(Math.round(utilization), 100)); + }, [amountAsBInt, maxAmount, amount]); + + const withdrawAmount = async () => { + if (currentSdk && address && amount && amountAsBInt > 0n && maxAmount) { + const currentTransactionStep = 0; + addStepsForType(TransactionType.WITHDRAW, [ + { + error: false, + message: INFO_MESSAGES.WITHDRAW.WITHDRAWING, + success: false + } + ]); + + try { + const amountToWithdraw = amountAsBInt; + const isMax = amountToWithdraw === maxAmount; + + const { tx, errorCode } = await currentSdk.withdraw( + selectedMarketData.cToken, + amountToWithdraw, + isMax + ); + + if (errorCode) { + console.error(errorCode); + throw new Error('Error during withdrawing!'); + } + + upsertStepForType(TransactionType.WITHDRAW, { + index: currentTransactionStep, + transactionStep: { + error: false, + message: INFO_MESSAGES.WITHDRAW.WITHDRAWING, + txHash: tx, + success: false + } + }); + + if (tx) { + await currentSdk.publicClient.waitForTransactionReceipt({ + hash: tx + }); + + setTxHash(tx); + setIsWaitingForIndexing(true); + + upsertStepForType(TransactionType.WITHDRAW, { + index: currentTransactionStep, + transactionStep: { + error: false, + message: INFO_MESSAGES.WITHDRAW.WITHDRAWING, + txHash: tx, + success: true + } + }); + + toast.success( + `Withdrawn ${amount} ${selectedMarketData.underlyingSymbol}` + ); + } + } catch (error) { + console.error(error); + setIsWaitingForIndexing(false); + setTxHash(undefined); + + upsertStepForType(TransactionType.WITHDRAW, { + index: currentTransactionStep, + transactionStep: { + error: true, + message: INFO_MESSAGES.WITHDRAW.WITHDRAWING, + success: false + } + }); + + toast.error('Error while withdrawing!'); + } + } + }; + + const { isPolling } = useBalancePolling({ + address, + chainId, + txHash, + enabled: isWaitingForIndexing, + onSuccess: () => { + setIsWaitingForIndexing(false); + setTxHash(undefined); + refetchMaxWithdraw(); + setAmount('0'); + setUtilizationPercentage(0); + toast.success( + `Withdrawn ${amount} ${selectedMarketData.underlyingSymbol}` + ); + } + }); + + return { + isWaitingForIndexing, + withdrawAmount, + isPolling, + amount, + setAmount, + utilizationPercentage, + handleUtilization, + amountAsBInt + }; +}; diff --git a/packages/ui/hooks/useBalancePolling.ts b/packages/ui/hooks/useBalancePolling.ts new file mode 100644 index 000000000..23eec7459 --- /dev/null +++ b/packages/ui/hooks/useBalancePolling.ts @@ -0,0 +1,83 @@ +import { useEffect, useState } from 'react'; + +import { useQueryClient } from '@tanstack/react-query'; + +import type { Address } from 'viem'; + +interface UseBalancePollingProps { + address?: Address; + chainId: number; + txHash?: Address; + enabled: boolean; + onSuccess?: () => void; + interval?: number; + timeout?: number; +} + +export const useBalancePolling = ({ + address, + chainId, + txHash, + enabled, + onSuccess, + interval = 3000, + timeout = 30000 +}: UseBalancePollingProps) => { + const [isPolling, setIsPolling] = useState(false); + const queryClient = useQueryClient(); + + useEffect(() => { + if (!enabled || !txHash || !address) return; + + let timeoutId: NodeJS.Timeout; + let intervalId: NodeJS.Timeout; + + const startPolling = () => { + setIsPolling(true); + + intervalId = setInterval(async () => { + await queryClient.invalidateQueries({ queryKey: ['useFusePoolData'] }); + await queryClient.invalidateQueries({ + queryKey: ['useUpdatedUserAssets'] + }); + await queryClient.invalidateQueries({ + queryKey: ['useMaxSupplyAmount'] + }); + await queryClient.invalidateQueries({ + queryKey: ['useMaxWithdrawAmount'] + }); + await queryClient.invalidateQueries({ + queryKey: ['useMaxBorrowAmount'] + }); + await queryClient.invalidateQueries({ + queryKey: ['useMaxRepayAmount'] + }); + }, interval); + + timeoutId = setTimeout(() => { + clearInterval(intervalId); + setIsPolling(false); + onSuccess?.(); + }, timeout); + }; + + startPolling(); + + return () => { + clearInterval(intervalId); + clearTimeout(timeoutId); + setIsPolling(false); + }; + }, [ + address, + chainId, + txHash, + enabled, + interval, + timeout, + onSuccess, + queryClient + ]); + + return { isPolling }; +}; diff --git a/packages/ui/next.config.js b/packages/ui/next.config.js index 0538dc260..f0d3b10d1 100644 --- a/packages/ui/next.config.js +++ b/packages/ui/next.config.js @@ -1,6 +1,9 @@ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + images: { + domains: ['img.icons8.com'] + }, webpack: (config) => { config.resolve.fallback = { fs: false, net: false, tls: false }; config.externals.push('pino-pretty', 'lokijs', 'encoding'); diff --git a/packages/ui/package.json b/packages/ui/package.json index eb5af9ae0..1d75d1da6 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-hover-card": "^1.1.2", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-progress": "^1.1.0", diff --git a/packages/ui/store/Store.ts b/packages/ui/store/Store.ts index 2b44adbca..b4ba1f968 100644 --- a/packages/ui/store/Store.ts +++ b/packages/ui/store/Store.ts @@ -1,9 +1,30 @@ // import type { Dispatch, SetStateAction } from 'react'; import { create } from 'zustand'; -import type { BorrowPopoverProps } from '@ui/app/_components/markets/BorrowPopover'; -import type { SupplyPopoverProps } from '@ui/app/_components/markets/SupplyPopover'; -// import type { PopupMode } from '@ui/app/_components/popup/page'; +import type { Address } from 'viem'; + +import type { FlywheelReward } from '@ionicprotocol/types'; + +type SupplyPopoverProps = { + asset: string; + cToken: Address; + dropdownSelectedChain: number; + pool: Address; + selectedPoolId: string; + supplyAPR?: number; + rewards?: FlywheelReward[]; +}; + +type BorrowPopoverProps = { + dropdownSelectedChain: number; + borrowAPR?: number; + rewardsAPR?: number; + selectedPoolId: string; + asset: string; + cToken: Address; + pool: Address; + rewards?: FlywheelReward[]; +}; interface IFeaturedBorrow extends BorrowPopoverProps { // asset: string; diff --git a/packages/ui/utils/multipliers.ts b/packages/ui/utils/multipliers.ts index d817dbbc8..f00dd115b 100644 --- a/packages/ui/utils/multipliers.ts +++ b/packages/ui/utils/multipliers.ts @@ -13,6 +13,7 @@ export type Multipliers = { underlyingAPR?: number; op?: boolean; anzen?: number; + nektar?: boolean; }; export const multipliers: Record< diff --git a/yarn.lock b/yarn.lock index 4d89383d7..1d0a9eca7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2394,6 +2394,7 @@ __metadata: "@radix-ui/react-checkbox": "npm:^1.1.2" "@radix-ui/react-dialog": "npm:^1.1.2" "@radix-ui/react-dropdown-menu": "npm:^2.1.2" + "@radix-ui/react-hover-card": "npm:^1.1.2" "@radix-ui/react-icons": "npm:^1.3.0" "@radix-ui/react-popover": "npm:^1.1.2" "@radix-ui/react-progress": "npm:^1.1.0" @@ -5803,6 +5804,33 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-hover-card@npm:^1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-hover-card@npm:1.1.2" + dependencies: + "@radix-ui/primitive": "npm:1.1.0" + "@radix-ui/react-compose-refs": "npm:1.1.0" + "@radix-ui/react-context": "npm:1.1.1" + "@radix-ui/react-dismissable-layer": "npm:1.1.1" + "@radix-ui/react-popper": "npm:1.2.0" + "@radix-ui/react-portal": "npm:1.1.2" + "@radix-ui/react-presence": "npm:1.1.1" + "@radix-ui/react-primitive": "npm:2.0.0" + "@radix-ui/react-use-controllable-state": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10/6feb49b93426a5bb266026a329ff59a0ab13854e6018887dc40108f9af37c234703c29e626993656d560c993ee867cd446c716b763d328ee400cc09059f6b4ec + languageName: node + linkType: hard + "@radix-ui/react-icons@npm:^1.3.0": version: 1.3.1 resolution: "@radix-ui/react-icons@npm:1.3.1"