From 17089e41bec225f18e8508d45520dc084a5e2798 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Mon, 11 Nov 2024 16:32:11 +0100 Subject: [PATCH 01/66] set up market info --- packages/contracts/package.json | 2 +- .../ui/app/_components/markets/MarketInfo.tsx | 179 ++++++++++++++++++ .../app/_components/markets/WrapEthSwaps.tsx | 68 ------- .../app/_components/stake/BaseBreakdown.tsx | 40 ---- .../app/_components/stake/ModeBreakdown.tsx | 48 ----- .../ui/app/_components/stake/OPBreakdown.tsx | 40 ---- .../app/_components/stake/RewardDisplay.tsx | 114 +++++++++++ packages/ui/app/market/page.tsx | 1 - packages/ui/app/stake/page.tsx | 17 +- 9 files changed, 301 insertions(+), 208 deletions(-) create mode 100644 packages/ui/app/_components/markets/MarketInfo.tsx delete mode 100644 packages/ui/app/_components/markets/WrapEthSwaps.tsx delete mode 100644 packages/ui/app/_components/stake/BaseBreakdown.tsx delete mode 100644 packages/ui/app/_components/stake/ModeBreakdown.tsx delete mode 100644 packages/ui/app/_components/stake/OPBreakdown.tsx create mode 100644 packages/ui/app/_components/stake/RewardDisplay.tsx diff --git a/packages/contracts/package.json b/packages/contracts/package.json index e601de7c0..49b927025 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -55,4 +55,4 @@ "typescript": "^5.5.3", "viem": "^2.21.12" } -} \ No newline at end of file +} 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/WrapEthSwaps.tsx b/packages/ui/app/_components/markets/WrapEthSwaps.tsx deleted file mode 100644 index 21bd167f1..000000000 --- a/packages/ui/app/_components/markets/WrapEthSwaps.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* eslint-disable @next/next/no-img-element */ -'use client'; - -import type { Dispatch, SetStateAction } from 'react'; - -import dynamic from 'next/dynamic'; - -const SwapWidget = dynamic(() => import('./SwapWidget'), { - ssr: false -}); - -const WrapWidget = dynamic(() => import('./WrapWidget'), { - ssr: false -}); - -interface IProps { - dropdownSelectedChain: number; - setSwapWidgetOpen: Dispatch>; - swapWidgetOpen: boolean; - setWrapWidgetOpen: Dispatch>; - wrapWidgetOpen: boolean; -} -export default function WrapEthSwaps({ - dropdownSelectedChain, - setSwapWidgetOpen, - swapWidgetOpen, - setWrapWidgetOpen, - wrapWidgetOpen -}: IProps) { - return ( -
- - - setSwapWidgetOpen(false)} - open={swapWidgetOpen} - toChain={+dropdownSelectedChain} - /> - - - setWrapWidgetOpen(false)} - open={wrapWidgetOpen} - chain={+dropdownSelectedChain} - /> -
- ); -} 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 new file mode 100644 index 000000000..2b63b6cf6 --- /dev/null +++ b/packages/ui/app/_components/stake/RewardDisplay.tsx @@ -0,0 +1,114 @@ +import Image from 'next/image'; + +import { base, mode, optimism } from 'viem/chains'; + +import { BaseSugarAddress } from '@ui/constants/baselp'; +import { ModeSugarAddress } from '@ui/constants/lp'; +import { OPSugarAddress } from '@ui/constants/oplp'; +import useSugarAPR from '@ui/hooks/useSugarAPR'; + +type ChainConfig = { + poolIndex: bigint; + sugarAddress: `0x${string}`; + rewardToken: { + name: string; + logo: string; + }; +}; + +type ChainConfigs = { + [key: number]: { + defaultConfig: ChainConfig; + tokenConfigs?: { + [key: string]: ChainConfig; + }; + }; +}; + +export const CHAIN_CONFIGS: ChainConfigs = { + [mode.id]: { + defaultConfig: { + poolIndex: 6n, + sugarAddress: ModeSugarAddress, + rewardToken: { + name: 'Velodrome', + logo: '/img/symbols/32/color/velo.png' + } + }, + tokenConfigs: { + mode: { + poolIndex: 26n, + sugarAddress: ModeSugarAddress, + rewardToken: { + name: 'Velodrome', + logo: '/img/symbols/32/color/velo.png' + } + } + } + }, + [optimism.id]: { + defaultConfig: { + poolIndex: 910n, + sugarAddress: OPSugarAddress, + rewardToken: { + name: 'Velodrome', + logo: '/img/symbols/32/color/velo.png' + } + } + }, + [base.id]: { + defaultConfig: { + poolIndex: 1489n, + sugarAddress: BaseSugarAddress, + rewardToken: { + name: 'Aerodrome', + logo: '/img/logo/AERO.png' + } + } + } +}; + +type RewardDisplayProps = { + chainId: number; + isUnstaking: boolean; + selectedToken?: 'eth' | 'mode' | 'weth'; +}; + +export default function RewardDisplay({ + chainId, + isUnstaking, + selectedToken = 'eth' +}: RewardDisplayProps) { + const chainConfig = CHAIN_CONFIGS[chainId]; + + 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; + + return ( +
+ {`${config.rewardToken.name} + {config.rewardToken.name} APR + + {apr} + +
+ ); +} diff --git a/packages/ui/app/market/page.tsx b/packages/ui/app/market/page.tsx index c94c72cc6..d46141680 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -139,7 +139,6 @@ export default function Market() { /> - {/* //............................................ */}
{/* breakdowns */} - {+chain === mode.id && ( - )} - {+chain === optimism.id && ( - - )} - {+chain === base.id && }
+ + setSwapWidgetOpen(false)} + open={swapWidgetOpen} + toChain={+dropdownSelectedChain} + /> + + + setWrapWidgetOpen(false)} + open={wrapWidgetOpen} + chain={+dropdownSelectedChain} + /> +
+ ); +} diff --git a/packages/ui/app/_components/stake/RewardDisplay.tsx b/packages/ui/app/_components/stake/RewardDisplay.tsx index 2b63b6cf6..8609ef699 100644 --- a/packages/ui/app/_components/stake/RewardDisplay.tsx +++ b/packages/ui/app/_components/stake/RewardDisplay.tsx @@ -70,13 +70,13 @@ export const CHAIN_CONFIGS: ChainConfigs = { type RewardDisplayProps = { chainId: number; - isUnstaking: boolean; + isUnstaking?: boolean; selectedToken?: 'eth' | 'mode' | 'weth'; }; export default function RewardDisplay({ chainId, - isUnstaking, + isUnstaking = false, selectedToken = 'eth' }: RewardDisplayProps) { const chainConfig = CHAIN_CONFIGS[chainId]; From fde2aa1c8cb05330639e9968fe27789e5c36354a Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Wed, 13 Nov 2024 17:23:23 +0700 Subject: [PATCH 03/66] migrate custom market table to CommonTable --- packages/ui/app/_components/CommonTable.tsx | 35 +- .../app/_components/dashboards/InfoRows.tsx | 95 ++-- .../ui/app/_components/markets/APRCell.tsx | 342 ++++++++++++++ .../app/_components/markets/BorrowPopover.tsx | 215 --------- .../ui/app/_components/markets/PoolRows.tsx | 407 ----------------- .../app/_components/markets/SupplyPopover.tsx | 311 ------------- packages/ui/app/market/page.tsx | 423 ++++++++---------- packages/ui/components/ui/hover-card.tsx | 40 ++ packages/ui/hooks/market/useMarketData.ts | 201 +++++++++ packages/ui/next.config.js | 3 + packages/ui/package.json | 1 + yarn.lock | 28 ++ 12 files changed, 854 insertions(+), 1247 deletions(-) create mode 100644 packages/ui/app/_components/markets/APRCell.tsx delete mode 100644 packages/ui/app/_components/markets/BorrowPopover.tsx delete mode 100644 packages/ui/app/_components/markets/PoolRows.tsx delete mode 100644 packages/ui/app/_components/markets/SupplyPopover.tsx create mode 100644 packages/ui/components/ui/hover-card.tsx create mode 100644 packages/ui/hooks/market/useMarketData.ts diff --git a/packages/ui/app/_components/CommonTable.tsx b/packages/ui/app/_components/CommonTable.tsx index 07b1c44bf..ab0c0f377 100644 --- a/packages/ui/app/_components/CommonTable.tsx +++ b/packages/ui/app/_components/CommonTable.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react'; import { useState } from 'react'; import { @@ -18,7 +19,7 @@ import { import { TableLoader } from './TableLoader'; -import type { ColumnDef, SortingState } from '@tanstack/react-table'; +import type { ColumnDef, Row, SortingState } from '@tanstack/react-table'; interface CommonTableProps { data: T[]; @@ -26,6 +27,7 @@ interface CommonTableProps { isLoading?: boolean; loadingRows?: number; showFooter?: boolean; + renderRow?: (row: Row) => ReactNode; } function CommonTable({ @@ -33,7 +35,8 @@ function CommonTable({ columns, isLoading = false, loadingRows = 5, - showFooter = false + showFooter = false, + renderRow }: CommonTableProps) { const [sorting, setSorting] = useState([]); @@ -84,18 +87,22 @@ function CommonTable({ {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) + table.getRowModel().rows.map((row) => + renderRow ? ( + renderRow(row) + ) : ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ) + ) ) : ( import('../markets/FlyWheelRewards'), { ssr: false }); -import BorrowPopover from '../markets/BorrowPopover'; -import SupplyPopover from '../markets/SupplyPopover'; +import APRCell from '../markets/APRCell'; import { PopupMode } from '../popup/page'; import type { Address } from 'viem'; @@ -87,12 +86,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,93 +96,71 @@ 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 ? ( diff --git a/packages/ui/app/_components/markets/APRCell.tsx b/packages/ui/app/_components/markets/APRCell.tsx new file mode 100644 index 000000000..d0fb7c7e1 --- /dev/null +++ b/packages/ui/app/_components/markets/APRCell.tsx @@ -0,0 +1,342 @@ +// components/APRCell.tsx +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 type { Address } from 'viem'; + +import type { FlywheelReward } from '@ionicprotocol/types'; + +const Rewards = 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 formatBaseAPR = () => { + if (type === 'borrow' && baseAPR > 0) + return ( + '-' + baseAPR.toLocaleString('en-US', { maximumFractionDigits: 2 }) + ); + return ( + (type === 'supply' ? '+' : '') + + baseAPR.toLocaleString('en-US', { maximumFractionDigits: 2 }) + ); + }; + + const formatTotalAPR = () => { + if (typeof aprTotal === 'undefined') return '-'; + const prefix = type === 'supply' || aprTotal > 0 ? '+' : ''; + return ( + prefix + + aprTotal.toLocaleString('en-US', { + maximumFractionDigits: type === 'supply' ? 2 : 1 + }) + ); + }; + + return ( + + +
+ {formatTotalAPR()}% +
+ + + ION APR + + + {showRewardsBadge && (!isMainModeMarket || type === 'supply') && ( + + {isMainModeMarket && type === 'supply' ? ( +
+ +{' '} + OP{' '} + REWARDS +
+ ) : ( + '+ REWARDS' + )} +
+ )} + + {config?.turtle && !isMainModeMarket && ( + + + + TURTLE{' '} + external-link + + + )} +
+
+
+ +
+
+ Base APR: {formatBaseAPR()}% +
+ + {type === 'supply' && isMainModeMarket && ( +
+ OP + + + OP Rewards:{' '} + {merklAprForToken?.toLocaleString('en-US', { + maximumFractionDigits: 2 + })} + % + +
+ )} + + {config?.underlyingAPR && ( +

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

+ )} + + {config?.flywheel && ( + + )} + + {/* Common rewards sections */} + {(config?.ionic ?? 0) > 0 && ( + <> +
+ + + {config?.ionic}x Ionic Points +
+
+ + + Turtle Ionic Points +
+ + )} + + {config?.turtle && asset === 'STONE' && ( +
+ + + Stone Turtle Points +
+ )} + + {config?.etherfi && ( +
+ + + {config.etherfi}x ether.fi Points +
+ )} + + {config?.kelp && ( + <> +
+ + + {config.kelp}x Kelp Miles +
+
+ + + Turtle Kelp Points +
+ + )} + + {config?.eigenlayer && ( +
+ + + EigenLayer Points +
+ )} + + {config?.spice && ( +
+ + + Spice Points +
+ )} + + {/* Supply-specific rewards */} + {type === 'supply' && ( + <> + {(config?.anzen ?? 0) > 0 && ( +
+ + + {config?.anzen}x Anzen Points +
+ )} + + {config?.nektar && ( +
+ + + Nektar Points +
+ )} + + )} +
+
+
+ ); +} 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/PoolRows.tsx b/packages/ui/app/_components/markets/PoolRows.tsx deleted file mode 100644 index 98ed0739f..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: - - {collateralFactor}% -

-
-
- {/* */} - - -
- {/* {!address && ( -
- -
- )} */} -
-
- ); -}; - -export default PoolRows; diff --git a/packages/ui/app/_components/markets/SupplyPopover.tsx b/packages/ui/app/_components/markets/SupplyPopover.tsx deleted file mode 100644 index 6fb877922..000000000 --- a/packages/ui/app/_components/markets/SupplyPopover.tsx +++ /dev/null @@ -1,311 +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 && ( - - {isMainModeMarket ? ( - <> - +{' '} - OP{' '} - REWARDS{' '} - - ) : ( - '+ REWARDS ' - )} - i - - )} - - {supplyConfig?.turtle && !isMainModeMarket && ( - - - + TURTLE{' '} - external-link - - - )} -
-
- - Base APR: + - {typeof supplyAPR !== 'undefined' - ? supplyAPR.toLocaleString('en-US', { maximumFractionDigits: 2 }) - : '-'} - % - -
- {isMainModeMarket && ( -
- 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 -
- )} - {supplyConfig?.nektar && ( -
- {' '} - + Nektar Points -
- )} -
- - ); -} diff --git a/packages/ui/app/market/page.tsx b/packages/ui/app/market/page.tsx index d46141680..ba60249d9 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -1,53 +1,40 @@ -/* eslint-disable @next/next/no-img-element */ +// Market.tsx '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 { useState } from 'react'; import dynamic from 'next/dynamic'; +import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; -import { type Address, formatEther, formatUnits } from 'viem'; +import { type ColumnDef } from '@tanstack/react-table'; 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 APRCell from '../_components/markets/APRCell'; import FeaturedMarketTile from '../_components/markets/FeaturedMarketTile'; -import PoolRows from '../_components/markets/PoolRows'; 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 Popup, { PopupMode } 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'; const NetworkSelector = dynamic( () => import('../_components/markets/NetworkSelector'), { ssr: false } ); +const PoolToggle = dynamic(() => import('../_components/markets/PoolToggle'), { + ssr: false +}); + export default function Market() { const searchParams = useSearchParams(); const querychain = searchParams.get('chain'); @@ -55,74 +42,175 @@ export default function Market() { 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 { address } = useMultiIonic(); const selectedPool = querypool ?? '0'; - const chain = querychain ? querychain : mode.id; - const { data: poolData, isLoading: isLoadingPoolData } = useFusePoolData( - selectedPool, - +chain - ); - - useEffect(() => { - if (!chain) return; - setDropdownSelectedChain(+chain); - }, [chain]); - - const assets = useMemo( - () => poolData?.assets, - [poolData] - ); + const chain = querychain ? querychain : mode.id.toString(); + const [selectedSymbol, setSelectedSymbol] = useState(); - const { data: borrowRates } = useBorrowAPYs( - assets ?? [], - dropdownSelectedChain + const { marketData, isLoading, poolData } = useMarketData( + selectedPool, + chain ); - const { data: supplyRates } = useSupplyAPYs( - assets ?? [], - dropdownSelectedChain - ); + const columns: ColumnDef[] = [ + { + accessorKey: 'asset', + header: 'ASSETS', + cell: ({ row }) => ( + + {row.original.asset} + {row.original.asset} + + ) + }, + { + accessorKey: 'supplyBalance', + header: 'SUPPLY BALANCE', + cell: ({ row }) => ( + {row.original.supplyBalance} + ) + }, + { + accessorKey: 'totalSupplied', + header: 'TOTAL SUPPLIED', + cell: ({ row }) => ( + {row.original.totalSupplied} + ) + }, + { + accessorKey: 'borrowBalance', + header: 'BORROW BALANCE', + cell: ({ row }) => ( + {row.original.borrowBalance} + ) + }, + { + accessorKey: 'totalBorrowing', + header: 'TOTAL BORROWED', + cell: ({ row }) => ( + {row.original.totalBorrowing} + ) + }, + { + accessorKey: 'supplyAPRTotal', + header: 'SUPPLY APR', + cell: ({ row }) => ( + + ) + }, + { + accessorKey: 'borrowAPRTotal', + header: 'BORROW APR', + cell: ({ row }) => ( + + ) + }, + { + accessorKey: 'collateralFactor', + header: 'COLLATERAL FACTOR', + cell: ({ row }) => {row.original.collateralFactor}% + }, + { + id: 'actions', + header: 'SUPPLY/BORROW', + cell: ({ row }) => ( +
+ + +
+ ) + } + ]; - const [selectedSymbol, setSelectedSymbol] = useState(); - const selectedMarketData = useMemo( - () => - poolData?.assets.find( - (_asset) => _asset.underlyingSymbol === selectedSymbol - ), - [selectedSymbol, poolData] + const selectedMarketData = marketData.find( + (asset) => asset.asset === selectedSymbol ); - const { data: loopMarkets, isLoading: isLoadingLoopMarkets } = useLoopMarkets( - poolData?.assets.map((asset) => asset.cToken) ?? [], - +chain - ); - - const { data: rewards } = useRewards({ - chainId: dropdownSelectedChain, - poolId: selectedPool - }); - // const { data: alltvl } = useAllTvlAcrossChain(); - // console.log(alltvl); return ( <> -
- {/* //........ */} -
-
+
+
+
@@ -130,16 +218,17 @@ export default function Market() { setPopupMode={setPopupMode} setSelectedSymbol={setSelectedSymbol} selectedChain={chainId} - isLoadingPoolData={isLoadingPoolData} + isLoadingPoolData={isLoading} setSwapWidgetOpen={setSwapWidgetOpen} swapWidgetOpen={swapWidgetOpen} - dropdownSelectedChain={dropdownSelectedChain.toString()} + dropdownSelectedChain={chain} setWrapWidgetOpen={setWrapWidgetOpen} wrapWidgetOpen={wrapWidgetOpen} />
-
+ +
-
-
+ +
+
- - - <> - {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 - } - )}`} - /> - ); - } - )} - - + +
+ {popupMode && selectedMarketData && poolData && ( setPopupMode(undefined)} comptrollerAddress={poolData.comptroller} - loopMarkets={loopMarkets} mode={popupMode} selectedMarketData={selectedMarketData} /> @@ -326,7 +273,7 @@ export default function Market() { {swapOpen && ( setSwapOpen(false)} - dropdownSelectedChain={dropdownSelectedChain} + dropdownSelectedChain={+chain} selectedChain={chainId} /> )} 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/hooks/market/useMarketData.ts b/packages/ui/hooks/market/useMarketData.ts new file mode 100644 index 000000000..b0ec4688e --- /dev/null +++ b/packages/ui/hooks/market/useMarketData.ts @@ -0,0 +1,201 @@ +// hooks/useMarketData.ts +import { useMemo } from 'react'; + +import { type Address, formatEther, formatUnits } from 'viem'; + +import { FLYWHEEL_TYPE_MAP, pools } 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 type { MarketData } from '@ui/types/TokensDataMap'; + +import type { FlywheelReward } from '@ionicprotocol/types'; + +export type MarketRowData = MarketData & { + asset: string; + logo: string; + supplyBalance: string; + totalSupplied: string; + borrowBalance: string; + totalBorrowing: 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; +}; + +export const useMarketData = (selectedPool: string, chain: number | string) => { + const { data: poolData, isLoading: isLoadingPoolData } = useFusePoolData( + selectedPool, + +chain + ); + + 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 marketData = useMemo(() => { + if (!assets) return []; + + return 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 + ) + ); + + const borrowRewards = rewards?.[asset.cToken]?.filter((reward) => + FLYWHEEL_TYPE_MAP[+chain]?.borrow?.includes( + (reward as FlywheelReward).flywheel + ) + ); + + 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]; + + return { + ...asset, + asset: asset.underlyingSymbol, + logo: `/img/symbols/32/color/${asset.underlyingSymbol.toLowerCase()}.png`, + supplyBalance: `${ + typeof asset.supplyBalance === 'bigint' + ? parseFloat( + formatUnits(asset.supplyBalance, asset.underlyingDecimals) + ).toLocaleString('en-US', { maximumFractionDigits: 2 }) + : '-' + } ${asset.underlyingSymbol} / $${asset.supplyBalanceFiat.toLocaleString( + 'en-US', + { maximumFractionDigits: 2 } + )}`, + totalSupplied: `${ + asset.totalSupplyNative + ? parseFloat( + formatUnits(asset.totalSupply, asset.underlyingDecimals) + ).toLocaleString('en-US', { maximumFractionDigits: 2 }) + : '0' + } ${asset.underlyingSymbol} / $${asset.totalSupplyFiat.toLocaleString( + 'en-US', + { maximumFractionDigits: 2 } + )}`, + borrowBalance: `${ + typeof asset.borrowBalance === 'bigint' + ? parseFloat( + formatUnits(asset.borrowBalance, asset.underlyingDecimals) + ).toLocaleString('en-US', { maximumFractionDigits: 2 }) + : '-' + } ${asset.underlyingSymbol} / $${asset.borrowBalanceFiat.toLocaleString( + 'en-US', + { maximumFractionDigits: 2 } + )}`, + totalBorrowing: `${ + asset.totalBorrowNative + ? parseFloat( + formatUnits(asset.totalBorrow, asset.underlyingDecimals) + ).toLocaleString('en-US', { maximumFractionDigits: 2 }) + : '0' + } ${asset.underlyingSymbol} / $${asset.totalBorrowFiat.toLocaleString( + 'en-US', + { maximumFractionDigits: 2 } + )}`, + 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[]; + }, [ + assets, + chain, + selectedPool, + rewards, + merklApr, + supplyRates, + borrowRates, + loopMarkets, + poolData?.comptroller, + borrowCapsData + ]); + + return { + marketData, + isLoading: isLoadingPoolData || isLoadingLoopMarkets, + poolData + }; +}; 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 cda16a3b3..6f45414fe 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -19,6 +19,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/yarn.lock b/yarn.lock index a8d0dd3c5..4da69f8ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2196,6 +2196,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" @@ -5412,6 +5413,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" From 0620a2ad4f88f5fd69e5378580f12b88d3b62753 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Wed, 13 Nov 2024 17:52:00 +0700 Subject: [PATCH 04/66] fix ion apr --- .../ui/app/_components/markets/APRCell.tsx | 2 ++ packages/ui/hooks/market/useMarketData.ts | 28 +++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/ui/app/_components/markets/APRCell.tsx b/packages/ui/app/_components/markets/APRCell.tsx index 40dba0e6a..1e3c2877d 100644 --- a/packages/ui/app/_components/markets/APRCell.tsx +++ b/packages/ui/app/_components/markets/APRCell.tsx @@ -43,6 +43,7 @@ export default function APRCell({ selectedPoolId, rewards }: APRCellProps) { + console.log('rewards', rewards); const isMainModeMarket = dropdownSelectedChain === 34443 && (asset === 'USDC' || asset === 'WETH') && @@ -76,6 +77,7 @@ export default function APRCell({ ); }; + console.log('aprTotal', aprTotal); const formatTotalAPR = () => { if (typeof aprTotal === 'undefined') return '-'; const prefix = type === 'supply' || aprTotal > 0 ? '+' : ''; diff --git a/packages/ui/hooks/market/useMarketData.ts b/packages/ui/hooks/market/useMarketData.ts index b0ec4688e..6b7eaf0c2 100644 --- a/packages/ui/hooks/market/useMarketData.ts +++ b/packages/ui/hooks/market/useMarketData.ts @@ -81,17 +81,27 @@ export const useMarketData = (selectedPool: string, chain: number | 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 + const supplyRewards = rewards?.[asset.cToken] + ?.filter((reward) => + FLYWHEEL_TYPE_MAP[+chain]?.supply?.includes( + (reward as FlywheelReward).flywheel + ) ) - ); - - 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 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() From 4a44489a7c328a4be5e653bf56a4a66b1fb1a7e5 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Fri, 15 Nov 2024 11:50:15 +0700 Subject: [PATCH 05/66] order columns & add sorting --- .../ui/app/_components/markets/APRCell.tsx | 12 +- packages/ui/app/market/page.tsx | 170 +++++++++++++----- packages/ui/hooks/market/useMarketData.ts | 4 +- 3 files changed, 137 insertions(+), 49 deletions(-) diff --git a/packages/ui/app/_components/markets/APRCell.tsx b/packages/ui/app/_components/markets/APRCell.tsx index 1e3c2877d..0bfafeebe 100644 --- a/packages/ui/app/_components/markets/APRCell.tsx +++ b/packages/ui/app/_components/markets/APRCell.tsx @@ -43,7 +43,6 @@ export default function APRCell({ selectedPoolId, rewards }: APRCellProps) { - console.log('rewards', rewards); const isMainModeMarket = dropdownSelectedChain === 34443 && (asset === 'USDC' || asset === 'WETH') && @@ -77,13 +76,12 @@ export default function APRCell({ ); }; - console.log('aprTotal', aprTotal); const formatTotalAPR = () => { - if (typeof aprTotal === 'undefined') return '-'; - const prefix = type === 'supply' || aprTotal > 0 ? '+' : ''; + const numericValue = aprTotal ?? 0; + const prefix = type === 'supply' || numericValue > 0 ? '+' : ''; return ( prefix + - aprTotal.toLocaleString('en-US', { + numericValue.toLocaleString('en-US', { maximumFractionDigits: type === 'supply' ? 2 : 1 }) ); @@ -92,9 +90,9 @@ export default function APRCell({ return ( -
+
{formatTotalAPR()}% -
+
[] = [ { accessorKey: 'asset', - header: 'ASSETS', + header: ({ column }) => ( + + ), cell: ({ row }) => ( {row.original.asset} - ) - }, - { - accessorKey: 'supplyBalance', - header: 'SUPPLY BALANCE', - cell: ({ row }) => ( - {row.original.supplyBalance} - ) - }, - { - accessorKey: 'totalSupplied', - header: 'TOTAL SUPPLIED', - cell: ({ row }) => ( - {row.original.totalSupplied} - ) - }, - { - accessorKey: 'borrowBalance', - header: 'BORROW BALANCE', - cell: ({ row }) => ( - {row.original.borrowBalance} - ) - }, - { - accessorKey: 'totalBorrowing', - header: 'TOTAL BORROWED', - cell: ({ row }) => ( - {row.original.totalBorrowing} - ) + ), + sortingFn: 'alphanumeric' }, { accessorKey: 'supplyAPRTotal', - header: 'SUPPLY APR', + header: ({ column }) => ( + + ), cell: ({ row }) => ( - ) + ), + sortingFn: (a, b) => + (b.original.supplyAPRTotal ?? 0) - (a.original.supplyAPRTotal ?? 0) }, { accessorKey: 'borrowAPRTotal', - header: 'BORROW APR', + header: ({ column }) => ( + + ), cell: ({ row }) => ( - ) + ), + sortingFn: (a, b) => + (a.original.borrowAPRTotal ?? 0) - (b.original.borrowAPRTotal ?? 0) + }, + { + accessorKey: 'supplyBalance', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + {row.original.supplyBalance} + ), + sortingFn: (a, b) => { + const aValue = + parseFloat( + (a.original.supplyBalance as string).replace(/[^0-9.-]+/g, '') + ) || 0; + const bValue = + parseFloat( + (b.original.supplyBalance as string).replace(/[^0-9.-]+/g, '') + ) || 0; + return aValue > bValue ? 1 : aValue < bValue ? -1 : 0; + } + }, + { + accessorKey: 'borrowBalance', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + {row.original.borrowBalance} + ), + sortingFn: (a, b) => { + const aValue = + parseFloat( + (a.original.borrowBalance as string).replace(/[^0-9.-]+/g, '') + ) || 0; + const bValue = + parseFloat( + (b.original.borrowBalance as string).replace(/[^0-9.-]+/g, '') + ) || 0; + return aValue > bValue ? 1 : aValue < bValue ? -1 : 0; + } }, { accessorKey: 'collateralFactor', - header: 'COLLATERAL FACTOR', - cell: ({ row }) => {row.original.collateralFactor}% + header: ({ column }) => ( + + ), + cell: ({ row }) => {row.original.collateralFactor}%, + sortingFn: (a, b) => { + const aValue = parseFloat(a.original.collateralFactor) || 0; + const bValue = parseFloat(b.original.collateralFactor) || 0; + return aValue > bValue ? 1 : aValue < bValue ? -1 : 0; + } }, { id: 'actions', diff --git a/packages/ui/hooks/market/useMarketData.ts b/packages/ui/hooks/market/useMarketData.ts index 6b7eaf0c2..6e81e700c 100644 --- a/packages/ui/hooks/market/useMarketData.ts +++ b/packages/ui/hooks/market/useMarketData.ts @@ -127,7 +127,7 @@ export const useMarketData = (selectedPool: string, chain: number | string) => { ? parseFloat( formatUnits(asset.supplyBalance, asset.underlyingDecimals) ).toLocaleString('en-US', { maximumFractionDigits: 2 }) - : '-' + : '0' } ${asset.underlyingSymbol} / $${asset.supplyBalanceFiat.toLocaleString( 'en-US', { maximumFractionDigits: 2 } @@ -147,7 +147,7 @@ export const useMarketData = (selectedPool: string, chain: number | string) => { ? parseFloat( formatUnits(asset.borrowBalance, asset.underlyingDecimals) ).toLocaleString('en-US', { maximumFractionDigits: 2 }) - : '-' + : '0' } ${asset.underlyingSymbol} / $${asset.borrowBalanceFiat.toLocaleString( 'en-US', { maximumFractionDigits: 2 } From 66f3550d8250c14c42c3ead5412b22cdb9d75ce0 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Sun, 17 Nov 2024 14:54:15 +0700 Subject: [PATCH 06/66] simplify table config --- packages/ui/app/_components/CommonTable.tsx | 218 +++++++----- .../app/_components/markets/SupplyPopover.tsx | 311 ------------------ packages/ui/app/market/page.tsx | 177 +++------- packages/ui/store/Store.ts | 27 +- 4 files changed, 213 insertions(+), 520 deletions(-) delete mode 100644 packages/ui/app/_components/markets/SupplyPopover.tsx diff --git a/packages/ui/app/_components/CommonTable.tsx b/packages/ui/app/_components/CommonTable.tsx index ab0c0f377..b445a9f72 100644 --- a/packages/ui/app/_components/CommonTable.tsx +++ b/packages/ui/app/_components/CommonTable.tsx @@ -1,11 +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 { @@ -17,104 +25,164 @@ import { TableRow } from '@ui/components/ui/table'; -import { TableLoader } from './TableLoader'; +import ResultHandler from './ResultHandler'; + +// Sorting functions +export const sortingFunctions = { + numerical: (a: any, b: any) => { + const aValue = parseFloat(String(a).replace(/[^0-9.-]+/g, '')) || 0; + const bValue = parseFloat(String(b).replace(/[^0-9.-]+/g, '')) || 0; + return aValue > bValue ? 1 : aValue < bValue ? -1 : 0; + }, + 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 ? 1 : aValue < bValue ? -1 : 0; + } +}; -import type { ColumnDef, Row, SortingState } from '@tanstack/react-table'; +type SortingType = keyof typeof sortingFunctions; + +export type EnhancedColumnDef = Omit, 'sortingFn'> & { + id: string; + header: string; + sortingFn?: SortingType | ((rowA: any, rowB: any) => number); + enableSorting?: boolean; +}; interface CommonTableProps { data: T[]; - columns: ColumnDef[]; + columns: EnhancedColumnDef[]; isLoading?: boolean; - loadingRows?: number; - showFooter?: boolean; renderRow?: (row: Row) => ReactNode; } -function CommonTable({ +const SortableHeader = ({ + column, + children +}: { + column: any; + children: ReactNode; +}) => { + const isSortable = column.getCanSort(); + const sorted = column.getIsSorted(); + + return ( + + ); +}; + +function CommonTable({ data, columns, isLoading = false, - loadingRows = 5, - showFooter = false, renderRow }: CommonTableProps) { const [sorting, setSorting] = useState([]); + const processedColumns = columns.map( + (col): ColumnDef => ({ + ...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.original[col.id], rowB.original[col.id]); + } + : col.sortingFn + }) + ); + const table = useReactTable({ data, - columns, + columns: processedColumns, getCoreRowModel: getCoreRowModel(), onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(), - state: { - sorting - } + state: { sorting } }); - 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) => - renderRow ? ( - renderRow(row) - ) : ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - ) - ) - ) : ( - - +
+ + {table.getHeaderGroups().map((headerGroup) => ( + - No results. - - - )} - -
+ {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => + renderRow ? ( + renderRow(row) + ) : ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ) + ) + ) : ( + + + No results. + + + )} + + + ); } diff --git a/packages/ui/app/_components/markets/SupplyPopover.tsx b/packages/ui/app/_components/markets/SupplyPopover.tsx deleted file mode 100644 index 0ae0deca7..000000000 --- a/packages/ui/app/_components/markets/SupplyPopover.tsx +++ /dev/null @@ -1,311 +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 || asset === 'dMBTC' ? ( - <> - +{' '} - OP{' '} - REWARDS{' '} - - ) : ( - '+ REWARDS ' - )} - i - - )} - - {supplyConfig?.turtle && !isMainModeMarket && ( - - - + TURTLE{' '} - external-link - - - )} -
-
- - Base APR: + - {typeof supplyAPR !== 'undefined' - ? supplyAPR.toLocaleString('en-US', { maximumFractionDigits: 2 }) - : '-'} - % - -
- {(merklAprForToken || asset === 'dMBTC') && ( -
- 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 -
- )} - {supplyConfig?.nektar && ( -
- {' '} - + Nektar Points -
- )} -
- - ); -} diff --git a/packages/ui/app/market/page.tsx b/packages/ui/app/market/page.tsx index 6ce185bf5..519dbc33b 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -4,10 +4,10 @@ import { 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 ColumnDef } from '@tanstack/react-table'; import { mode } from 'viem/chains'; import { useChainId } from 'wagmi'; @@ -26,6 +26,9 @@ import TvlTile from '../_components/markets/TvlTile'; import Popup, { PopupMode } from '../_components/popup/page'; import Swap from '../_components/popup/Swap'; +import type { EnhancedColumnDef } from '../_components/CommonTable'; +import type { Row } from '@tanstack/react-table'; + const NetworkSelector = dynamic( () => import('../_components/markets/NetworkSelector'), { ssr: false } @@ -35,6 +38,11 @@ const PoolToggle = dynamic(() => import('../_components/markets/PoolToggle'), { ssr: false }); +interface MarketCellProps { + row: Row; + getValue: () => any; +} + export default function Market() { const searchParams = useSearchParams(); const querychain = searchParams.get('chain'); @@ -55,23 +63,12 @@ export default function Market() { chain ); - const columns: ColumnDef[] = [ + const columns: EnhancedColumnDef[] = [ { - accessorKey: 'asset', - header: ({ column }) => ( - - ), - cell: ({ row }) => ( + id: 'asset', + header: 'ASSETS', + sortingFn: 'alphabetical', + cell: ({ row }: MarketCellProps) => ( - {row.original.asset} {row.original.asset} - ), - sortingFn: 'alphanumeric' + ) }, { - accessorKey: 'supplyAPRTotal', - header: ({ column }) => ( - - ), - cell: ({ row }) => ( + id: 'supplyAPRTotal', + header: 'SUPPLY APR', + sortingFn: 'numerical', + cell: ({ row }: MarketCellProps) => ( - ), - sortingFn: (a, b) => - (b.original.supplyAPRTotal ?? 0) - (a.original.supplyAPRTotal ?? 0) + ) }, { - accessorKey: 'borrowAPRTotal', - header: ({ column }) => ( - - ), - cell: ({ row }) => ( + id: 'borrowAPRTotal', + header: 'BORROW APR', + sortingFn: 'numerical', + cell: ({ row }: MarketCellProps) => ( - ), - sortingFn: (a, b) => - (a.original.borrowAPRTotal ?? 0) - (b.original.borrowAPRTotal ?? 0) + ) }, { - accessorKey: 'supplyBalance', - header: ({ column }) => ( - - ), - cell: ({ row }) => ( + id: 'supplyBalance', + header: 'SUPPLY BALANCE', + sortingFn: 'numerical', + cell: ({ row }: MarketCellProps) => ( {row.original.supplyBalance} - ), - sortingFn: (a, b) => { - const aValue = - parseFloat( - (a.original.supplyBalance as string).replace(/[^0-9.-]+/g, '') - ) || 0; - const bValue = - parseFloat( - (b.original.supplyBalance as string).replace(/[^0-9.-]+/g, '') - ) || 0; - return aValue > bValue ? 1 : aValue < bValue ? -1 : 0; - } + ) }, { - accessorKey: 'borrowBalance', - header: ({ column }) => ( - - ), - cell: ({ row }) => ( + id: 'borrowBalance', + header: 'BORROW BALANCE', + sortingFn: 'numerical', + cell: ({ row }: MarketCellProps) => ( {row.original.borrowBalance} - ), - sortingFn: (a, b) => { - const aValue = - parseFloat( - (a.original.borrowBalance as string).replace(/[^0-9.-]+/g, '') - ) || 0; - const bValue = - parseFloat( - (b.original.borrowBalance as string).replace(/[^0-9.-]+/g, '') - ) || 0; - return aValue > bValue ? 1 : aValue < bValue ? -1 : 0; - } + ) }, { - accessorKey: 'collateralFactor', - header: ({ column }) => ( - - ), - cell: ({ row }) => {row.original.collateralFactor}%, - sortingFn: (a, b) => { - const aValue = parseFloat(a.original.collateralFactor) || 0; - const bValue = parseFloat(b.original.collateralFactor) || 0; - return aValue > bValue ? 1 : aValue < bValue ? -1 : 0; - } + id: 'collateralFactor', + header: 'COLLATERAL FACTOR', + sortingFn: 'percentage', + cell: ({ row }: MarketCellProps) => ( + {row.original.collateralFactor}% + ) }, { id: 'actions', header: 'SUPPLY/BORROW', - cell: ({ row }) => ( + enableSorting: false, + cell: ({ row }: MarketCellProps) => (
+ {loopPossible && ( + + )} +
+ + + + + +
+
+ COLLATERAL APR + {updatedValues.collateralApr}% +
+ +
+ Market Supply Balance +
+ {updatedValues.balanceFrom} + → + + {updatedValues.balanceTo} + +
+
+ +
+ Market Supply APR +
+ {updatedValues.aprFrom}% + → + + {updatedValues.aprTo}% + +
+
+ +
+ + Enable Collateral + + 0 || !selectedMarketData.supplyBalance + } + /> +
+
+ + {totalStats && ( +
+
+ +
+
+
Total Supplied:
+
+ + {millify(totalStats.totalAmount)} of{' '} + {millify(totalStats.capAmount)}{' '} + {selectedMarketData.underlyingSymbol} + +
+
+ ${millify(totalStats.totalFiat)} of ${millify(totalStats.capFiat)} +
+
+
+ )} + + {transactionSteps.length > 0 ? ( + + ) : ( + + )} +
+ ); +}; + +export const WithdrawTab = ({ + isLoadingUpdatedAssets, + selectedMarketData, + amount, + setAmount, + currentUtilizationPercentage, + handleUtilization, + hfpStatus, + maxAmount, + isLoadingMax, + transactionSteps, + resetTransactionSteps, + chainId, + onAction, + isDisabled, + updatedValues, + healthFactor +}: WithdrawTabProps) => { + return ( +
+ + + + + {hfpStatus === 'WARNING' && ( + + + You are close to the liquidation threshold. Manage your health + factor carefully. + + + )} + + {hfpStatus === 'CRITICAL' && ( + + Health factor too low. + + )} + + {hfpStatus === 'UNKNOWN' && ( + + + Unable to calculate health factor. + + + )} + +
+
+ Market Supply Balance +
+ {updatedValues.balanceFrom} + → + + {updatedValues.balanceTo} + +
+
+ +
+ Market Supply APR +
+ {updatedValues.aprFrom}% + → + + {updatedValues.aprTo}% + +
+
+ +
+ Health Factor +
+ {healthFactor.current} + → + + {healthFactor.predicted} + +
+
+
+ + {transactionSteps.length > 0 ? ( + + ) : ( + + )} +
+ ); +}; + +export const BorrowTab = ({ + isLoadingUpdatedAssets, + selectedMarketData, + amount, + setAmount, + currentUtilizationPercentage, + handleUtilization, + hfpStatus, + maxAmount, + isLoadingMax, + transactionSteps, + resetTransactionSteps, + chainId, + onAction, + isDisabled, + updatedValues, + borrowLimits, + totalStats, + healthFactor +}: BorrowTabProps) => { + const isUnderMinBorrow = + amount && + borrowLimits.min && + parseFloat(amount) < parseFloat(borrowLimits.min); + + return ( +
+ + + + + {isUnderMinBorrow && ( + + + Amount must be greater than minimum borrow amount ( + {borrowLimits.min} {selectedMarketData.underlyingSymbol}) + + + )} + + {hfpStatus === 'UNKNOWN' && ( + + + Unable to calculate health factor. + + + )} + + {hfpStatus === 'WARNING' && ( + + + You are close to the liquidation threshold. Manage your health + factor carefully. + + + )} + + {hfpStatus === 'CRITICAL' && ( + + Health factor too low. + + )} + +
+
+ MIN BORROW + {borrowLimits.min} +
+ +
+ MAX BORROW + {borrowLimits.max} +
+ +
+ CURRENTLY BORROWING +
+ {updatedValues.balanceFrom} + → + + {updatedValues.balanceTo} + +
+
+ +
+ Market Borrow APR +
+ {updatedValues.aprFrom}% + → + + {updatedValues.aprTo}% + +
+
+ +
+ Health Factor +
+ {healthFactor.current} + → + + {healthFactor.predicted} + +
+
+
+ + {totalStats && ( +
+
+ +
+
+
Total Borrowed:
+
+ + {millify(totalStats.totalAmount)} of{' '} + {millify(totalStats.capAmount)}{' '} + {selectedMarketData.underlyingSymbol} + +
+
+ ${millify(totalStats.totalFiat)} of ${millify(totalStats.capFiat)} +
+
+
+ )} + + {transactionSteps.length > 0 ? ( + + ) : ( + + )} +
+ ); +}; + +export const RepayTab = ({ + isLoadingUpdatedAssets, + selectedMarketData, + amount, + setAmount, + currentUtilizationPercentage, + handleUtilization, + hfpStatus, + maxAmount, + isLoadingMax, + transactionSteps, + resetTransactionSteps, + chainId, + onAction, + isDisabled, + updatedValues, + healthFactor +}: RepayTabProps) => { + return ( +
+ + + + + {hfpStatus === 'CRITICAL' && ( + + Health factor too low. + + )} + +
+
+ CURRENTLY BORROWING +
+ {updatedValues.balanceFrom} + → + + {updatedValues.balanceTo} + +
+
+ +
+ Market Borrow APR +
+ {updatedValues.aprFrom}% + → + + {updatedValues.aprTo}% + +
+
+ +
+ Health Factor +
+ {healthFactor.current} + → + + {healthFactor.predicted} + +
+
+
+ + {transactionSteps.length > 0 ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/packages/ui/app/_components/popup/page.tsx b/packages/ui/app/_components/popup/page.tsx index a86dfc724..4e92babdc 100644 --- a/packages/ui/app/_components/popup/page.tsx +++ b/packages/ui/app/_components/popup/page.tsx @@ -6,7 +6,6 @@ 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, @@ -18,6 +17,13 @@ import { } from 'viem'; import { useChainId } from 'wagmi'; +import { Dialog, DialogContent } from '@ui/components/ui/dialog'; +import { + Tabs, + TabsList, + TabsTrigger, + TabsContent +} from '@ui/components/ui/tabs'; import { INFO_MESSAGES } from '@ui/constants/index'; import { useMultiIonic } from '@ui/context/MultiIonicContext'; import { useBorrowCapsDataForAsset } from '@ui/hooks/ionic/useBorrowCapsDataForAsset'; @@ -39,15 +45,14 @@ 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 { useTransactionSteps } from './TransactionStepsHandler'; +import { + SupplyTab, + WithdrawTab, + BorrowTab, + RepayTab +} from '../markets/ManageTabs'; import { FundOperationMode } from '@ionicprotocol/types'; @@ -165,18 +170,17 @@ const Popup = ({ [selectedMarketData], chainId ); - const collateralApr = useMemo(() => { + const collateralApr = useMemo(() => { // Todo: add the market rewards to this calculation if (assetsSupplyAprData) { - return `${assetsSupplyAprData[selectedMarketData.cToken].apy.toFixed( - 2 - )}%`; + return parseFloat( + assetsSupplyAprData[selectedMarketData.cToken].apy.toFixed(2) + ); } - return '0.00%'; + return 0.0; }, [assetsSupplyAprData, selectedMarketData.cToken]); const [active, setActive] = useState(mode); - const slide = useRef(null!); const [amount, setAmount] = useReducer( (_: string | undefined, value: string | undefined): string | undefined => value, @@ -455,31 +459,19 @@ const Popup = ({ 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]); @@ -1193,31 +1185,12 @@ const Popup = ({ }, [predictedHealthFactor]); return ( <> -
!open && initiateCloseAnimation()} > - 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 */} - -
{ + switch (value) { + case 'supply': + setActive(PopupMode.SUPPLY); + break; + case 'withdraw': + setActive(PopupMode.WITHDRAW); + break; + case 'borrow': + setActive(PopupMode.BORROW); + break; + case 'repay': + setActive(PopupMode.REPAY); + break; + } + }} > - {(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 ? ( - - ) : ( - - )} -
-
- - )} -
-
-
+ + Supply + Withdraw + Borrow + Repay + + + + 0 + : false + } + /> + + + + + + + + + + + + + + + + setSwapWidgetOpen(false)} + open={swapWidgetOpen} + fromChain={chainId} + toChain={chainId} + toToken={selectedMarketData.underlyingToken} + onRouteExecutionCompleted={() => refetchMaxSupplyAmount()} + /> ( -
- - -
+ }} + disabled={!address} + > + Manage + ) } ]; 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 }; From fee4fc237c86a96aef76aa50ed8263a4d1081bb5 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Wed, 20 Nov 2024 10:29:33 +0100 Subject: [PATCH 08/66] fix loop/manage --- .../ui/app/_components/markets/ManageTabs.tsx | 15 +-- packages/ui/app/_components/popup/page.tsx | 124 ++++++------------ packages/ui/app/market/page.tsx | 87 ++++++++---- 3 files changed, 103 insertions(+), 123 deletions(-) diff --git a/packages/ui/app/_components/markets/ManageTabs.tsx b/packages/ui/app/_components/markets/ManageTabs.tsx index e787c745b..9e9277e70 100644 --- a/packages/ui/app/_components/markets/ManageTabs.tsx +++ b/packages/ui/app/_components/markets/ManageTabs.tsx @@ -38,7 +38,6 @@ interface BaseTabProps { }; enableCollateral?: boolean; onCollateralToggle?: () => void; - loopPossible?: boolean; totalStats?: { capAmount: number; totalAmount: number; @@ -111,8 +110,7 @@ export const SupplyTab = ({ totalStats, setSwapWidgetOpen, enableCollateral, - onCollateralToggle, - loopPossible + onCollateralToggle }: SupplyTabProps) => { return (
@@ -123,17 +121,6 @@ export const SupplyTab = ({ > Get {selectedMarketData.underlyingSymbol} - {loopPossible && ( - - )}
import('../markets/SwapWidget'), { }); export enum PopupMode { - SUPPLY = 1, - WITHDRAW, - BORROW, - REPAY, - LOOP + MANAGE = 1, + LOOP = 2 } +type ActiveTab = 'borrow' | 'repay' | 'supply' | 'withdraw'; +type FundOperation = + | FundOperationMode.BORROW + | FundOperationMode.REPAY + | FundOperationMode.SUPPLY + | FundOperationMode.WITHDRAW; + export enum HFPStatus { CRITICAL = 'CRITICAL', NORMAL = 'NORMAL', @@ -78,13 +79,10 @@ export enum HFPStatus { interface IPopup { closePopup: () => void; comptrollerAddress: Address; - loopMarkets?: LoopMarketData; - mode?: PopupMode; selectedMarketData: MarketData; } + const Popup = ({ - mode = PopupMode.SUPPLY, - loopMarkets, selectedMarketData, closePopup, comptrollerAddress @@ -180,7 +178,22 @@ const Popup = ({ return 0.0; }, [assetsSupplyAprData, selectedMarketData.cToken]); - const [active, setActive] = useState(mode); + const [active, setActive] = useState('supply'); + const operationMap: Record = { + supply: FundOperationMode.SUPPLY, + withdraw: FundOperationMode.WITHDRAW, + borrow: FundOperationMode.BORROW, + repay: FundOperationMode.REPAY + }; + + useEffect(() => { + setAmount('0'); + setCurrentUtilizationPercentage(0); + upsertTransactionStep(undefined); + setCurrentFundOperation(operationMap[active]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [active, upsertTransactionStep]); + const [amount, setAmount] = useReducer( (_: string | undefined, value: string | undefined): string | undefined => value, @@ -209,13 +222,13 @@ const Popup = ({ comptrollerAddress, address ?? ('' as Address), selectedMarketData.cToken, - active === PopupMode.WITHDRAW + active === 'withdraw' ? (amountAsBInt * BigInt(1e18)) / selectedMarketData.exchangeRate : parseUnits('0', selectedMarketData.underlyingDecimals), - active === PopupMode.BORROW + active === 'borrow' ? amountAsBInt : parseUnits('0', selectedMarketData.underlyingDecimals), - active === PopupMode.REPAY + active === 'repay' ? (amountAsBInt * BigInt(1e18)) / selectedMarketData.exchangeRate : parseUnits('0', selectedMarketData.underlyingDecimals) ); @@ -388,57 +401,43 @@ const Popup = ({ */ useEffect(() => { switch (active) { - case PopupMode.SUPPLY: { + case '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: { + case 'withdraw': { const div = Number(formatEther(amountAsBInt)) / (maxWithdrawAmount && maxWithdrawAmount > 0n ? Number(formatEther(maxWithdrawAmount)) : 1); setCurrentUtilizationPercentage(Math.round(div * 100)); - break; } - case PopupMode.BORROW: { + case '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: { + case '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; } } @@ -449,7 +448,6 @@ const Popup = ({ maxRepayAmount, maxSupplyAmount, maxWithdrawAmount - // setBorrow ]); useEffect(() => { @@ -458,23 +456,23 @@ const Popup = ({ upsertTransactionStep(undefined); switch (active) { - case PopupMode.SUPPLY: + case 'supply': setCurrentFundOperation(FundOperationMode.SUPPLY); break; - case PopupMode.WITHDRAW: + case 'withdraw': setCurrentFundOperation(FundOperationMode.WITHDRAW); break; - case PopupMode.BORROW: + case 'borrow': setCurrentFundOperation(FundOperationMode.BORROW); break; - case PopupMode.REPAY: + case 'repay': setCurrentFundOperation(FundOperationMode.REPAY); break; } - }, [active, mode, upsertTransactionStep]); + }, [active, upsertTransactionStep]); const initiateCloseAnimation = () => setIsMounted(false); @@ -1201,22 +1199,9 @@ const Popup = ({
{ - switch (value) { - case 'supply': - setActive(PopupMode.SUPPLY); - break; - case 'withdraw': - setActive(PopupMode.WITHDRAW); - break; - case 'borrow': - setActive(PopupMode.BORROW); - break; - case 'repay': - setActive(PopupMode.REPAY); - break; - } + setActive(value as ActiveTab); }} > @@ -1258,11 +1243,6 @@ const Popup = ({ setSwapWidgetOpen={setSwapWidgetOpen} enableCollateral={enableCollateral} onCollateralToggle={handleCollateralToggle} - loopPossible={ - loopMarkets - ? loopMarkets[selectedMarketData.cToken].length > 0 - : false - } /> @@ -1393,30 +1373,8 @@ const Popup = ({ toToken={selectedMarketData.underlyingToken} onRouteExecutionCompleted={() => refetchMaxSupplyAmount()} /> - - { - 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/market/page.tsx b/packages/ui/app/market/page.tsx index a2dc88e7e..e645d1abf 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -1,7 +1,7 @@ // Market.tsx 'use client'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import dynamic from 'next/dynamic'; import Image from 'next/image'; @@ -11,10 +11,10 @@ import { useSearchParams } from 'next/navigation'; import { mode } from 'viem/chains'; import { useChainId } from 'wagmi'; -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 type { LoopMarketData } from '@ui/hooks/useLoopMarkets'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; import CommonTable from '../_components/CommonTable'; @@ -23,6 +23,7 @@ import FeaturedMarketTile from '../_components/markets/FeaturedMarketTile'; import StakingTile from '../_components/markets/StakingTile'; import TotalTvlTile from '../_components/markets/TotalTvlTile'; import TvlTile from '../_components/markets/TvlTile'; +import Loop from '../_components/popup/Loop'; import Popup, { PopupMode } from '../_components/popup/page'; import Swap from '../_components/popup/Swap'; @@ -50,9 +51,10 @@ export default function Market() { const [swapOpen, setSwapOpen] = useState(false); const [swapWidgetOpen, setSwapWidgetOpen] = useState(false); const [wrapWidgetOpen, setWrapWidgetOpen] = useState(false); - const [popupMode, setPopupMode] = useState(); const chainId = useChainId(); const { address } = useMultiIonic(); + const [popupMode, setPopupMode] = useState(); + const [loopMarkets, setLoopMarkets] = useState(); const selectedPool = querypool ?? '0'; const chain = querychain ? querychain : mode.id.toString(); @@ -63,6 +65,21 @@ export default function Market() { chain ); + const selectedMarketData = marketData.find( + (asset) => asset.asset === selectedSymbol + ); + + const loopProps = useMemo(() => { + if (!selectedMarketData || !poolData) return null; + return { + borrowableAssets: loopMarkets + ? loopMarkets[selectedMarketData.cToken] + : [], + comptrollerAddress: poolData.comptroller, + selectedCollateralAsset: selectedMarketData + }; + }, [selectedMarketData, poolData, loopMarkets]); + const columns: EnhancedColumnDef[] = [ { id: 'asset', @@ -162,31 +179,42 @@ export default function Market() { header: 'ACTIONS', enableSorting: false, cell: ({ row }: MarketCellProps) => ( - +
+ {!row.original.isBorrowDisabled && ( + + )} + {row.original.loopPossible && ( + + )} +
) } ]; - const selectedMarketData = marketData.find( - (asset) => asset.asset === selectedSymbol - ); - return ( <>
@@ -248,15 +276,22 @@ export default function Market() {
- {popupMode && selectedMarketData && poolData && ( + {popupMode === PopupMode.MANAGE && selectedMarketData && poolData && ( setPopupMode(undefined)} comptrollerAddress={poolData.comptroller} - mode={popupMode} selectedMarketData={selectedMarketData} /> )} + {popupMode === PopupMode.LOOP && loopProps && ( + setPopupMode(undefined)} + isOpen={true} + /> + )} + {swapOpen && ( setSwapOpen(false)} From 14f8891879ff762959a59a93429285845811bad9 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Wed, 20 Nov 2024 18:13:14 +0100 Subject: [PATCH 09/66] first pass of migrating to manage dialog context --- .../dashboards/CollateralSwapPopup.tsx | 4 +- .../app/_components/dashboards/InfoRows.tsx | 2 +- .../{popup => manage-dialog}/Amount.tsx | 0 .../{popup => manage-dialog}/Approved.tsx | 0 .../_components/manage-dialog/BorrowTab.tsx | 297 ++++ .../{popup => manage-dialog}/DonutChart.tsx | 0 .../{popup => manage-dialog}/Loop.tsx | 0 .../_components/manage-dialog/RepayTab.tsx | 266 ++++ .../{popup => manage-dialog}/Slider.tsx | 0 .../_components/manage-dialog/SupplyTab.tsx | 328 ++++ .../{popup => manage-dialog}/Swap.tsx | 0 .../{popup => manage-dialog}/Tab.tsx | 2 +- .../TransactionStepsHandler.tsx | 0 .../_components/manage-dialog/WithdrawTab.tsx | 236 +++ .../app/_components/manage-dialog/index.tsx | 492 ++++++ .../markets/FeaturedMarketTile.tsx | 2 +- .../ui/app/_components/markets/ManageTabs.tsx | 589 ------- packages/ui/app/_components/popup/page.tsx | 1380 ----------------- packages/ui/app/dashboard/page.tsx | 6 +- .../ui/app/market/details/[asset]/page.tsx | 4 +- packages/ui/app/market/page.tsx | 8 +- packages/ui/app/stake/page.tsx | 2 +- packages/ui/context/ManageDialogContext.tsx | 614 ++++++++ 23 files changed, 2248 insertions(+), 1984 deletions(-) rename packages/ui/app/_components/{popup => manage-dialog}/Amount.tsx (100%) rename packages/ui/app/_components/{popup => manage-dialog}/Approved.tsx (100%) create mode 100644 packages/ui/app/_components/manage-dialog/BorrowTab.tsx rename packages/ui/app/_components/{popup => manage-dialog}/DonutChart.tsx (100%) rename packages/ui/app/_components/{popup => manage-dialog}/Loop.tsx (100%) create mode 100644 packages/ui/app/_components/manage-dialog/RepayTab.tsx rename packages/ui/app/_components/{popup => manage-dialog}/Slider.tsx (100%) create mode 100644 packages/ui/app/_components/manage-dialog/SupplyTab.tsx rename packages/ui/app/_components/{popup => manage-dialog}/Swap.tsx (100%) rename packages/ui/app/_components/{popup => manage-dialog}/Tab.tsx (98%) rename packages/ui/app/_components/{popup => manage-dialog}/TransactionStepsHandler.tsx (100%) create mode 100644 packages/ui/app/_components/manage-dialog/WithdrawTab.tsx create mode 100644 packages/ui/app/_components/manage-dialog/index.tsx delete mode 100644 packages/ui/app/_components/markets/ManageTabs.tsx delete mode 100644 packages/ui/app/_components/popup/page.tsx create mode 100644 packages/ui/context/ManageDialogContext.tsx diff --git a/packages/ui/app/_components/dashboards/CollateralSwapPopup.tsx b/packages/ui/app/_components/dashboards/CollateralSwapPopup.tsx index 333f02fd6..9fcfee70c 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/manage-dialog/Slider'; import TransactionStepsHandler, { useTransactionSteps -} from '@ui/app/_components/popup/TransactionStepsHandler'; +} from '@ui/app/_components/manage-dialog/TransactionStepsHandler'; import type { IBal } from '@ui/app/_components/stake/MaxDeposit'; import { donutoptions, getDonutData } from '@ui/app/_constants/mock'; import { INFO_MESSAGES } from '@ui/constants/index'; diff --git a/packages/ui/app/_components/dashboards/InfoRows.tsx b/packages/ui/app/_components/dashboards/InfoRows.tsx index 8a7221754..9a8d44cdf 100644 --- a/packages/ui/app/_components/dashboards/InfoRows.tsx +++ b/packages/ui/app/_components/dashboards/InfoRows.tsx @@ -20,7 +20,7 @@ const Rewards = dynamic(() => import('../markets/FlyWheelRewards'), { ssr: false }); import APRCell from '../markets/APRCell'; -import { PopupMode } from '../popup/page'; +import { PopupMode } from '../manage-dialog'; import type { Address } from 'viem'; diff --git a/packages/ui/app/_components/popup/Amount.tsx b/packages/ui/app/_components/manage-dialog/Amount.tsx similarity index 100% rename from packages/ui/app/_components/popup/Amount.tsx rename to packages/ui/app/_components/manage-dialog/Amount.tsx diff --git a/packages/ui/app/_components/popup/Approved.tsx b/packages/ui/app/_components/manage-dialog/Approved.tsx similarity index 100% rename from packages/ui/app/_components/popup/Approved.tsx rename to packages/ui/app/_components/manage-dialog/Approved.tsx diff --git a/packages/ui/app/_components/manage-dialog/BorrowTab.tsx b/packages/ui/app/_components/manage-dialog/BorrowTab.tsx new file mode 100644 index 000000000..3e7381adf --- /dev/null +++ b/packages/ui/app/_components/manage-dialog/BorrowTab.tsx @@ -0,0 +1,297 @@ +import millify from 'millify'; +import toast from 'react-hot-toast'; +import { formatUnits } from 'viem'; + +import { Alert, AlertDescription } from '@ui/components/ui/alert'; +import { Button } from '@ui/components/ui/button'; +import { INFO_MESSAGES } from '@ui/constants'; +import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import { useManageDialogContext } from '@ui/context/ManageDialogContext'; + +import Amount from './Amount'; +import MemoizedDonutChart from './DonutChart'; +import SliderComponent from './Slider'; +import TransactionStepsHandler, { + useTransactionSteps +} from './TransactionStepsHandler'; +import ResultHandler from '../ResultHandler'; + +interface BorrowTabProps { + isLoadingUpdatedAssets: boolean; + maxAmount: bigint; + isLoadingMax: boolean; + isDisabled: boolean; + updatedValues: { + balanceFrom?: string; + balanceTo?: string; + aprFrom?: number; + aprTo?: number; + }; + totalStats?: { + capAmount: number; + totalAmount: number; + capFiat: number; + totalFiat: number; + }; +} + +const BorrowTab = ({ + isLoadingUpdatedAssets, + maxAmount, + isLoadingMax, + isDisabled, + updatedValues, + totalStats +}: BorrowTabProps) => { + const { + selectedMarketData, + amount, + setAmount, + currentUtilizationPercentage, + handleUtilization, + hfpStatus, + transactionSteps, + resetTransactionSteps, + chainId, + normalizedHealthFactor, + normalizedPredictedHealthFactor, + amountAsBInt, + minBorrowAmount, + maxBorrowAmount + } = useManageDialogContext(); + + const healthFactor = { + current: normalizedHealthFactor ?? '0', + predicted: normalizedPredictedHealthFactor ?? '0' + }; + + 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 { currentSdk, address } = useMultiIonic(); + + const { addStepsForAction, upsertTransactionStep } = useTransactionSteps(); + + 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!'); + } + } + }; + + return ( +
+ + + + + {isUnderMinBorrow && ( + + + Amount must be greater than minimum borrow amount ( + {borrowLimits.min} {selectedMarketData.underlyingSymbol}) + + + )} + + {hfpStatus === 'UNKNOWN' && ( + + + Unable to calculate health factor. + + + )} + + {hfpStatus === 'WARNING' && ( + + + You are close to the liquidation threshold. Manage your health + factor carefully. + + + )} + + {hfpStatus === 'CRITICAL' && ( + + Health factor too low. + + )} + +
+
+ MIN BORROW + {borrowLimits.min} +
+ +
+ MAX BORROW + {borrowLimits.max} +
+ +
+ CURRENTLY BORROWING +
+ {updatedValues.balanceFrom} + → + + {updatedValues.balanceTo} + +
+
+ +
+ Market Borrow APR +
+ {updatedValues.aprFrom}% + → + + {updatedValues.aprTo}% + +
+
+ +
+ Health Factor +
+ {healthFactor.current} + → + + {healthFactor.predicted} + +
+
+
+ + {totalStats && ( +
+
+ +
+
+
Total Borrowed:
+
+ + {millify(totalStats.totalAmount)} of{' '} + {millify(totalStats.capAmount)}{' '} + {selectedMarketData.underlyingSymbol} + +
+
+ ${millify(totalStats.totalFiat)} of ${millify(totalStats.capFiat)} +
+
+
+ )} + + {transactionSteps.length > 0 ? ( + + ) : ( + + )} +
+ ); +}; + +export default BorrowTab; diff --git a/packages/ui/app/_components/popup/DonutChart.tsx b/packages/ui/app/_components/manage-dialog/DonutChart.tsx similarity index 100% rename from packages/ui/app/_components/popup/DonutChart.tsx rename to packages/ui/app/_components/manage-dialog/DonutChart.tsx diff --git a/packages/ui/app/_components/popup/Loop.tsx b/packages/ui/app/_components/manage-dialog/Loop.tsx similarity index 100% rename from packages/ui/app/_components/popup/Loop.tsx rename to packages/ui/app/_components/manage-dialog/Loop.tsx diff --git a/packages/ui/app/_components/manage-dialog/RepayTab.tsx b/packages/ui/app/_components/manage-dialog/RepayTab.tsx new file mode 100644 index 000000000..e7192afc1 --- /dev/null +++ b/packages/ui/app/_components/manage-dialog/RepayTab.tsx @@ -0,0 +1,266 @@ +import { useMemo } from 'react'; + +import toast from 'react-hot-toast'; +import { formatUnits } from 'viem'; + +import { Alert, AlertDescription } from '@ui/components/ui/alert'; +import { Button } from '@ui/components/ui/button'; +import { INFO_MESSAGES } from '@ui/constants'; +import { useManageDialogContext } from '@ui/context/ManageDialogContext'; +import { useMultiIonic } from '@ui/context/MultiIonicContext'; + +import Amount from './Amount'; +import SliderComponent from './Slider'; +import TransactionStepsHandler, { + useTransactionSteps +} from './TransactionStepsHandler'; +import ResultHandler from '../ResultHandler'; + +interface RepayTabProps { + isLoadingUpdatedAssets: boolean; + maxAmount: bigint; + isLoadingMax: boolean; + updatedValues: { + balanceFrom?: string; + balanceTo?: string; + aprFrom?: number; + aprTo?: number; + }; +} + +const RepayTab = ({ + isLoadingUpdatedAssets, + maxAmount, + isLoadingMax, + updatedValues +}: RepayTabProps) => { + const { + selectedMarketData, + amount, + setAmount, + currentUtilizationPercentage, + handleUtilization, + hfpStatus, + transactionSteps, + resetTransactionSteps, + chainId, + normalizedHealthFactor, + normalizedPredictedHealthFactor, + amountAsBInt + } = useManageDialogContext(); + const { currentSdk, address } = useMultiIonic(); + + const { addStepsForAction, upsertTransactionStep } = useTransactionSteps(); + const currentBorrowAmountAsFloat = useMemo( + () => parseFloat(selectedMarketData.borrowBalance.toString()), + [selectedMarketData] + ); + + 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 healthFactor = { + current: normalizedHealthFactor ?? '0', + predicted: normalizedPredictedHealthFactor ?? '0' + }; + + return ( +
+ + + + + {hfpStatus === 'CRITICAL' && ( + + Health factor too low. + + )} + +
+
+ CURRENTLY BORROWING +
+ {updatedValues.balanceFrom} + → + + {updatedValues.balanceTo} + +
+
+ +
+ Market Borrow APR +
+ {updatedValues.aprFrom}% + → + + {updatedValues.aprTo}% + +
+
+ +
+ Health Factor +
+ {healthFactor.current} + → + + {healthFactor.predicted} + +
+
+
+ + {transactionSteps.length > 0 ? ( + + ) : ( + + )} +
+ ); +}; + +export default RepayTab; diff --git a/packages/ui/app/_components/popup/Slider.tsx b/packages/ui/app/_components/manage-dialog/Slider.tsx similarity index 100% rename from packages/ui/app/_components/popup/Slider.tsx rename to packages/ui/app/_components/manage-dialog/Slider.tsx diff --git a/packages/ui/app/_components/manage-dialog/SupplyTab.tsx b/packages/ui/app/_components/manage-dialog/SupplyTab.tsx new file mode 100644 index 000000000..8442b5257 --- /dev/null +++ b/packages/ui/app/_components/manage-dialog/SupplyTab.tsx @@ -0,0 +1,328 @@ +import millify from 'millify'; +import toast from 'react-hot-toast'; +import { formatUnits } from 'viem'; + +import { Button } from '@ui/components/ui/button'; +import { Switch } from '@ui/components/ui/switch'; +import { INFO_MESSAGES } from '@ui/constants'; +import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import { useManageDialogContext } from '@ui/context/ManageDialogContext'; + +import Amount from './Amount'; +import MemoizedDonutChart from './DonutChart'; +import SliderComponent from './Slider'; +import TransactionStepsHandler, { + useTransactionSteps +} from './TransactionStepsHandler'; +import ResultHandler from '../ResultHandler'; + +interface SupplyTabProps { + isLoadingUpdatedAssets: boolean; + maxAmount: bigint; + isLoadingMax: boolean; + isDisabled: boolean; + updatedValues: { + balanceFrom?: string; + balanceTo?: string; + aprFrom?: number; + aprTo?: number; + collateralApr?: number; + }; + totalStats?: { + capAmount: number; + totalAmount: number; + capFiat: number; + totalFiat: number; + }; + setSwapWidgetOpen: (open: boolean) => void; +} + +const SupplyTab = ({ + isLoadingUpdatedAssets, + maxAmount, + isLoadingMax, + isDisabled, + updatedValues, + totalStats, + setSwapWidgetOpen +}: SupplyTabProps) => { + const { + selectedMarketData, + amount, + setAmount, + currentUtilizationPercentage, + handleUtilization, + enableCollateral, + transactionSteps, + resetTransactionSteps, + chainId, + amountAsBInt, + comptrollerAddress, + handleCollateralToggle + } = useManageDialogContext(); + + const { currentSdk, address } = useMultiIonic(); + const { addStepsForAction, upsertTransactionStep } = useTransactionSteps(); + + const supplyAmount = async () => { + if ( + !transactionSteps.length && + currentSdk && + address && + amount && + amountAsBInt > 0n && + amountAsBInt <= maxAmount + ) { + 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 + } + }); + } + } + }; + + return ( +
+
+ +
+ + + + + +
+
+ COLLATERAL APR + {updatedValues.collateralApr}% +
+ +
+ Market Supply Balance +
+ {updatedValues.balanceFrom} + → + + {updatedValues.balanceTo} + +
+
+ +
+ Market Supply APR +
+ {updatedValues.aprFrom}% + → + + {updatedValues.aprTo}% + +
+
+ +
+ + Enable Collateral + + 0 || !selectedMarketData.supplyBalance + } + /> +
+
+ + {totalStats && ( +
+
+ +
+
+
Total Supplied:
+
+ + {millify(totalStats.totalAmount)} of{' '} + {millify(totalStats.capAmount)}{' '} + {selectedMarketData.underlyingSymbol} + +
+
+ ${millify(totalStats.totalFiat)} of ${millify(totalStats.capFiat)} +
+
+
+ )} + + {transactionSteps.length > 0 ? ( + + ) : ( + + )} +
+ ); +}; + +export default SupplyTab; diff --git a/packages/ui/app/_components/popup/Swap.tsx b/packages/ui/app/_components/manage-dialog/Swap.tsx similarity index 100% rename from packages/ui/app/_components/popup/Swap.tsx rename to packages/ui/app/_components/manage-dialog/Swap.tsx diff --git a/packages/ui/app/_components/popup/Tab.tsx b/packages/ui/app/_components/manage-dialog/Tab.tsx similarity index 98% rename from packages/ui/app/_components/popup/Tab.tsx rename to packages/ui/app/_components/manage-dialog/Tab.tsx index c71fe30ef..fd8fc4b1d 100644 --- a/packages/ui/app/_components/popup/Tab.tsx +++ b/packages/ui/app/_components/manage-dialog/Tab.tsx @@ -1,4 +1,4 @@ -import { PopupMode } from './page'; +import { PopupMode } from '.'; interface IMode { active: PopupMode; loopPossible: boolean; diff --git a/packages/ui/app/_components/popup/TransactionStepsHandler.tsx b/packages/ui/app/_components/manage-dialog/TransactionStepsHandler.tsx similarity index 100% rename from packages/ui/app/_components/popup/TransactionStepsHandler.tsx rename to packages/ui/app/_components/manage-dialog/TransactionStepsHandler.tsx diff --git a/packages/ui/app/_components/manage-dialog/WithdrawTab.tsx b/packages/ui/app/_components/manage-dialog/WithdrawTab.tsx new file mode 100644 index 000000000..6cb41bfd9 --- /dev/null +++ b/packages/ui/app/_components/manage-dialog/WithdrawTab.tsx @@ -0,0 +1,236 @@ +import toast from 'react-hot-toast'; +import { formatUnits } from 'viem'; + +import { Alert, AlertDescription } from '@ui/components/ui/alert'; +import { Button } from '@ui/components/ui/button'; +import { INFO_MESSAGES } from '@ui/constants'; +import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import { useManageDialogContext } from '@ui/context/ManageDialogContext'; + +import Amount from './Amount'; +import SliderComponent from './Slider'; +import TransactionStepsHandler, { + useTransactionSteps +} from './TransactionStepsHandler'; +import ResultHandler from '../ResultHandler'; + +interface WithdrawTabProps { + isLoadingUpdatedAssets: boolean; + maxAmount: bigint; + isLoadingMax: boolean; + isDisabled: boolean; + updatedValues: { + balanceFrom?: string; + balanceTo?: string; + aprFrom?: number; + aprTo?: number; + }; +} + +const WithdrawTab = ({ + isLoadingUpdatedAssets, + maxAmount, + isLoadingMax, + isDisabled, + updatedValues +}: WithdrawTabProps) => { + const { + selectedMarketData, + amount, + setAmount, + currentUtilizationPercentage, + handleUtilization, + hfpStatus, + transactionSteps, + resetTransactionSteps, + chainId, + normalizedHealthFactor, + normalizedPredictedHealthFactor, + amountAsBInt + } = useManageDialogContext(); + + const healthFactor = { + current: normalizedHealthFactor ?? '0', + predicted: normalizedPredictedHealthFactor ?? '0' + }; + const { currentSdk, address } = useMultiIonic(); + + const { addStepsForAction, upsertTransactionStep } = useTransactionSteps(); + + const withdrawAmount = async () => { + if ( + !transactionSteps.length && + currentSdk && + address && + amount && + amountAsBInt > 0n && + maxAmount + ) { + 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 === maxAmount) { + 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!'); + } + } + }; + + return ( +
+ + + + + {hfpStatus === 'WARNING' && ( + + + You are close to the liquidation threshold. Manage your health + factor carefully. + + + )} + + {hfpStatus === 'CRITICAL' && ( + + Health factor too low. + + )} + + {hfpStatus === 'UNKNOWN' && ( + + + Unable to calculate health factor. + + + )} + +
+
+ Market Supply Balance +
+ {updatedValues.balanceFrom} + → + + {updatedValues.balanceTo} + +
+
+ +
+ Market Supply APR +
+ {updatedValues.aprFrom}% + → + + {updatedValues.aprTo}% + +
+
+ +
+ Health Factor +
+ {healthFactor.current} + → + + {healthFactor.predicted} + +
+
+
+ + {transactionSteps.length > 0 ? ( + + ) : ( + + )} +
+ ); +}; + +export default WithdrawTab; diff --git a/packages/ui/app/_components/manage-dialog/index.tsx b/packages/ui/app/_components/manage-dialog/index.tsx new file mode 100644 index 000000000..5601fa842 --- /dev/null +++ b/packages/ui/app/_components/manage-dialog/index.tsx @@ -0,0 +1,492 @@ +'use client'; +import { useEffect, useMemo, useReducer, useState } from 'react'; + +import dynamic from 'next/dynamic'; +import Image from 'next/image'; + +import { useQueryClient } from '@tanstack/react-query'; +import { + type Address, + formatEther, + formatUnits, + maxUint256, + parseEther, + parseUnits +} from 'viem'; +import { useChainId } from 'wagmi'; + +import { Dialog, DialogContent } from '@ui/components/ui/dialog'; +import { + Tabs, + TabsList, + TabsTrigger, + TabsContent +} from '@ui/components/ui/tabs'; +import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import { PopupProvider } from '@ui/context/ManageDialogContext'; +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 { 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 { getBlockTimePerMinuteByChainId } from '@ui/utils/networkData'; + +import BorrowTab from './BorrowTab'; +import RepayTab from './RepayTab'; +import SupplyTab from './SupplyTab'; +import WithdrawTab from './WithdrawTab'; + +import { FundOperationMode } from '@ionicprotocol/types'; + +const SwapWidget = dynamic(() => import('../markets/SwapWidget'), { + ssr: false +}); + +export enum PopupMode { + MANAGE = 1, + LOOP = 2 +} + +type ActiveTab = 'borrow' | 'repay' | 'supply' | 'withdraw'; + +export enum HFPStatus { + CRITICAL = 'CRITICAL', + NORMAL = 'NORMAL', + UNKNOWN = 'UNKNOWN', + WARNING = 'WARNING' +} + +interface IPopup { + closePopup: () => void; + comptrollerAddress: Address; + selectedMarketData: MarketData; +} + +const ManageDialog = ({ + selectedMarketData, + closePopup, + comptrollerAddress +}: IPopup) => { + 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: 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 parseFloat( + assetsSupplyAprData[selectedMarketData.cToken].apy.toFixed(2) + ); + } + + return 0.0; + }, [assetsSupplyAprData, selectedMarketData.cToken]); + const [active, setActive] = useState('supply'); + + 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 === 'withdraw' + ? (amountAsBInt * BigInt(1e18)) / selectedMarketData.exchangeRate + : parseUnits('0', selectedMarketData.underlyingDecimals), + active === 'borrow' + ? amountAsBInt + : parseUnits('0', selectedMarketData.underlyingDecimals), + active === 'repay' + ? (amountAsBInt * BigInt(1e18)) / selectedMarketData.exchangeRate + : parseUnits('0', selectedMarketData.underlyingDecimals) + ); + + 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 [isMounted, setIsMounted] = 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]); + + /** + * Fade in animation + */ + useEffect(() => { + setIsMounted(true); + }, []); + + useEffect(() => { + let closeTimer: ReturnType; + + if (!isMounted) { + closeTimer = setTimeout(() => { + closePopup(); + }, 301); + } + + return () => { + clearTimeout(closeTimer); + }; + }, [isMounted, closePopup]); + + const initiateCloseAnimation = () => setIsMounted(false); + + return ( + + !open && initiateCloseAnimation()} + > + +
+ modlogo +
+ + { + setActive(value as ActiveTab); + }} + > + + Supply + Withdraw + Borrow + Repay + + + + + + + + + + + + + + + + +
+
+ + setSwapWidgetOpen(false)} + open={swapWidgetOpen} + fromChain={chainId} + toChain={chainId} + toToken={selectedMarketData.underlyingToken} + onRouteExecutionCompleted={() => refetchMaxSupplyAmount()} + /> +
+ ); +}; + +export default ManageDialog; diff --git a/packages/ui/app/_components/markets/FeaturedMarketTile.tsx b/packages/ui/app/_components/markets/FeaturedMarketTile.tsx index fc235a14a..7f1f15d1c 100644 --- a/packages/ui/app/_components/markets/FeaturedMarketTile.tsx +++ b/packages/ui/app/_components/markets/FeaturedMarketTile.tsx @@ -6,7 +6,7 @@ import { useStore } from '@ui/store/Store'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; import WrapEthSwaps from './WrapEthSwaps'; -import { PopupMode } from '../popup/page'; +import { PopupMode } from '../manage-dialog'; import ResultHandler from '../ResultHandler'; // import BorrowPopover from './BorrowPopover'; diff --git a/packages/ui/app/_components/markets/ManageTabs.tsx b/packages/ui/app/_components/markets/ManageTabs.tsx deleted file mode 100644 index 9e9277e70..000000000 --- a/packages/ui/app/_components/markets/ManageTabs.tsx +++ /dev/null @@ -1,589 +0,0 @@ -import millify from 'millify'; -import { formatUnits } from 'viem'; - -import { Alert, AlertDescription } from '@ui/components/ui/alert'; -import { Button } from '@ui/components/ui/button'; -import { Switch } from '@ui/components/ui/switch'; -import { type MarketData } from '@ui/types/TokensDataMap'; - -import Amount from '../popup/Amount'; -import MemoizedDonutChart from '../popup/DonutChart'; -import SliderComponent from '../popup/Slider'; -import TransactionStepsHandler from '../popup/TransactionStepsHandler'; -import ResultHandler from '../ResultHandler'; - -import type { HFPStatus } from '../popup/page'; - -interface BaseTabProps { - selectedMarketData: MarketData; - amount?: string; - setAmount: (amount?: string) => void; - currentUtilizationPercentage: number; - handleUtilization: (percentage: number) => void; - hfpStatus: HFPStatus; - isLoadingUpdatedAssets: boolean; - transactionSteps: any[]; - resetTransactionSteps: () => void; - chainId: number; - maxAmount: bigint; - onAction: () => Promise; - isLoadingMax: boolean; - isDisabled: boolean; - updatedValues: { - balanceFrom?: string; - balanceTo?: string; - aprFrom?: number; - aprTo?: number; - collateralApr?: number; - }; - enableCollateral?: boolean; - onCollateralToggle?: () => void; - totalStats?: { - capAmount: number; - totalAmount: number; - capFiat: number; - totalFiat: number; - }; - healthFactor?: { - current: string; - predicted: string; - }; -} - -type SupplyTabProps = BaseTabProps & { - totalStats?: { - capAmount: number; - totalAmount: number; - capFiat: number; - totalFiat: number; - }; - setSwapWidgetOpen: (open: boolean) => void; -}; - -type WithdrawTabProps = BaseTabProps & { - healthFactor: { - current: string; - predicted: string; - }; - setSwapWidgetOpen: (open: boolean) => void; -}; - -type BorrowTabProps = BaseTabProps & { - borrowLimits: { - min: string; - max: string; - }; - totalStats?: { - capAmount: number; - totalAmount: number; - capFiat: number; - totalFiat: number; - }; - healthFactor: { - current: string; - predicted: string; - }; -}; - -type RepayTabProps = BaseTabProps & { - healthFactor: { - current: string; - predicted: string; - }; -}; - -export const SupplyTab = ({ - isLoadingUpdatedAssets, - selectedMarketData, - amount, - setAmount, - currentUtilizationPercentage, - handleUtilization, - maxAmount, - isLoadingMax, - transactionSteps, - resetTransactionSteps, - chainId, - onAction, - isDisabled, - updatedValues, - totalStats, - setSwapWidgetOpen, - enableCollateral, - onCollateralToggle -}: SupplyTabProps) => { - return ( -
-
- -
- - - - - -
-
- COLLATERAL APR - {updatedValues.collateralApr}% -
- -
- Market Supply Balance -
- {updatedValues.balanceFrom} - → - - {updatedValues.balanceTo} - -
-
- -
- Market Supply APR -
- {updatedValues.aprFrom}% - → - - {updatedValues.aprTo}% - -
-
- -
- - Enable Collateral - - 0 || !selectedMarketData.supplyBalance - } - /> -
-
- - {totalStats && ( -
-
- -
-
-
Total Supplied:
-
- - {millify(totalStats.totalAmount)} of{' '} - {millify(totalStats.capAmount)}{' '} - {selectedMarketData.underlyingSymbol} - -
-
- ${millify(totalStats.totalFiat)} of ${millify(totalStats.capFiat)} -
-
-
- )} - - {transactionSteps.length > 0 ? ( - - ) : ( - - )} -
- ); -}; - -export const WithdrawTab = ({ - isLoadingUpdatedAssets, - selectedMarketData, - amount, - setAmount, - currentUtilizationPercentage, - handleUtilization, - hfpStatus, - maxAmount, - isLoadingMax, - transactionSteps, - resetTransactionSteps, - chainId, - onAction, - isDisabled, - updatedValues, - healthFactor -}: WithdrawTabProps) => { - return ( -
- - - - - {hfpStatus === 'WARNING' && ( - - - You are close to the liquidation threshold. Manage your health - factor carefully. - - - )} - - {hfpStatus === 'CRITICAL' && ( - - Health factor too low. - - )} - - {hfpStatus === 'UNKNOWN' && ( - - - Unable to calculate health factor. - - - )} - -
-
- Market Supply Balance -
- {updatedValues.balanceFrom} - → - - {updatedValues.balanceTo} - -
-
- -
- Market Supply APR -
- {updatedValues.aprFrom}% - → - - {updatedValues.aprTo}% - -
-
- -
- Health Factor -
- {healthFactor.current} - → - - {healthFactor.predicted} - -
-
-
- - {transactionSteps.length > 0 ? ( - - ) : ( - - )} -
- ); -}; - -export const BorrowTab = ({ - isLoadingUpdatedAssets, - selectedMarketData, - amount, - setAmount, - currentUtilizationPercentage, - handleUtilization, - hfpStatus, - maxAmount, - isLoadingMax, - transactionSteps, - resetTransactionSteps, - chainId, - onAction, - isDisabled, - updatedValues, - borrowLimits, - totalStats, - healthFactor -}: BorrowTabProps) => { - const isUnderMinBorrow = - amount && - borrowLimits.min && - parseFloat(amount) < parseFloat(borrowLimits.min); - - return ( -
- - - - - {isUnderMinBorrow && ( - - - Amount must be greater than minimum borrow amount ( - {borrowLimits.min} {selectedMarketData.underlyingSymbol}) - - - )} - - {hfpStatus === 'UNKNOWN' && ( - - - Unable to calculate health factor. - - - )} - - {hfpStatus === 'WARNING' && ( - - - You are close to the liquidation threshold. Manage your health - factor carefully. - - - )} - - {hfpStatus === 'CRITICAL' && ( - - Health factor too low. - - )} - -
-
- MIN BORROW - {borrowLimits.min} -
- -
- MAX BORROW - {borrowLimits.max} -
- -
- CURRENTLY BORROWING -
- {updatedValues.balanceFrom} - → - - {updatedValues.balanceTo} - -
-
- -
- Market Borrow APR -
- {updatedValues.aprFrom}% - → - - {updatedValues.aprTo}% - -
-
- -
- Health Factor -
- {healthFactor.current} - → - - {healthFactor.predicted} - -
-
-
- - {totalStats && ( -
-
- -
-
-
Total Borrowed:
-
- - {millify(totalStats.totalAmount)} of{' '} - {millify(totalStats.capAmount)}{' '} - {selectedMarketData.underlyingSymbol} - -
-
- ${millify(totalStats.totalFiat)} of ${millify(totalStats.capFiat)} -
-
-
- )} - - {transactionSteps.length > 0 ? ( - - ) : ( - - )} -
- ); -}; - -export const RepayTab = ({ - isLoadingUpdatedAssets, - selectedMarketData, - amount, - setAmount, - currentUtilizationPercentage, - handleUtilization, - hfpStatus, - maxAmount, - isLoadingMax, - transactionSteps, - resetTransactionSteps, - chainId, - onAction, - isDisabled, - updatedValues, - healthFactor -}: RepayTabProps) => { - return ( -
- - - - - {hfpStatus === 'CRITICAL' && ( - - Health factor too low. - - )} - -
-
- CURRENTLY BORROWING -
- {updatedValues.balanceFrom} - → - - {updatedValues.balanceTo} - -
-
- -
- Market Borrow APR -
- {updatedValues.aprFrom}% - → - - {updatedValues.aprTo}% - -
-
- -
- Health Factor -
- {healthFactor.current} - → - - {healthFactor.predicted} - -
-
-
- - {transactionSteps.length > 0 ? ( - - ) : ( - - )} -
- ); -}; diff --git a/packages/ui/app/_components/popup/page.tsx b/packages/ui/app/_components/popup/page.tsx deleted file mode 100644 index 167895375..000000000 --- a/packages/ui/app/_components/popup/page.tsx +++ /dev/null @@ -1,1380 +0,0 @@ -'use client'; -import { useEffect, useMemo, useReducer, useState } from 'react'; - -import dynamic from 'next/dynamic'; - -import { useQueryClient } from '@tanstack/react-query'; -import toast from 'react-hot-toast'; -import { - type Address, - formatEther, - formatUnits, - maxUint256, - parseEther, - parseUnits -} from 'viem'; -import { useChainId } from 'wagmi'; - -import { Dialog, DialogContent } from '@ui/components/ui/dialog'; -import { - Tabs, - TabsList, - TabsTrigger, - TabsContent -} from '@ui/components/ui/tabs'; -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 { 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 Loop from './Loop'; -import { useTransactionSteps } from './TransactionStepsHandler'; -import { - SupplyTab, - WithdrawTab, - BorrowTab, - RepayTab -} from '../markets/ManageTabs'; - -import { FundOperationMode } from '@ionicprotocol/types'; - -const SwapWidget = dynamic(() => import('../markets/SwapWidget'), { - ssr: false -}); - -export enum PopupMode { - MANAGE = 1, - LOOP = 2 -} - -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' -} - -interface IPopup { - closePopup: () => void; - comptrollerAddress: Address; - selectedMarketData: MarketData; -} - -const Popup = ({ - 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 parseFloat( - assetsSupplyAprData[selectedMarketData.cToken].apy.toFixed(2) - ); - } - - return 0.0; - }, [assetsSupplyAprData, selectedMarketData.cToken]); - const [active, setActive] = useState('supply'); - const operationMap: Record = { - supply: FundOperationMode.SUPPLY, - withdraw: FundOperationMode.WITHDRAW, - borrow: FundOperationMode.BORROW, - repay: FundOperationMode.REPAY - }; - - useEffect(() => { - setAmount('0'); - setCurrentUtilizationPercentage(0); - upsertTransactionStep(undefined); - setCurrentFundOperation(operationMap[active]); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [active, upsertTransactionStep]); - - 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 === 'withdraw' - ? (amountAsBInt * BigInt(1e18)) / selectedMarketData.exchangeRate - : parseUnits('0', selectedMarketData.underlyingDecimals), - active === 'borrow' - ? amountAsBInt - : parseUnits('0', selectedMarketData.underlyingDecimals), - active === '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 'supply': { - const div = - Number(formatEther(amountAsBInt)) / - (maxSupplyAmount?.bigNumber && maxSupplyAmount.number > 0 - ? Number(formatEther(maxSupplyAmount?.bigNumber)) - : 1); - setCurrentUtilizationPercentage(Math.round(div * 100)); - break; - } - - case 'withdraw': { - const div = - Number(formatEther(amountAsBInt)) / - (maxWithdrawAmount && maxWithdrawAmount > 0n - ? Number(formatEther(maxWithdrawAmount)) - : 1); - setCurrentUtilizationPercentage(Math.round(div * 100)); - break; - } - - case 'borrow': { - const div = - Number(formatEther(amountAsBInt)) / - (maxBorrowAmount?.bigNumber && maxBorrowAmount.number > 0 - ? Number(formatEther(maxBorrowAmount?.bigNumber)) - : 1); - setCurrentUtilizationPercentage(Math.round(div * 100)); - break; - } - - case 'repay': { - const div = - Number(formatEther(amountAsBInt)) / - (maxRepayAmount && maxRepayAmount > 0n - ? Number(formatEther(maxRepayAmount)) - : 1); - setCurrentUtilizationPercentage(Math.round(div * 100)); - break; - } - } - }, [ - amountAsBInt, - active, - maxBorrowAmount, - maxRepayAmount, - maxSupplyAmount, - maxWithdrawAmount - ]); - - useEffect(() => { - setAmount('0'); - setCurrentUtilizationPercentage(0); - upsertTransactionStep(undefined); - - switch (active) { - case 'supply': - setCurrentFundOperation(FundOperationMode.SUPPLY); - break; - - case 'withdraw': - setCurrentFundOperation(FundOperationMode.WITHDRAW); - break; - - case 'borrow': - setCurrentFundOperation(FundOperationMode.BORROW); - break; - - case 'repay': - setCurrentFundOperation(FundOperationMode.REPAY); - break; - } - }, [active, 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 ( - <> - !open && initiateCloseAnimation()} - > - -
- modlogo -
- - { - setActive(value as ActiveTab); - }} - > - - Supply - Withdraw - Borrow - Repay - - - - - - - - - - - - - - - - -
-
- - setSwapWidgetOpen(false)} - open={swapWidgetOpen} - fromChain={chainId} - toChain={chainId} - toToken={selectedMarketData.underlyingToken} - onRouteExecutionCompleted={() => refetchMaxSupplyAmount()} - /> - - ); -}; - -export default Popup; diff --git a/packages/ui/app/dashboard/page.tsx b/packages/ui/app/dashboard/page.tsx index de656108d..7eb0300cd 100644 --- a/packages/ui/app/dashboard/page.tsx +++ b/packages/ui/app/dashboard/page.tsx @@ -34,11 +34,11 @@ import CollateralSwapPopup from '../_components/dashboards/CollateralSwapPopup'; import InfoRows, { InfoMode } from '../_components/dashboards/InfoRows'; import LoopRewards from '../_components/dashboards/LoopRewards'; import NetworkSelector from '../_components/markets/NetworkSelector'; -import Loop from '../_components/popup/Loop'; -import Popup from '../_components/popup/page'; +import Loop from '../_components/manage-dialog/Loop'; +import Popup from '../_components/manage-dialog'; import ResultHandler from '../_components/ResultHandler'; -import type { PopupMode } from '../_components/popup/page'; +import type { PopupMode } from '../_components/manage-dialog'; import type { FlywheelReward, diff --git a/packages/ui/app/market/details/[asset]/page.tsx b/packages/ui/app/market/details/[asset]/page.tsx index 3676d5bf1..e01497f1e 100644 --- a/packages/ui/app/market/details/[asset]/page.tsx +++ b/packages/ui/app/market/details/[asset]/page.tsx @@ -59,10 +59,10 @@ import { // ]; // const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042']; import { INFO } from '@ui/constants/index'; -import Popup, { PopupMode } from '@ui/app/_components/popup/page'; +import Popup, { PopupMode } from '@ui/app/_components/manage-dialog/page'; import { extractAndConvertStringTOValue } from '@ui/utils/stringToValue'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; -import Swap from '@ui/app/_components/popup/Swap'; +import Swap from '@ui/app/_components/manage-dialog/Swap'; import { MarketData, PoolData } from '@ui/types/TokensDataMap'; import { useFusePoolData } from '@ui/hooks/useFusePoolData'; import { useLoopMarkets } from '@ui/hooks/useLoopMarkets'; diff --git a/packages/ui/app/market/page.tsx b/packages/ui/app/market/page.tsx index e645d1abf..079001052 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -18,14 +18,14 @@ import type { LoopMarketData } from '@ui/hooks/useLoopMarkets'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; import CommonTable from '../_components/CommonTable'; +import ManageDialog, { PopupMode } from '../_components/manage-dialog'; +import Loop from '../_components/manage-dialog/Loop'; +import Swap from '../_components/manage-dialog/Swap'; import APRCell from '../_components/markets/APRCell'; import FeaturedMarketTile from '../_components/markets/FeaturedMarketTile'; import StakingTile from '../_components/markets/StakingTile'; import TotalTvlTile from '../_components/markets/TotalTvlTile'; import TvlTile from '../_components/markets/TvlTile'; -import Loop from '../_components/popup/Loop'; -import Popup, { PopupMode } from '../_components/popup/page'; -import Swap from '../_components/popup/Swap'; import type { EnhancedColumnDef } from '../_components/CommonTable'; import type { Row } from '@tanstack/react-table'; @@ -277,7 +277,7 @@ export default function Market() {
{popupMode === PopupMode.MANAGE && selectedMarketData && poolData && ( - setPopupMode(undefined)} comptrollerAddress={poolData.comptroller} selectedMarketData={selectedMarketData} diff --git a/packages/ui/app/stake/page.tsx b/packages/ui/app/stake/page.tsx index 2eeeddc69..540b27e41 100644 --- a/packages/ui/app/stake/page.tsx +++ b/packages/ui/app/stake/page.tsx @@ -38,7 +38,7 @@ import { } from '@ui/utils/getStakingTokens'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; -import SliderComponent from '../_components/popup/Slider'; +import SliderComponent from '../_components/manage-dialog/Slider'; import ResultHandler from '../_components/ResultHandler'; import ClaimRewards from '../_components/stake/ClaimRewards'; import MaxDeposit from '../_components/stake/MaxDeposit'; diff --git a/packages/ui/context/ManageDialogContext.tsx b/packages/ui/context/ManageDialogContext.tsx new file mode 100644 index 000000000..259460958 --- /dev/null +++ b/packages/ui/context/ManageDialogContext.tsx @@ -0,0 +1,614 @@ +'use client'; +import { + createContext, + useContext, + useEffect, + useMemo, + useReducer, + useState +} from 'react'; + +import { useQueryClient } from '@tanstack/react-query'; +import toast from 'react-hot-toast'; +import { + type Address, + formatEther, + formatUnits, + maxUint256, + parseEther, + parseUnits +} from 'viem'; +import { useChainId } from 'wagmi'; + +import type { TransactionStep } from '@ui/app/_components/manage-dialog/TransactionStepsHandler'; +import { useTransactionSteps } from '@ui/app/_components/manage-dialog/TransactionStepsHandler'; +import { INFO_MESSAGES } from '@ui/constants/index'; +import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import useUpdatedUserAssets from '@ui/hooks/ionic/useUpdatedUserAssets'; +import { + useHealthFactor, + useHealthFactorPrediction +} from '@ui/hooks/pools/useHealthFactor'; +import { useBorrowMinimum } from '@ui/hooks/useBorrowMinimum'; +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 { errorCodeToMessage } from '@ui/utils/errorCodeToMessage'; + +import { FundOperationMode } from '@ionicprotocol/types'; + +export enum PopupMode { + MANAGE = 1, + LOOP = 2 +} + +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' +} + +interface PopupContextType { + active: ActiveTab; + setActive: (tab: ActiveTab) => void; + amount?: string; + setAmount: (value: string | undefined) => void; + currentUtilizationPercentage: number; + handleUtilization: (utilizationPercentage: number) => void; + hfpStatus: HFPStatus; + enableCollateral: boolean; + handleCollateralToggle: () => Promise; + normalizedHealthFactor: string | undefined; + normalizedPredictedHealthFactor: string | undefined; + isMounted: boolean; + initiateCloseAnimation: () => void; + transactionSteps: TransactionStep[]; + resetTransactionSteps: () => void; + refetchUsedQueries: () => Promise; + selectedMarketData: MarketData; + chainId: number; + amountAsBInt: bigint; + comptrollerAddress: Address; + minBorrowAmount?: { + minBorrowAsset: bigint | undefined; + minBorrowNative: bigint | undefined; + minBorrowUSD: number | undefined; + }; + maxBorrowAmount?: + | { + bigNumber: bigint; + number: number; + } + | null + | undefined; + // Add any other shared variables or functions here +} + +const ManageDialogContext = createContext( + undefined +); + +export const PopupProvider: React.FC<{ + selectedMarketData: MarketData; + comptrollerAddress: Address; + closePopup: () => void; + children: React.ReactNode; +}> = ({ selectedMarketData, comptrollerAddress, closePopup, children }) => { + const { addStepsForAction, transactionSteps, upsertTransactionStep } = + useTransactionSteps(); + const { currentSdk, address } = useMultiIonic(); + const chainId = useChainId(); + + const { data: maxSupplyAmount } = useMaxSupplyAmount( + selectedMarketData, + comptrollerAddress, + chainId + ); + const [active, setActive] = useState('supply'); + const operationMap: Record = { + supply: FundOperationMode.SUPPLY, + withdraw: FundOperationMode.WITHDRAW, + borrow: FundOperationMode.BORROW, + repay: FundOperationMode.REPAY + }; + + useEffect(() => { + setAmount('0'); + setCurrentUtilizationPercentage(0); + upsertTransactionStep(undefined); + setCurrentFundOperation(operationMap[active]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [active, upsertTransactionStep]); + + const [amount, setAmount] = useReducer( + (_: string | undefined, value: string | undefined): string | undefined => + value, + '0' + ); + + const { data: maxRepayAmount } = useMaxRepayAmount( + selectedMarketData, + chainId + ); + const amountAsBInt = useMemo( + () => + parseUnits( + amount?.toString() ?? '0', + selectedMarketData.underlyingDecimals + ), + [amount, selectedMarketData.underlyingDecimals] + ); + const { data: maxBorrowAmount } = useMaxBorrowAmount( + selectedMarketData, + comptrollerAddress, + chainId + ); + + const { data: minBorrowAmount } = useBorrowMinimum( + selectedMarketData, + chainId + ); + + const { data: healthFactor } = useHealthFactor(comptrollerAddress, chainId); + const { data: _predictedHealthFactor } = useHealthFactorPrediction( + comptrollerAddress, + address ?? ('' as Address), + selectedMarketData.cToken, + active === 'withdraw' + ? (amountAsBInt * BigInt(1e18)) / selectedMarketData.exchangeRate + : parseUnits('0', selectedMarketData.underlyingDecimals), + active === 'borrow' + ? amountAsBInt + : parseUnits('0', selectedMarketData.underlyingDecimals), + active === 'repay' + ? (amountAsBInt * BigInt(1e18)) / selectedMarketData.exchangeRate + : parseUnits('0', selectedMarketData.underlyingDecimals) + ); + + const [currentUtilizationPercentage, setCurrentUtilizationPercentage] = + useState(0); + const [currentFundOperation, setCurrentFundOperation] = + useState(FundOperationMode.SUPPLY); + const { data: updatedAssets } = useUpdatedUserAssets({ + amount: amountAsBInt, + assets: [selectedMarketData], + index: 0, + mode: currentFundOperation, + poolChainId: chainId + }); + const updatedAsset = updatedAssets ? updatedAssets[0] : undefined; + const { data: maxWithdrawAmount } = useMaxWithdrawAmount( + selectedMarketData, + chainId + ); + + const [enableCollateral, setEnableCollateral] = useState( + selectedMarketData.membership && selectedMarketData.supplyBalance > 0n + ); + const [isMounted, setIsMounted] = 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 'supply': { + const div = + Number(formatEther(amountAsBInt)) / + (maxSupplyAmount?.bigNumber && maxSupplyAmount.number > 0 + ? Number(formatEther(maxSupplyAmount?.bigNumber)) + : 1); + setCurrentUtilizationPercentage(Math.round(div * 100)); + break; + } + + case 'withdraw': { + const div = + Number(formatEther(amountAsBInt)) / + (maxWithdrawAmount && maxWithdrawAmount > 0n + ? Number(formatEther(maxWithdrawAmount)) + : 1); + setCurrentUtilizationPercentage(Math.round(div * 100)); + break; + } + + case 'borrow': { + const div = + Number(formatEther(amountAsBInt)) / + (maxBorrowAmount?.bigNumber && maxBorrowAmount.number > 0 + ? Number(formatEther(maxBorrowAmount?.bigNumber)) + : 1); + setCurrentUtilizationPercentage(Math.round(div * 100)); + break; + } + + case 'repay': { + const div = + Number(formatEther(amountAsBInt)) / + (maxRepayAmount && maxRepayAmount > 0n + ? Number(formatEther(maxRepayAmount)) + : 1); + setCurrentUtilizationPercentage(Math.round(div * 100)); + break; + } + } + }, [ + amountAsBInt, + active, + maxBorrowAmount, + maxRepayAmount, + maxSupplyAmount, + maxWithdrawAmount + ]); + + useEffect(() => { + setAmount('0'); + setCurrentUtilizationPercentage(0); + upsertTransactionStep(undefined); + + switch (active) { + case 'supply': + setCurrentFundOperation(FundOperationMode.SUPPLY); + break; + + case 'withdraw': + setCurrentFundOperation(FundOperationMode.WITHDRAW); + break; + + case 'borrow': + setCurrentFundOperation(FundOperationMode.BORROW); + break; + + case 'repay': + setCurrentFundOperation(FundOperationMode.REPAY); + break; + } + }, [active, upsertTransactionStep]); + + const initiateCloseAnimation = () => setIsMounted(false); + + const handleUtilization = (utilizationPercentage: number) => { + let maxAmountNumber = 0; + + switch (active) { + case 'supply': + maxAmountNumber = Number( + formatUnits( + maxSupplyAmount?.bigNumber ?? 0n, + selectedMarketData.underlyingDecimals + ) + ); + break; + case 'withdraw': + maxAmountNumber = Number( + formatUnits( + maxWithdrawAmount ?? 0n, + selectedMarketData.underlyingDecimals + ) + ); + break; + case 'borrow': + maxAmountNumber = Number( + formatUnits( + maxBorrowAmount?.bigNumber ?? 0n, + selectedMarketData.underlyingDecimals + ) + ); + break; + case 'repay': + maxAmountNumber = Number( + formatUnits( + maxRepayAmount ?? 0n, + selectedMarketData.underlyingDecimals + ) + ); + break; + default: + break; + } + + const calculatedAmount = ( + (utilizationPercentage / 100) * + maxAmountNumber + ).toFixed(parseInt(selectedMarketData.underlyingDecimals.toString())); + setAmount(calculatedAmount); + }; + + 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 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 ( + + {children} + + ); +}; + +export const useManageDialogContext = (): PopupContextType => { + const context = useContext(ManageDialogContext); + if (!context) { + throw new Error( + 'useManageDialogContext must be used within a PopupProvider' + ); + } + return context; +}; From 13135c0f67c8b2294bff1363f26de9f06a197f9d Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Wed, 20 Nov 2024 18:54:21 +0100 Subject: [PATCH 10/66] continue cleaning up --- .../_components/manage-dialog/BorrowTab.tsx | 42 ++- .../_components/manage-dialog/RepayTab.tsx | 30 +- .../_components/manage-dialog/SupplyTab.tsx | 31 +-- .../_components/manage-dialog/WithdrawTab.tsx | 41 ++- .../app/_components/manage-dialog/index.tsx | 261 +----------------- .../markets/FeaturedMarketTile.tsx | 16 +- packages/ui/app/market/page.tsx | 22 +- packages/ui/context/ManageDialogContext.tsx | 121 +++++++- 8 files changed, 214 insertions(+), 350 deletions(-) diff --git a/packages/ui/app/_components/manage-dialog/BorrowTab.tsx b/packages/ui/app/_components/manage-dialog/BorrowTab.tsx index 3e7381adf..d186ce1a3 100644 --- a/packages/ui/app/_components/manage-dialog/BorrowTab.tsx +++ b/packages/ui/app/_components/manage-dialog/BorrowTab.tsx @@ -6,7 +6,10 @@ import { Alert, AlertDescription } from '@ui/components/ui/alert'; import { Button } from '@ui/components/ui/button'; import { INFO_MESSAGES } from '@ui/constants'; import { useMultiIonic } from '@ui/context/MultiIonicContext'; -import { useManageDialogContext } from '@ui/context/ManageDialogContext'; +import { + HFPStatus, + useManageDialogContext +} from '@ui/context/ManageDialogContext'; import Amount from './Amount'; import MemoizedDonutChart from './DonutChart'; @@ -17,16 +20,8 @@ import TransactionStepsHandler, { import ResultHandler from '../ResultHandler'; interface BorrowTabProps { - isLoadingUpdatedAssets: boolean; maxAmount: bigint; isLoadingMax: boolean; - isDisabled: boolean; - updatedValues: { - balanceFrom?: string; - balanceTo?: string; - aprFrom?: number; - aprTo?: number; - }; totalStats?: { capAmount: number; totalAmount: number; @@ -35,14 +30,7 @@ interface BorrowTabProps { }; } -const BorrowTab = ({ - isLoadingUpdatedAssets, - maxAmount, - isLoadingMax, - isDisabled, - updatedValues, - totalStats -}: BorrowTabProps) => { +const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { const { selectedMarketData, amount, @@ -57,9 +45,19 @@ const BorrowTab = ({ normalizedPredictedHealthFactor, amountAsBInt, minBorrowAmount, - maxBorrowAmount + maxBorrowAmount, + isLoadingPredictedHealthFactor, + isLoadingUpdatedAssets, + updatedValues } = useManageDialogContext(); + const isDisabled = + !amount || + amountAsBInt === 0n || + isLoadingPredictedHealthFactor || + hfpStatus === HFPStatus.CRITICAL || + hfpStatus === HFPStatus.UNKNOWN; + const healthFactor = { current: normalizedHealthFactor ?? '0', predicted: normalizedPredictedHealthFactor ?? '0' @@ -220,10 +218,10 @@ const BorrowTab = ({
CURRENTLY BORROWING
- {updatedValues.balanceFrom} + {updatedValues.borrowBalanceFrom} → - {updatedValues.balanceTo} + {updatedValues.borrowBalanceTo}
@@ -231,10 +229,10 @@ const BorrowTab = ({
Market Borrow APR
- {updatedValues.aprFrom}% + {updatedValues.borrowAPR}% → - {updatedValues.aprTo}% + {updatedValues.updatedBorrowAPR}%
diff --git a/packages/ui/app/_components/manage-dialog/RepayTab.tsx b/packages/ui/app/_components/manage-dialog/RepayTab.tsx index e7192afc1..2ccf3d06f 100644 --- a/packages/ui/app/_components/manage-dialog/RepayTab.tsx +++ b/packages/ui/app/_components/manage-dialog/RepayTab.tsx @@ -17,23 +17,11 @@ import TransactionStepsHandler, { import ResultHandler from '../ResultHandler'; interface RepayTabProps { - isLoadingUpdatedAssets: boolean; maxAmount: bigint; isLoadingMax: boolean; - updatedValues: { - balanceFrom?: string; - balanceTo?: string; - aprFrom?: number; - aprTo?: number; - }; } -const RepayTab = ({ - isLoadingUpdatedAssets, - maxAmount, - isLoadingMax, - updatedValues -}: RepayTabProps) => { +const RepayTab = ({ maxAmount, isLoadingMax }: RepayTabProps) => { const { selectedMarketData, amount, @@ -46,7 +34,9 @@ const RepayTab = ({ chainId, normalizedHealthFactor, normalizedPredictedHealthFactor, - amountAsBInt + amountAsBInt, + isLoadingUpdatedAssets, + updatedValues } = useManageDialogContext(); const { currentSdk, address } = useMultiIonic(); @@ -211,10 +201,14 @@ const RepayTab = ({
CURRENTLY BORROWING
- {updatedValues.balanceFrom} + + {updatedValues.borrowBalanceFrom} + → - {updatedValues.balanceTo} + + {updatedValues.borrowBalanceTo} +
@@ -222,10 +216,10 @@ const RepayTab = ({
Market Borrow APR
- {updatedValues.aprFrom}% + {updatedValues.borrowAPR}% → - {updatedValues.aprTo}% + {updatedValues.updatedBorrowAPR}%
diff --git a/packages/ui/app/_components/manage-dialog/SupplyTab.tsx b/packages/ui/app/_components/manage-dialog/SupplyTab.tsx index 8442b5257..741847727 100644 --- a/packages/ui/app/_components/manage-dialog/SupplyTab.tsx +++ b/packages/ui/app/_components/manage-dialog/SupplyTab.tsx @@ -17,17 +17,8 @@ import TransactionStepsHandler, { import ResultHandler from '../ResultHandler'; interface SupplyTabProps { - isLoadingUpdatedAssets: boolean; maxAmount: bigint; isLoadingMax: boolean; - isDisabled: boolean; - updatedValues: { - balanceFrom?: string; - balanceTo?: string; - aprFrom?: number; - aprTo?: number; - collateralApr?: number; - }; totalStats?: { capAmount: number; totalAmount: number; @@ -35,16 +26,15 @@ interface SupplyTabProps { totalFiat: number; }; setSwapWidgetOpen: (open: boolean) => void; + collateralApr: number; } const SupplyTab = ({ - isLoadingUpdatedAssets, maxAmount, isLoadingMax, - isDisabled, - updatedValues, totalStats, - setSwapWidgetOpen + setSwapWidgetOpen, + collateralApr }: SupplyTabProps) => { const { selectedMarketData, @@ -58,9 +48,12 @@ const SupplyTab = ({ chainId, amountAsBInt, comptrollerAddress, - handleCollateralToggle + handleCollateralToggle, + updatedValues, + isLoadingUpdatedAssets } = useManageDialogContext(); + const isDisabled = !amount || amountAsBInt === 0n; const { currentSdk, address } = useMultiIonic(); const { addStepsForAction, upsertTransactionStep } = useTransactionSteps(); @@ -243,16 +236,16 @@ const SupplyTab = ({
COLLATERAL APR - {updatedValues.collateralApr}% + {collateralApr}%
Market Supply Balance
- {updatedValues.balanceFrom} + {updatedValues.supplyBalanceFrom} → - {updatedValues.balanceTo} + {updatedValues.supplyBalanceTo}
@@ -260,10 +253,10 @@ const SupplyTab = ({
Market Supply APR
- {updatedValues.aprFrom}% + {updatedValues.supplyAPY}% → - {updatedValues.aprTo}% + {updatedValues.updatedSupplyAPY}%
diff --git a/packages/ui/app/_components/manage-dialog/WithdrawTab.tsx b/packages/ui/app/_components/manage-dialog/WithdrawTab.tsx index 6cb41bfd9..770b475d5 100644 --- a/packages/ui/app/_components/manage-dialog/WithdrawTab.tsx +++ b/packages/ui/app/_components/manage-dialog/WithdrawTab.tsx @@ -5,7 +5,10 @@ import { Alert, AlertDescription } from '@ui/components/ui/alert'; import { Button } from '@ui/components/ui/button'; import { INFO_MESSAGES } from '@ui/constants'; import { useMultiIonic } from '@ui/context/MultiIonicContext'; -import { useManageDialogContext } from '@ui/context/ManageDialogContext'; +import { + HFPStatus, + useManageDialogContext +} from '@ui/context/ManageDialogContext'; import Amount from './Amount'; import SliderComponent from './Slider'; @@ -15,25 +18,11 @@ import TransactionStepsHandler, { import ResultHandler from '../ResultHandler'; interface WithdrawTabProps { - isLoadingUpdatedAssets: boolean; maxAmount: bigint; isLoadingMax: boolean; - isDisabled: boolean; - updatedValues: { - balanceFrom?: string; - balanceTo?: string; - aprFrom?: number; - aprTo?: number; - }; } -const WithdrawTab = ({ - isLoadingUpdatedAssets, - maxAmount, - isLoadingMax, - isDisabled, - updatedValues -}: WithdrawTabProps) => { +const WithdrawTab = ({ maxAmount, isLoadingMax }: WithdrawTabProps) => { const { selectedMarketData, amount, @@ -46,9 +35,19 @@ const WithdrawTab = ({ chainId, normalizedHealthFactor, normalizedPredictedHealthFactor, - amountAsBInt + amountAsBInt, + isLoadingPredictedHealthFactor, + updatedValues, + isLoadingUpdatedAssets } = useManageDialogContext(); + const isDisabled = + !amount || + amountAsBInt === 0n || + isLoadingPredictedHealthFactor || + hfpStatus === HFPStatus.CRITICAL || + hfpStatus === HFPStatus.UNKNOWN; + const healthFactor = { current: normalizedHealthFactor ?? '0', predicted: normalizedPredictedHealthFactor ?? '0' @@ -183,10 +182,10 @@ const WithdrawTab = ({
Market Supply Balance
- {updatedValues.balanceFrom} + {updatedValues.supplyBalanceFrom} → - {updatedValues.balanceTo} + {updatedValues.supplyBalanceTo}
@@ -194,10 +193,10 @@ const WithdrawTab = ({
Market Supply APR
- {updatedValues.aprFrom}% + {updatedValues.supplyAPY}% → - {updatedValues.aprTo}% + {updatedValues.updatedSupplyAPY}%
diff --git a/packages/ui/app/_components/manage-dialog/index.tsx b/packages/ui/app/_components/manage-dialog/index.tsx index 5601fa842..00d918bdd 100644 --- a/packages/ui/app/_components/manage-dialog/index.tsx +++ b/packages/ui/app/_components/manage-dialog/index.tsx @@ -1,18 +1,10 @@ 'use client'; -import { useEffect, useMemo, useReducer, useState } from 'react'; +import { useMemo, useState } from 'react'; import dynamic from 'next/dynamic'; import Image from 'next/image'; -import { useQueryClient } from '@tanstack/react-query'; -import { - type Address, - formatEther, - formatUnits, - maxUint256, - parseEther, - parseUnits -} from 'viem'; +import { type Address, formatEther, formatUnits } from 'viem'; import { useChainId } from 'wagmi'; import { Dialog, DialogContent } from '@ui/components/ui/dialog'; @@ -26,11 +18,6 @@ import { useMultiIonic } from '@ui/context/MultiIonicContext'; import { PopupProvider } from '@ui/context/ManageDialogContext'; 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 { useMaxBorrowAmount } from '@ui/hooks/useMaxBorrowAmount'; import { useMaxRepayAmount } from '@ui/hooks/useMaxRepayAmount'; @@ -38,15 +25,12 @@ 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 { getBlockTimePerMinuteByChainId } from '@ui/utils/networkData'; import BorrowTab from './BorrowTab'; import RepayTab from './RepayTab'; import SupplyTab from './SupplyTab'; import WithdrawTab from './WithdrawTab'; -import { FundOperationMode } from '@ionicprotocol/types'; - const SwapWidget = dynamic(() => import('../markets/SwapWidget'), { ssr: false }); @@ -66,17 +50,19 @@ export enum HFPStatus { } interface IPopup { - closePopup: () => void; + isOpen: boolean; + setIsOpen: (open: boolean) => void; comptrollerAddress: Address; selectedMarketData: MarketData; } const ManageDialog = ({ + isOpen, + setIsOpen, selectedMarketData, - closePopup, comptrollerAddress }: IPopup) => { - const { currentSdk, address } = useMultiIonic(); + const { currentSdk } = useMultiIonic(); const chainId = useChainId(); const { data: usdPrice } = useUsdPrice(chainId.toString()); const pricePerSingleAsset = useMemo( @@ -163,209 +149,35 @@ const ManageDialog = ({ }, [assetsSupplyAprData, selectedMarketData.cToken]); const [active, setActive] = useState('supply'); - 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 === 'withdraw' - ? (amountAsBInt * BigInt(1e18)) / selectedMarketData.exchangeRate - : parseUnits('0', selectedMarketData.underlyingDecimals), - active === 'borrow' - ? amountAsBInt - : parseUnits('0', selectedMarketData.underlyingDecimals), - active === 'repay' - ? (amountAsBInt * BigInt(1e18)) / selectedMarketData.exchangeRate - : parseUnits('0', selectedMarketData.underlyingDecimals) - ); - - 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 [isMounted, setIsMounted] = 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]); /** * Fade in animation */ - useEffect(() => { - setIsMounted(true); - }, []); - - useEffect(() => { - let closeTimer: ReturnType; - - if (!isMounted) { - closeTimer = setTimeout(() => { - closePopup(); - }, 301); - } - - return () => { - clearTimeout(closeTimer); - }; - }, [isMounted, closePopup]); - - const initiateCloseAnimation = () => setIsMounted(false); return ( setIsOpen(false)} selectedMarketData={selectedMarketData} > !open && initiateCloseAnimation()} + open={true} + onOpenChange={(open) => { + if (!open) { + setIsOpen(false); // Close the popup when the dialog is closed + } + }} >
@@ -393,17 +205,9 @@ const ManageDialog = ({ diff --git a/packages/ui/app/_components/markets/FeaturedMarketTile.tsx b/packages/ui/app/_components/markets/FeaturedMarketTile.tsx index 7f1f15d1c..54a3f65ab 100644 --- a/packages/ui/app/_components/markets/FeaturedMarketTile.tsx +++ b/packages/ui/app/_components/markets/FeaturedMarketTile.tsx @@ -6,7 +6,6 @@ import { useStore } from '@ui/store/Store'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; import WrapEthSwaps from './WrapEthSwaps'; -import { PopupMode } from '../manage-dialog'; import ResultHandler from '../ResultHandler'; // import BorrowPopover from './BorrowPopover'; @@ -16,7 +15,7 @@ import ResultHandler from '../ResultHandler'; interface Iprop { selectedChain: number; - setPopupMode: Dispatch>; + setIsManageDialogOpen: Dispatch>; setSelectedSymbol: Dispatch>; isLoadingPoolData: boolean; dropdownSelectedChain: string; @@ -28,7 +27,7 @@ interface Iprop { export default function FeaturedMarketTile({ selectedChain, - setPopupMode, + setIsManageDialogOpen, setSelectedSymbol, isLoadingPoolData = true, dropdownSelectedChain, @@ -37,6 +36,11 @@ export default function FeaturedMarketTile({ setWrapWidgetOpen, wrapWidgetOpen }: Iprop) { + console.log('dropdownSelectedChain', dropdownSelectedChain); + console.log('selectedChain', selectedChain); + console.log('isLoadingPoolData', isLoadingPoolData); + console.log('setWrapWidgetOpen', setWrapWidgetOpen); + console.log('wrapWidgetOpen', wrapWidgetOpen); // const { // asset, // borrowAPR, @@ -49,7 +53,9 @@ export default function FeaturedMarketTile({ // loopPossible // } = useStore((state) => state.featuredBorrow); const featuredSupply = useStore((state) => state.featuredSupply); + console.log('featuredSupply', featuredSupply); const featuredSupply2 = useStore((state) => state.featuredSupply2); + console.log('featuredSupply2', featuredSupply2); return (
@@ -169,7 +175,7 @@ export default function FeaturedMarketTile({ ); if (result) { setSelectedSymbol(featuredSupply2.asset); - setPopupMode(PopupMode.SUPPLY); + setIsManageDialogOpen(true); } }} > diff --git a/packages/ui/app/market/page.tsx b/packages/ui/app/market/page.tsx index 079001052..5ce43e5e6 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -18,7 +18,7 @@ import type { LoopMarketData } from '@ui/hooks/useLoopMarkets'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; import CommonTable from '../_components/CommonTable'; -import ManageDialog, { PopupMode } from '../_components/manage-dialog'; +import ManageDialog from '../_components/manage-dialog'; import Loop from '../_components/manage-dialog/Loop'; import Swap from '../_components/manage-dialog/Swap'; import APRCell from '../_components/markets/APRCell'; @@ -53,8 +53,9 @@ export default function Market() { const [wrapWidgetOpen, setWrapWidgetOpen] = useState(false); const chainId = useChainId(); const { address } = useMultiIonic(); - const [popupMode, setPopupMode] = useState(); const [loopMarkets, setLoopMarkets] = useState(); + const [isManageDialogOpen, setIsManageDialogOpen] = useState(false); + const [isLoopDialogOpen, setIsLoopDialogOpen] = useState(false); const selectedPool = querypool ?? '0'; const chain = querychain ? querychain : mode.id.toString(); @@ -187,7 +188,7 @@ export default function Market() { const result = await handleSwitchOriginChain(+chain, chainId); if (result) { setSelectedSymbol(row.original.asset); - setPopupMode(PopupMode.MANAGE); + setIsManageDialogOpen(true); } }} disabled={!address} @@ -202,7 +203,7 @@ export default function Market() { const result = await handleSwitchOriginChain(+chain, chainId); if (result) { setSelectedSymbol(row.original.asset); - setPopupMode(PopupMode.LOOP); + setIsLoopDialogOpen(true); } }} disabled={!address} @@ -230,7 +231,7 @@ export default function Market() { />
- {popupMode === PopupMode.MANAGE && selectedMarketData && poolData && ( + {selectedMarketData && poolData && ( setPopupMode(undefined)} + isOpen={isManageDialogOpen} + setIsOpen={setIsManageDialogOpen} comptrollerAddress={poolData.comptroller} selectedMarketData={selectedMarketData} /> )} - {popupMode === PopupMode.LOOP && loopProps && ( + {loopProps && ( setPopupMode(undefined)} - isOpen={true} + closeLoop={() => setIsLoopDialogOpen(false)} + isOpen={isLoopDialogOpen} /> )} diff --git a/packages/ui/context/ManageDialogContext.tsx b/packages/ui/context/ManageDialogContext.tsx index 259460958..091bf916f 100644 --- a/packages/ui/context/ManageDialogContext.tsx +++ b/packages/ui/context/ManageDialogContext.tsx @@ -38,6 +38,7 @@ import type { MarketData } from '@ui/types/TokensDataMap'; import { errorCodeToMessage } from '@ui/utils/errorCodeToMessage'; import { FundOperationMode } from '@ionicprotocol/types'; +import { getBlockTimePerMinuteByChainId } from '@ui/utils/networkData'; export enum PopupMode { MANAGE = 1, @@ -91,6 +92,21 @@ interface PopupContextType { } | null | undefined; + updatedValues: { + borrowAPR: number | undefined; + borrowBalanceFrom: string; + borrowBalanceTo: string | undefined; + supplyAPY: number | undefined; + supplyBalanceFrom: string; + supplyBalanceTo: string | undefined; + totalBorrows: number | undefined; + updatedBorrowAPR: number | undefined; + updatedSupplyAPY: number | undefined; + updatedTotalBorrows: number | undefined; + }; + isLoadingPredictedHealthFactor: boolean; + isLoadingUpdatedAssets: boolean; + // Add any other shared variables or functions here } @@ -160,7 +176,10 @@ export const PopupProvider: React.FC<{ ); const { data: healthFactor } = useHealthFactor(comptrollerAddress, chainId); - const { data: _predictedHealthFactor } = useHealthFactorPrediction( + const { + data: _predictedHealthFactor, + isLoading: isLoadingPredictedHealthFactor + } = useHealthFactorPrediction( comptrollerAddress, address ?? ('' as Address), selectedMarketData.cToken, @@ -179,13 +198,14 @@ export const PopupProvider: React.FC<{ useState(0); const [currentFundOperation, setCurrentFundOperation] = useState(FundOperationMode.SUPPLY); - const { data: updatedAssets } = useUpdatedUserAssets({ - amount: amountAsBInt, - assets: [selectedMarketData], - index: 0, - mode: currentFundOperation, - poolChainId: chainId - }); + 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 } = useMaxWithdrawAmount( selectedMarketData, @@ -557,6 +577,86 @@ export const PopupProvider: React.FC<{ } }; + const updatedValues = 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 + }; + } + + // Ensure all properties are defined, even if undefined + return { + borrowAPR: undefined, + borrowBalanceFrom: '', + borrowBalanceTo: undefined, + supplyAPY: undefined, + supplyBalanceFrom: '', + supplyBalanceTo: undefined, + totalBorrows: undefined, + updatedBorrowAPR: undefined, + updatedSupplyAPY: undefined, + updatedTotalBorrows: undefined + }; + }, [chainId, updatedAsset, selectedMarketData, updatedAssets, currentSdk]); + const normalizedHealthFactor = useMemo(() => { return healthFactor ? healthFactor === '-1' @@ -595,7 +695,10 @@ export const PopupProvider: React.FC<{ amountAsBInt, comptrollerAddress, minBorrowAmount, - maxBorrowAmount + maxBorrowAmount, + isLoadingPredictedHealthFactor, + isLoadingUpdatedAssets, + updatedValues }} > {children} From 9a24911bdba6e802e5263325cd815f99589d9461 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Wed, 20 Nov 2024 22:02:11 +0100 Subject: [PATCH 11/66] move more logic to context --- packages/ui/app/market/page.tsx | 47 ++++++++--------------- packages/ui/hooks/market/useMarketData.ts | 27 +++++++++++-- 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/packages/ui/app/market/page.tsx b/packages/ui/app/market/page.tsx index 5ce43e5e6..7784d3601 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -46,40 +46,23 @@ interface MarketCellProps { 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 chainId = useChainId(); - const { address } = useMultiIonic(); - const [loopMarkets, setLoopMarkets] = useState(); const [isManageDialogOpen, setIsManageDialogOpen] = useState(false); const [isLoopDialogOpen, setIsLoopDialogOpen] = useState(false); - - const selectedPool = querypool ?? '0'; - const chain = querychain ? querychain : mode.id.toString(); const [selectedSymbol, setSelectedSymbol] = useState(); - const { marketData, isLoading, poolData } = useMarketData( - selectedPool, - chain - ); - - const selectedMarketData = marketData.find( - (asset) => asset.asset === selectedSymbol - ); - - const loopProps = useMemo(() => { - if (!selectedMarketData || !poolData) return null; - return { - borrowableAssets: loopMarkets - ? loopMarkets[selectedMarketData.cToken] - : [], - comptrollerAddress: poolData.comptroller, - selectedCollateralAsset: selectedMarketData - }; - }, [selectedMarketData, poolData, loopMarkets]); + const { marketData, isLoading, poolData, selectedMarketData, loopProps } = + useMarketData(selectedPool, chain, selectedSymbol); const columns: EnhancedColumnDef[] = [ { @@ -223,23 +206,23 @@ export default function Market() {
diff --git a/packages/ui/hooks/market/useMarketData.ts b/packages/ui/hooks/market/useMarketData.ts index 6e81e700c..b4fbfc77e 100644 --- a/packages/ui/hooks/market/useMarketData.ts +++ b/packages/ui/hooks/market/useMarketData.ts @@ -37,7 +37,11 @@ export type MarketRowData = MarketData & { isBorrowDisabled: boolean; }; -export const useMarketData = (selectedPool: string, chain: number | string) => { +export const useMarketData = ( + selectedPool: string, + chain: number | string, + selectedSymbol: string | undefined +) => { const { data: poolData, isLoading: isLoadingPoolData } = useFusePoolData( selectedPool, +chain @@ -203,9 +207,26 @@ export const useMarketData = (selectedPool: string, chain: number | string) => { borrowCapsData ]); + const selectedMarketData = marketData.find( + (asset) => asset.asset === 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 || isLoadingLoopMarkets, - poolData + isLoading: isLoadingPoolData, + poolData, + selectedMarketData, + loopProps }; }; From b8d74bdd5b8d64beb2a27814572920e561844ce4 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Wed, 20 Nov 2024 22:08:19 +0100 Subject: [PATCH 12/66] fix dialog open/close --- packages/ui/app/_components/manage-dialog/index.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/ui/app/_components/manage-dialog/index.tsx b/packages/ui/app/_components/manage-dialog/index.tsx index 00d918bdd..e3028948a 100644 --- a/packages/ui/app/_components/manage-dialog/index.tsx +++ b/packages/ui/app/_components/manage-dialog/index.tsx @@ -172,12 +172,8 @@ const ManageDialog = ({ selectedMarketData={selectedMarketData} > { - if (!open) { - setIsOpen(false); // Close the popup when the dialog is closed - } - }} + open={isOpen} + onOpenChange={setIsOpen} >
From d435e8eac0c49d7338f3a3fea5ee3ef3c3f0995f Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Wed, 20 Nov 2024 22:21:07 +0100 Subject: [PATCH 13/66] show manage button & disable borrow tabs if disabled --- .../app/_components/manage-dialog/index.tsx | 27 +++++--- .../markets/FeaturedMarketTile.tsx | 18 ------ packages/ui/app/market/page.tsx | 31 ++++----- packages/ui/hooks/market/useMarketData.ts | 63 ++++++++++++++++++- 4 files changed, 98 insertions(+), 41 deletions(-) diff --git a/packages/ui/app/_components/manage-dialog/index.tsx b/packages/ui/app/_components/manage-dialog/index.tsx index e3028948a..87adf9645 100644 --- a/packages/ui/app/_components/manage-dialog/index.tsx +++ b/packages/ui/app/_components/manage-dialog/index.tsx @@ -54,13 +54,15 @@ interface IPopup { setIsOpen: (open: boolean) => void; comptrollerAddress: Address; selectedMarketData: MarketData; + isBorrowDisabled?: boolean; } const ManageDialog = ({ isOpen, setIsOpen, selectedMarketData, - comptrollerAddress + comptrollerAddress, + isBorrowDisabled = false }: IPopup) => { const { currentSdk } = useMultiIonic(); const chainId = useChainId(); @@ -187,16 +189,25 @@ const ManageDialog = ({
{ - setActive(value as ActiveTab); - }} + defaultValue="supply" + value={active} + onValueChange={(value) => setActive(value as ActiveTab)} > Supply Withdraw - Borrow - Repay + + Borrow + + + Repay + @@ -220,6 +231,7 @@ const ManageDialog = ({ isLoadingMax={isLoadingMaxWithdrawAmount} /> + + state.featuredBorrow); const featuredSupply = useStore((state) => state.featuredSupply); - console.log('featuredSupply', featuredSupply); const featuredSupply2 = useStore((state) => state.featuredSupply2); - console.log('featuredSupply2', featuredSupply2); return (
(false); const [isLoopDialogOpen, setIsLoopDialogOpen] = useState(false); const [selectedSymbol, setSelectedSymbol] = useState(); + const [isBorrowDisabled, setIsBorrowDisabled] = useState(false); const { marketData, isLoading, poolData, selectedMarketData, loopProps } = useMarketData(selectedPool, chain, selectedSymbol); @@ -164,21 +165,22 @@ export default function Market() { enableSorting: false, cell: ({ row }: MarketCellProps) => (
- {!row.original.isBorrowDisabled && ( - - )} + } + }} + disabled={!address} + > + Manage + {row.original.loopPossible && (
setActive(value as ActiveTab)} + defaultValue={activeTab} + // onValueChange={(value) => setActive(value as ActiveTab)} > Supply diff --git a/packages/ui/app/dashboard/page.tsx b/packages/ui/app/dashboard/page.tsx index 7eb0300cd..27c501b05 100644 --- a/packages/ui/app/dashboard/page.tsx +++ b/packages/ui/app/dashboard/page.tsx @@ -33,9 +33,9 @@ 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 NetworkSelector from '../_components/markets/NetworkSelector'; -import Loop from '../_components/manage-dialog/Loop'; import Popup from '../_components/manage-dialog'; +import Loop from '../_components/manage-dialog/Loop'; +import NetworkSelector from '../_components/markets/NetworkSelector'; import ResultHandler from '../_components/ResultHandler'; import type { PopupMode } from '../_components/manage-dialog'; diff --git a/packages/ui/app/market/details/[asset]/page.tsx b/packages/ui/app/market/details/[asset]/page.tsx index e01497f1e..c7a2616eb 100644 --- a/packages/ui/app/market/details/[asset]/page.tsx +++ b/packages/ui/app/market/details/[asset]/page.tsx @@ -59,8 +59,7 @@ import { // ]; // const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042']; import { INFO } from '@ui/constants/index'; -import Popup, { PopupMode } from '@ui/app/_components/manage-dialog/page'; -import { extractAndConvertStringTOValue } from '@ui/utils/stringToValue'; + import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; import Swap from '@ui/app/_components/manage-dialog/Swap'; import { MarketData, PoolData } from '@ui/types/TokensDataMap'; @@ -72,6 +71,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/manage-dialog'; // import { useBorrowAPYs } from '@ui/hooks/useBorrowAPYs'; // import { useSupplyAPYs } from '@ui/hooks/useSupplyAPYs'; @@ -80,6 +80,7 @@ interface IGraph { supplyAtY: number[]; valAtX: string[]; } +type ActiveTab = 'borrow' | 'repay' | 'supply' | 'withdraw'; const supabase = createClient( 'https://uoagtjstsdrjypxlkuzr.supabase.co/', @@ -95,6 +96,8 @@ const Asset = ({ params }: IProp) => { // console.log(data); 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'); @@ -108,7 +111,6 @@ const Asset = ({ params }: IProp) => { 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< @@ -596,7 +598,8 @@ const Asset = ({ params }: IProp) => { Number(selectedChain) ); if (result) { - setPopupMode(PopupMode.SUPPLY); + setIsManageDialogOpen(true); + setActiveTab('supply'); } }} > @@ -638,7 +641,8 @@ const Asset = ({ params }: IProp) => { Number(selectedChain) ); if (result) { - setPopupMode(PopupMode.BORROW); + setIsManageDialogOpen(true); + setActiveTab('borrow'); } }} > @@ -707,13 +711,13 @@ const Asset = ({ params }: IProp) => {
- {popupMode && selectedMarketData && poolData && ( - setPopupMode(undefined)} + {selectedMarketData && poolData && ( + )} diff --git a/packages/ui/app/market/page.tsx b/packages/ui/app/market/page.tsx index 7931acdd1..4c9aeb118 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -1,7 +1,7 @@ // Market.tsx 'use client'; -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import dynamic from 'next/dynamic'; import Image from 'next/image'; @@ -14,7 +14,6 @@ import { useChainId } from 'wagmi'; import { useMultiIonic } from '@ui/context/MultiIonicContext'; import type { MarketRowData } from '@ui/hooks/market/useMarketData'; import { useMarketData } from '@ui/hooks/market/useMarketData'; -import type { LoopMarketData } from '@ui/hooks/useLoopMarkets'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; import CommonTable from '../_components/CommonTable'; diff --git a/packages/ui/context/ManageDialogContext.tsx b/packages/ui/context/ManageDialogContext.tsx index 091bf916f..8fb668bbf 100644 --- a/packages/ui/context/ManageDialogContext.tsx +++ b/packages/ui/context/ManageDialogContext.tsx @@ -36,9 +36,9 @@ import { useMaxSupplyAmount } from '@ui/hooks/useMaxSupplyAmount'; import { useMaxWithdrawAmount } from '@ui/hooks/useMaxWithdrawAmount'; import type { MarketData } from '@ui/types/TokensDataMap'; import { errorCodeToMessage } from '@ui/utils/errorCodeToMessage'; +import { getBlockTimePerMinuteByChainId } from '@ui/utils/networkData'; import { FundOperationMode } from '@ionicprotocol/types'; -import { getBlockTimePerMinuteByChainId } from '@ui/utils/networkData'; export enum PopupMode { MANAGE = 1, diff --git a/packages/ui/hooks/market/useMarketData.ts b/packages/ui/hooks/market/useMarketData.ts index 6cf294073..67e7c2511 100644 --- a/packages/ui/hooks/market/useMarketData.ts +++ b/packages/ui/hooks/market/useMarketData.ts @@ -15,10 +15,10 @@ 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'; -import { useStore } from '@ui/store/Store'; export type MarketRowData = MarketData & { asset: string; @@ -226,7 +226,6 @@ export const useMarketData = ( selectedPool ]?.toLowerCase() === market.asset.toLowerCase() ) { - console.log('yeee'); setFeaturedSupply({ asset: market.asset, supplyAPR: market.supplyAPR, From a0caec115c956122292a5898f86059be34a24f77 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Thu, 21 Nov 2024 20:00:25 +0100 Subject: [PATCH 15/66] fix build --- .../app/_components/dashboards/InfoRows.tsx | 19 ++-- .../ui/app/_components/manage-dialog/Tab.tsx | 91 ------------------- .../app/_components/manage-dialog/index.tsx | 2 +- .../markets/FeaturedMarketTile.tsx | 38 -------- packages/ui/app/dashboard/page.tsx | 23 +++-- packages/ui/context/ManageDialogContext.tsx | 5 - 6 files changed, 25 insertions(+), 153 deletions(-) delete mode 100644 packages/ui/app/_components/manage-dialog/Tab.tsx diff --git a/packages/ui/app/_components/dashboards/InfoRows.tsx b/packages/ui/app/_components/dashboards/InfoRows.tsx index ed8363272..3e7f097ab 100644 --- a/packages/ui/app/_components/dashboards/InfoRows.tsx +++ b/packages/ui/app/_components/dashboards/InfoRows.tsx @@ -1,6 +1,6 @@ /* eslint-disable @next/next/no-img-element */ 'use client'; -import { useMemo, type Dispatch, type SetStateAction } from 'react'; +import { useMemo, useState, type Dispatch, type SetStateAction } from 'react'; import dynamic from 'next/dynamic'; @@ -19,7 +19,6 @@ import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; const Rewards = dynamic(() => import('../markets/FlyWheelRewards'), { ssr: false }); -import { PopupMode } from '../manage-dialog'; import APRCell from '../markets/APRCell'; import type { Address } from 'viem'; @@ -31,6 +30,8 @@ export enum InfoMode { BORROW = 1 } +type ActiveTab = 'borrow' | 'repay' | 'supply' | 'withdraw'; + export type InfoRowsProps = { amount: string; apr: string; @@ -44,7 +45,8 @@ export type InfoRowsProps = { comptrollerAddress: Address; rewards: FlywheelReward[]; selectedChain: number; - setPopupMode: Dispatch>; + setActiveTab: Dispatch>; + setIsManageDialogOpen: Dispatch>; setSelectedSymbol: Dispatch>; utilization: string; toggler?: () => void; @@ -58,7 +60,8 @@ const InfoRows = ({ membership, mode, setSelectedSymbol, - setPopupMode, + setActiveTab, + setIsManageDialogOpen, apr, selectedChain, cToken, @@ -195,9 +198,8 @@ const InfoRows = ({ ); if (result) { setSelectedSymbol(asset); - setPopupMode( - mode === InfoMode.SUPPLY ? PopupMode.SUPPLY : PopupMode.REPAY - ); + setIsManageDialogOpen(true); + setActiveTab(mode === InfoMode.SUPPLY ? 'supply' : 'repay'); } }} > @@ -224,7 +226,8 @@ const InfoRows = ({ // Router.push() // toggle the mode setSelectedSymbol(asset); - setPopupMode(PopupMode.BORROW); + setIsManageDialogOpen(true); + setActiveTab('borrow'); } } }} diff --git a/packages/ui/app/_components/manage-dialog/Tab.tsx b/packages/ui/app/_components/manage-dialog/Tab.tsx deleted file mode 100644 index fd8fc4b1d..000000000 --- a/packages/ui/app/_components/manage-dialog/Tab.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { PopupMode } from '.'; -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/manage-dialog/index.tsx b/packages/ui/app/_components/manage-dialog/index.tsx index 3b50810e1..e8d25e4c4 100644 --- a/packages/ui/app/_components/manage-dialog/index.tsx +++ b/packages/ui/app/_components/manage-dialog/index.tsx @@ -39,7 +39,7 @@ export enum PopupMode { LOOP = 2 } -type ActiveTab = 'borrow' | 'repay' | 'supply' | 'withdraw'; +export type ActiveTab = 'borrow' | 'repay' | 'supply' | 'withdraw'; export enum HFPStatus { CRITICAL = 'CRITICAL', diff --git a/packages/ui/app/_components/markets/FeaturedMarketTile.tsx b/packages/ui/app/_components/markets/FeaturedMarketTile.tsx index f453003ae..c0e5e2ac0 100644 --- a/packages/ui/app/_components/markets/FeaturedMarketTile.tsx +++ b/packages/ui/app/_components/markets/FeaturedMarketTile.tsx @@ -8,11 +8,6 @@ import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; import WrapEthSwaps from './WrapEthSwaps'; import ResultHandler from '../ResultHandler'; -// import BorrowPopover from './BorrowPopover'; -// import SupplyPopover from './SupplyPopover'; - -// import { pools } from '@ui/constants/index'; - interface Iprop { selectedChain: number; setIsManageDialogOpen: Dispatch>; @@ -81,15 +76,6 @@ export default function FeaturedMarketTile({ }) ?? '-'} % - {/* */}
- {/* */}
)} diff --git a/packages/ui/app/dashboard/page.tsx b/packages/ui/app/dashboard/page.tsx index 27c501b05..1fb3e94b2 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 Popup from '../_components/manage-dialog'; +import ManageDialog from '../_components/manage-dialog'; import Loop from '../_components/manage-dialog/Loop'; import NetworkSelector from '../_components/markets/NetworkSelector'; import ResultHandler from '../_components/ResultHandler'; -import type { PopupMode } from '../_components/manage-dialog'; +import type { ActiveTab } from '../_components/manage-dialog'; 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(); @@ -238,7 +239,6 @@ export default function Dashboard() { toggle: swapToggle } = useOutsideClick(); - // console.log(suppliedAssets); return ( <> {swapOpen && marketData?.comptroller && ( @@ -555,7 +555,8 @@ export default function Dashboard() { })) as FlywheelReward[]) ?? [] } selectedChain={+chain} - setPopupMode={setPopupMode} + setActiveTab={setActiveTab} + setIsManageDialogOpen={setIsManageDialogOpen} setSelectedSymbol={setSelectedSymbol} // utilization={utilizations[i]} toggler={async () => { @@ -657,7 +658,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%" @@ -748,11 +750,12 @@ export default function Dashboard() { /> )} - {popupMode && selectedMarketData && marketData && ( - setPopupMode(undefined)} + {selectedMarketData && marketData && ( + )} diff --git a/packages/ui/context/ManageDialogContext.tsx b/packages/ui/context/ManageDialogContext.tsx index 8fb668bbf..79ec56ea1 100644 --- a/packages/ui/context/ManageDialogContext.tsx +++ b/packages/ui/context/ManageDialogContext.tsx @@ -40,11 +40,6 @@ import { getBlockTimePerMinuteByChainId } from '@ui/utils/networkData'; import { FundOperationMode } from '@ionicprotocol/types'; -export enum PopupMode { - MANAGE = 1, - LOOP = 2 -} - type ActiveTab = 'borrow' | 'repay' | 'supply' | 'withdraw'; type FundOperation = | FundOperationMode.BORROW From a18460ad672c4d8b01e82785b48a57a067dcbad8 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Thu, 21 Nov 2024 20:23:48 +0100 Subject: [PATCH 16/66] fix decimals --- .../_components/manage-dialog/BorrowTab.tsx | 4 +- .../_components/manage-dialog/RepayTab.tsx | 4 +- .../_components/manage-dialog/SupplyTab.tsx | 5 +- .../_components/manage-dialog/WithdrawTab.tsx | 4 +- .../app/_components/manage-dialog/index.tsx | 6 +- packages/ui/context/ManageDialogContext.tsx | 90 ++++++++++--------- 6 files changed, 61 insertions(+), 52 deletions(-) diff --git a/packages/ui/app/_components/manage-dialog/BorrowTab.tsx b/packages/ui/app/_components/manage-dialog/BorrowTab.tsx index 0700f34ad..b2d66b0a4 100644 --- a/packages/ui/app/_components/manage-dialog/BorrowTab.tsx +++ b/packages/ui/app/_components/manage-dialog/BorrowTab.tsx @@ -229,10 +229,10 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => {
Market Borrow APR
- {updatedValues.borrowAPR}% + {updatedValues.borrowAPR?.toFixed(2)}% → - {updatedValues.updatedBorrowAPR}% + {updatedValues.updatedBorrowAPR?.toFixed(2)}%
diff --git a/packages/ui/app/_components/manage-dialog/RepayTab.tsx b/packages/ui/app/_components/manage-dialog/RepayTab.tsx index 2ccf3d06f..694263547 100644 --- a/packages/ui/app/_components/manage-dialog/RepayTab.tsx +++ b/packages/ui/app/_components/manage-dialog/RepayTab.tsx @@ -216,10 +216,10 @@ const RepayTab = ({ maxAmount, isLoadingMax }: RepayTabProps) => {
Market Borrow APR
- {updatedValues.borrowAPR}% + {updatedValues.borrowAPR?.toFixed(2)}% → - {updatedValues.updatedBorrowAPR}% + {updatedValues.updatedBorrowAPR?.toFixed(2)}%
diff --git a/packages/ui/app/_components/manage-dialog/SupplyTab.tsx b/packages/ui/app/_components/manage-dialog/SupplyTab.tsx index 6b894ec06..def96befd 100644 --- a/packages/ui/app/_components/manage-dialog/SupplyTab.tsx +++ b/packages/ui/app/_components/manage-dialog/SupplyTab.tsx @@ -52,6 +52,7 @@ const SupplyTab = ({ updatedValues, isLoadingUpdatedAssets } = useManageDialogContext(); + console.log('updatedValues', updatedValues); const isDisabled = !amount || amountAsBInt === 0n; const { currentSdk, address } = useMultiIonic(); @@ -253,10 +254,10 @@ const SupplyTab = ({
Market Supply APR
- {updatedValues.supplyAPY}% + {updatedValues.supplyAPY?.toFixed(2)}% → - {updatedValues.updatedSupplyAPY}% + {updatedValues.updatedSupplyAPY?.toFixed(2)}%
diff --git a/packages/ui/app/_components/manage-dialog/WithdrawTab.tsx b/packages/ui/app/_components/manage-dialog/WithdrawTab.tsx index 802209d13..a36b9bcad 100644 --- a/packages/ui/app/_components/manage-dialog/WithdrawTab.tsx +++ b/packages/ui/app/_components/manage-dialog/WithdrawTab.tsx @@ -193,10 +193,10 @@ const WithdrawTab = ({ maxAmount, isLoadingMax }: WithdrawTabProps) => {
Market Supply APR
- {updatedValues.supplyAPY}% + {updatedValues.supplyAPY?.toFixed(2)}% → - {updatedValues.updatedSupplyAPY}% + {updatedValues.updatedSupplyAPY?.toFixed(2)}%
diff --git a/packages/ui/app/_components/manage-dialog/index.tsx b/packages/ui/app/_components/manage-dialog/index.tsx index e8d25e4c4..5de5533e3 100644 --- a/packages/ui/app/_components/manage-dialog/index.tsx +++ b/packages/ui/app/_components/manage-dialog/index.tsx @@ -14,7 +14,7 @@ import { TabsTrigger, TabsContent } from '@ui/components/ui/tabs'; -import { PopupProvider } from '@ui/context/ManageDialogContext'; +import { ManageDialogProvider } from '@ui/context/ManageDialogContext'; import { useBorrowCapsDataForAsset } from '@ui/hooks/ionic/useBorrowCapsDataForAsset'; import { useSupplyCapsDataForAsset } from '@ui/hooks/ionic/useSupplyCapsDataForPool'; import { useUsdPrice } from '@ui/hooks/useAllUsdPrices'; @@ -167,7 +167,7 @@ const ManageDialog = ({ */ return ( - setIsOpen(false)} selectedMarketData={selectedMarketData} @@ -261,7 +261,7 @@ const ManageDialog = ({ toToken={selectedMarketData.underlyingToken} onRouteExecutionCompleted={() => refetchMaxSupplyAmount()} /> - + ); }; diff --git a/packages/ui/context/ManageDialogContext.tsx b/packages/ui/context/ManageDialogContext.tsx index 79ec56ea1..41cdf3c3b 100644 --- a/packages/ui/context/ManageDialogContext.tsx +++ b/packages/ui/context/ManageDialogContext.tsx @@ -93,7 +93,7 @@ interface PopupContextType { borrowBalanceTo: string | undefined; supplyAPY: number | undefined; supplyBalanceFrom: string; - supplyBalanceTo: string | undefined; + supplyBalanceTo: string | number; totalBorrows: number | undefined; updatedBorrowAPR: number | undefined; updatedSupplyAPY: number | undefined; @@ -109,7 +109,7 @@ const ManageDialogContext = createContext( undefined ); -export const PopupProvider: React.FC<{ +export const ManageDialogProvider: React.FC<{ selectedMarketData: MarketData; comptrollerAddress: Address; closePopup: () => void; @@ -576,35 +576,36 @@ export const PopupProvider: React.FC<{ 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: Number( - formatUnits( - selectedMarketData.borrowBalance, - selectedMarketData.underlyingDecimals - ) - ).toLocaleString('en-US', { maximumFractionDigits: 2 }), + borrowBalanceFrom: formatBalanceValue( + selectedMarketData.borrowBalance, + selectedMarketData.underlyingDecimals + ), borrowBalanceTo: updatedAsset - ? Number( - formatUnits( - updatedAsset.borrowBalance, - updatedAsset.underlyingDecimals - ) - ).toLocaleString('en-US', { maximumFractionDigits: 2 }) - : undefined, + ? formatBalanceValue( + updatedAsset.borrowBalance, + updatedAsset.underlyingDecimals + ) + : '0', supplyAPY: currentSdk.ratePerBlockToAPY( selectedMarketData.supplyRatePerBlock, blocksPerMinute ), - supplyBalanceFrom: Number( - formatUnits( - selectedMarketData.supplyBalance, - selectedMarketData.underlyingDecimals - ) - ).toLocaleString('en-US', { maximumFractionDigits: 2 }), + supplyBalanceFrom: formatBalanceValue( + selectedMarketData.supplyBalance ?? 0n, + selectedMarketData.underlyingDecimals + ), supplyBalanceTo: updatedAsset ? Math.abs( Number( @@ -613,12 +614,14 @@ export const PopupProvider: React.FC<{ updatedAsset.underlyingDecimals ) ) - ).toLocaleString('en-US', { maximumFractionDigits: 2 }) - : undefined, - totalBorrows: updatedAssets?.reduce( - (acc, cur) => acc + cur.borrowBalanceFiat, - 0 - ), + ) || 0 + : '0', + totalBorrows: + updatedAssets?.reduce( + (acc, cur) => + acc + (isNaN(cur.borrowBalanceFiat) ? 0 : cur.borrowBalanceFiat), + 0 + ) ?? 0, updatedBorrowAPR: updatedAsset ? currentSdk.ratePerBlockToAPY( updatedAsset.borrowRatePerBlock, @@ -632,23 +635,28 @@ export const PopupProvider: React.FC<{ ) : undefined, updatedTotalBorrows: updatedAssets - ? updatedAssets.reduce((acc, cur) => acc + cur.borrowBalanceFiat, 0) - : undefined + ? updatedAssets.reduce( + (acc, cur) => + acc + + (isNaN(cur.borrowBalanceFiat) ? 0 : cur.borrowBalanceFiat), + 0 + ) + : 0 }; } - // Ensure all properties are defined, even if undefined + // Default values when currentSdk is not available return { - borrowAPR: undefined, - borrowBalanceFrom: '', - borrowBalanceTo: undefined, - supplyAPY: undefined, - supplyBalanceFrom: '', - supplyBalanceTo: undefined, - totalBorrows: undefined, - updatedBorrowAPR: undefined, - updatedSupplyAPY: undefined, - updatedTotalBorrows: undefined + 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]); @@ -705,7 +713,7 @@ export const useManageDialogContext = (): PopupContextType => { const context = useContext(ManageDialogContext); if (!context) { throw new Error( - 'useManageDialogContext must be used within a PopupProvider' + 'useManageDialogContext must be used within a ManageDialogProvider' ); } return context; From c3b025219dfa4eea11b8e7838036c0b618524ae8 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Thu, 21 Nov 2024 20:28:23 +0100 Subject: [PATCH 17/66] use shadcn for swap widget --- .../ui/app/_components/markets/SwapWidget.tsx | 53 +++++-------------- 1 file changed, 13 insertions(+), 40 deletions(-) diff --git a/packages/ui/app/_components/markets/SwapWidget.tsx b/packages/ui/app/_components/markets/SwapWidget.tsx index 37128b85d..bf4003baa 100644 --- a/packages/ui/app/_components/markets/SwapWidget.tsx +++ b/packages/ui/app/_components/markets/SwapWidget.tsx @@ -1,15 +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'; - import type { Route, WidgetConfig } from '@lifi/widget'; interface IProps { @@ -22,7 +17,7 @@ interface IProps { onRouteExecutionCompleted?: (route: Route) => void; } -export default function Widget({ +export default function SwapWidget({ close, open, toChain, @@ -38,10 +33,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 +54,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 }); From 38abc4d3c3b5a3aeff27adc52a7de205ac576f5f Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Thu, 21 Nov 2024 20:32:41 +0100 Subject: [PATCH 18/66] always reset pool when switching chain --- .../ui/app/_components/markets/NetworkSelector.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/ui/app/_components/markets/NetworkSelector.tsx b/packages/ui/app/_components/markets/NetworkSelector.tsx index 56a0592ff..f24d54317 100644 --- a/packages/ui/app/_components/markets/NetworkSelector.tsx +++ b/packages/ui/app/_components/markets/NetworkSelector.tsx @@ -1,9 +1,7 @@ import React from 'react'; - import Image from 'next/image'; import Link from 'next/link'; import { usePathname, useSearchParams } from 'next/navigation'; - import { Button } from '@ui/components/ui/button'; import { Tooltip, @@ -20,6 +18,7 @@ interface INetworkSelector { enabledChains?: number[]; upcomingChains?: string[]; } + const NETWORK_ORDER = ['Mode', 'Base', 'Optimism', 'Fraxtal', 'Lisk', 'BoB']; function NetworkSelector({ @@ -33,7 +32,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 +41,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()}`; }; From 31a2cebe7e4a9a695704aae69db7caad1c2950f3 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Thu, 21 Nov 2024 22:19:48 +0100 Subject: [PATCH 19/66] fix claim rewards popover on dashboard --- .../app/_components/dashboards/InfoRows.tsx | 23 +- .../ui/app/_components/markets/APRCell.tsx | 203 +++++++----------- .../_components/markets/FlyWheelRewards.tsx | 154 ++++++++----- 3 files changed, 183 insertions(+), 197 deletions(-) diff --git a/packages/ui/app/_components/dashboards/InfoRows.tsx b/packages/ui/app/_components/dashboards/InfoRows.tsx index 3e7f097ab..68caf2f26 100644 --- a/packages/ui/app/_components/dashboards/InfoRows.tsx +++ b/packages/ui/app/_components/dashboards/InfoRows.tsx @@ -16,7 +16,7 @@ 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 APRCell from '../markets/APRCell'; @@ -75,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() @@ -165,23 +168,13 @@ const InfoRows = ({ rewards={mode === InfoMode.SUPPLY ? supplyRewards : borrowRewards} />

- {multipliers[selectedChain]?.[pool]?.[asset]?.borrow?.flywheel && - mode == InfoMode.BORROW ? ( - - ) : multipliers[selectedChain]?.[pool]?.[asset]?.supply?.flywheel && - mode == InfoMode.SUPPLY ? ( - ) : (
diff --git a/packages/ui/app/_components/markets/APRCell.tsx b/packages/ui/app/_components/markets/APRCell.tsx index 0bfafeebe..a9baf96e7 100644 --- a/packages/ui/app/_components/markets/APRCell.tsx +++ b/packages/ui/app/_components/markets/APRCell.tsx @@ -1,4 +1,3 @@ -// components/APRCell.tsx import dynamic from 'next/dynamic'; import Image from 'next/image'; import Link from 'next/link'; @@ -15,10 +14,11 @@ import { cn } from '@ui/lib/utils'; import { multipliers } from '@ui/utils/multipliers'; import type { Address } from 'viem'; - import type { FlywheelReward } from '@ionicprotocol/types'; -const Rewards = dynamic(() => import('./FlyWheelRewards'), { ssr: false }); +const FlyWheelRewards = dynamic(() => import('./FlyWheelRewards'), { + ssr: false +}); export type APRCellProps = { type: 'borrow' | 'supply'; @@ -87,6 +87,19 @@ export default function APRCell({ ); }; + const RewardRow = ({ icon, text }: { icon: string; text: string }) => ( +
+ + {text} +
+ ); + return ( @@ -153,39 +166,39 @@ export default function APRCell({
-
-
+
+
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 @@ -195,145 +208,85 @@ export default function APRCell({ )} {config?.flywheel && ( - +

+ +
)} - {/* Common rewards sections */} {(config?.ionic ?? 0) > 0 && ( <> -
- - + {config?.ionic}x Ionic Points -
-
- - + Turtle Ionic Points -
+ + )} {config?.turtle && asset === 'STONE' && ( -
- - + Stone Turtle Points -
+ )} {config?.etherfi && ( -
- - + {config.etherfi}x ether.fi Points -
+ )} {config?.kelp && ( <> -
- - + {config.kelp}x Kelp Miles -
-
- - + Turtle Kelp Points -
+ + )} {config?.eigenlayer && ( -
- - + EigenLayer Points -
+ )} {config?.spice && ( -
- - + Spice Points -
+ )} - {/* Supply-specific rewards */} {type === 'supply' && ( <> {(config?.anzen ?? 0) > 0 && ( -
- - + {config?.anzen}x Anzen Points -
+ )} {config?.nektar && ( -
- - + Nektar Points -
+ )} )} diff --git a/packages/ui/app/_components/markets/FlyWheelRewards.tsx b/packages/ui/app/_components/markets/FlyWheelRewards.tsx index d8ef6e565..5b5eb7d97 100644 --- a/packages/ui/app/_components/markets/FlyWheelRewards.tsx +++ b/packages/ui/app/_components/markets/FlyWheelRewards.tsx @@ -1,20 +1,18 @@ 'use client'; import { useState } from 'react'; - import dynamic from 'next/dynamic'; - import { formatEther, type Address } from 'viem'; import { useChainId } from 'wagmi'; - +import { cn } from '@ui/lib/utils'; +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 { 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 +20,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 +67,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 && ( -
- +
)} -
+ )} - +
); }; From 1c95a9e42ebbd10cac183ad26580b6878b4ae343 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Fri, 22 Nov 2024 10:50:50 +0100 Subject: [PATCH 20/66] lint --- packages/ui/app/_components/dashboards/InfoRows.tsx | 2 +- packages/ui/app/_components/markets/APRCell.tsx | 1 + packages/ui/app/_components/markets/FlyWheelRewards.tsx | 7 ++++++- packages/ui/app/_components/markets/NetworkSelector.tsx | 2 ++ packages/ui/app/_components/markets/SwapWidget.tsx | 3 +++ packages/ui/context/ManageDialogContext.tsx | 2 +- 6 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/ui/app/_components/dashboards/InfoRows.tsx b/packages/ui/app/_components/dashboards/InfoRows.tsx index 68caf2f26..3a768225b 100644 --- a/packages/ui/app/_components/dashboards/InfoRows.tsx +++ b/packages/ui/app/_components/dashboards/InfoRows.tsx @@ -1,6 +1,6 @@ /* eslint-disable @next/next/no-img-element */ 'use client'; -import { useMemo, useState, type Dispatch, type SetStateAction } from 'react'; +import { useMemo, type Dispatch, type SetStateAction } from 'react'; import dynamic from 'next/dynamic'; diff --git a/packages/ui/app/_components/markets/APRCell.tsx b/packages/ui/app/_components/markets/APRCell.tsx index a9baf96e7..cbed4286e 100644 --- a/packages/ui/app/_components/markets/APRCell.tsx +++ b/packages/ui/app/_components/markets/APRCell.tsx @@ -14,6 +14,7 @@ import { cn } from '@ui/lib/utils'; import { multipliers } from '@ui/utils/multipliers'; import type { Address } from 'viem'; + import type { FlywheelReward } from '@ionicprotocol/types'; const FlyWheelRewards = dynamic(() => import('./FlyWheelRewards'), { diff --git a/packages/ui/app/_components/markets/FlyWheelRewards.tsx b/packages/ui/app/_components/markets/FlyWheelRewards.tsx index 5b5eb7d97..fc2aa0353 100644 --- a/packages/ui/app/_components/markets/FlyWheelRewards.tsx +++ b/packages/ui/app/_components/markets/FlyWheelRewards.tsx @@ -1,17 +1,22 @@ 'use client'; import { useState } from 'react'; + import dynamic from 'next/dynamic'; + import { formatEther, type Address } from 'viem'; import { useChainId } from 'wagmi'; -import { cn } from '@ui/lib/utils'; + 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'; type FlyWheelRewardsProps = { diff --git a/packages/ui/app/_components/markets/NetworkSelector.tsx b/packages/ui/app/_components/markets/NetworkSelector.tsx index f24d54317..5c6da0c54 100644 --- a/packages/ui/app/_components/markets/NetworkSelector.tsx +++ b/packages/ui/app/_components/markets/NetworkSelector.tsx @@ -1,7 +1,9 @@ import React from 'react'; + import Image from 'next/image'; import Link from 'next/link'; import { usePathname, useSearchParams } from 'next/navigation'; + import { Button } from '@ui/components/ui/button'; import { Tooltip, diff --git a/packages/ui/app/_components/markets/SwapWidget.tsx b/packages/ui/app/_components/markets/SwapWidget.tsx index bf4003baa..6883e5a50 100644 --- a/packages/ui/app/_components/markets/SwapWidget.tsx +++ b/packages/ui/app/_components/markets/SwapWidget.tsx @@ -1,10 +1,13 @@ 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'; + import type { Route, WidgetConfig } from '@lifi/widget'; interface IProps { diff --git a/packages/ui/context/ManageDialogContext.tsx b/packages/ui/context/ManageDialogContext.tsx index 41cdf3c3b..bf85c73ae 100644 --- a/packages/ui/context/ManageDialogContext.tsx +++ b/packages/ui/context/ManageDialogContext.tsx @@ -93,7 +93,7 @@ interface PopupContextType { borrowBalanceTo: string | undefined; supplyAPY: number | undefined; supplyBalanceFrom: string; - supplyBalanceTo: string | number; + supplyBalanceTo: number | string; totalBorrows: number | undefined; updatedBorrowAPR: number | undefined; updatedSupplyAPY: number | undefined; From d9dfe4dae397902e1ee81963ddaf077f83f6ef4a Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Fri, 22 Nov 2024 12:28:17 +0100 Subject: [PATCH 21/66] fix market supply balance remove console log --- .../_components/manage-dialog/SupplyTab.tsx | 1 - packages/ui/context/ManageDialogContext.tsx | 21 ++++++++++--------- packages/ui/hooks/market/useMarketData.ts | 13 ++++++++---- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/ui/app/_components/manage-dialog/SupplyTab.tsx b/packages/ui/app/_components/manage-dialog/SupplyTab.tsx index def96befd..66d5d9032 100644 --- a/packages/ui/app/_components/manage-dialog/SupplyTab.tsx +++ b/packages/ui/app/_components/manage-dialog/SupplyTab.tsx @@ -52,7 +52,6 @@ const SupplyTab = ({ updatedValues, isLoadingUpdatedAssets } = useManageDialogContext(); - console.log('updatedValues', updatedValues); const isDisabled = !amount || amountAsBInt === 0n; const { currentSdk, address } = useMultiIonic(); diff --git a/packages/ui/context/ManageDialogContext.tsx b/packages/ui/context/ManageDialogContext.tsx index bf85c73ae..26cb63eae 100644 --- a/packages/ui/context/ManageDialogContext.tsx +++ b/packages/ui/context/ManageDialogContext.tsx @@ -105,6 +105,11 @@ interface PopupContextType { // Add any other shared variables or functions here } +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 ); @@ -602,19 +607,15 @@ export const ManageDialogProvider: React.FC<{ selectedMarketData.supplyRatePerBlock, blocksPerMinute ), - supplyBalanceFrom: formatBalanceValue( - selectedMarketData.supplyBalance ?? 0n, + supplyBalanceFrom: formatBalance( + selectedMarketData.supplyBalance, selectedMarketData.underlyingDecimals ), supplyBalanceTo: updatedAsset - ? Math.abs( - Number( - formatUnits( - updatedAsset.supplyBalance, - updatedAsset.underlyingDecimals - ) - ) - ) || 0 + ? formatBalance( + updatedAsset.supplyBalance, + updatedAsset.underlyingDecimals + ) : '0', totalBorrows: updatedAssets?.reduce( diff --git a/packages/ui/hooks/market/useMarketData.ts b/packages/ui/hooks/market/useMarketData.ts index 67e7c2511..5d5de97a6 100644 --- a/packages/ui/hooks/market/useMarketData.ts +++ b/packages/ui/hooks/market/useMarketData.ts @@ -88,7 +88,7 @@ export const useMarketData = ( const marketData = useMemo(() => { if (!assets) return []; - return pools[+chain].pools[+selectedPool].assets + const transformedData = pools[+chain].pools[+selectedPool].assets .map((symbol: string) => { const asset = assets.find((a) => a.underlyingSymbol === symbol); if (!asset) return null; @@ -202,6 +202,8 @@ export const useMarketData = ( }; }) .filter(Boolean) as MarketRowData[]; + + return transformedData; }, [ assets, chain, @@ -265,9 +267,12 @@ export const useMarketData = ( poolData?.comptroller ]); - const selectedMarketData = marketData.find( - (asset) => asset.asset === selectedSymbol - ); + const selectedMarketData = useMemo(() => { + const found = assets?.find( + (asset) => asset.underlyingSymbol === selectedSymbol + ); + return found; + }, [assets, selectedSymbol]); const loopProps = useMemo(() => { if (!selectedMarketData || !poolData) return null; From 81a1e9260772f84f01306d4d6e8cf49858dc5541 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Fri, 22 Nov 2024 17:06:55 +0100 Subject: [PATCH 22/66] action buttons & supply/borrow balance data --- packages/ui/app/market/page.tsx | 23 ++++-- packages/ui/hooks/market/useMarketData.ts | 92 ++++++++++++----------- 2 files changed, 65 insertions(+), 50 deletions(-) diff --git a/packages/ui/app/market/page.tsx b/packages/ui/app/market/page.tsx index 4c9aeb118..5c5b6482d 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -1,4 +1,3 @@ -// Market.tsx 'use client'; import { useState } from 'react'; @@ -139,7 +138,12 @@ export default function Market() { header: 'SUPPLY BALANCE', sortingFn: 'numerical', cell: ({ row }: MarketCellProps) => ( - {row.original.supplyBalance} +
+ {row.original.supply.balance} + + ${row.original.supply.balanceUSD} + +
) }, { @@ -147,7 +151,12 @@ export default function Market() { header: 'BORROW BALANCE', sortingFn: 'numerical', cell: ({ row }: MarketCellProps) => ( - {row.original.borrowBalance} +
+ {row.original.borrow.balance} + + ${row.original.borrow.balanceUSD} + +
) }, { @@ -163,9 +172,11 @@ export default function Market() { header: 'ACTIONS', enableSorting: false, cell: ({ row }: MarketCellProps) => ( -
+
{row.original.loopPossible && (
); + 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 ( @@ -109,7 +129,7 @@ export default function APRCell({
{showRewardsBadge && ( - - {showOPRewards ? ( -
- +{' '} - OP{' '} - REWARDS -
- ) : ( - '+ REWARDS' - )} -
+ + Rewards + +
)} {config?.turtle && !isMainModeMarket && ( { + return ( +
+ {rewards.map((reward, index) => ( +
+
+ {reward} +
+
+ ))} +
+ ); +}; From 7e4d9c6c55a7ba1961515b51d487ce9a92b047e9 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Fri, 22 Nov 2024 19:04:18 +0100 Subject: [PATCH 27/66] split loop dialog --- .../_components/loop-dialog/BorrowActions.tsx | 152 +++++++ .../loop-dialog/LoopHealthRatioDisplay.tsx | 82 ++++ .../loop-dialog/LoopInfoDisplay.tsx | 85 ++++ .../_components/loop-dialog/SupplyActions.tsx | 167 +++++++ .../Loop.tsx => loop-dialog/index.tsx} | 419 +----------------- packages/ui/app/dashboard/page.tsx | 2 +- packages/ui/app/market/page.tsx | 2 +- 7 files changed, 494 insertions(+), 415 deletions(-) create mode 100644 packages/ui/app/_components/loop-dialog/BorrowActions.tsx create mode 100644 packages/ui/app/_components/loop-dialog/LoopHealthRatioDisplay.tsx create mode 100644 packages/ui/app/_components/loop-dialog/LoopInfoDisplay.tsx create mode 100644 packages/ui/app/_components/loop-dialog/SupplyActions.tsx rename packages/ui/app/_components/{manage-dialog/Loop.tsx => loop-dialog/index.tsx} (70%) diff --git a/packages/ui/app/_components/loop-dialog/BorrowActions.tsx b/packages/ui/app/_components/loop-dialog/BorrowActions.tsx new file mode 100644 index 000000000..4e8fc04e8 --- /dev/null +++ b/packages/ui/app/_components/loop-dialog/BorrowActions.tsx @@ -0,0 +1,152 @@ +import type { Dispatch, SetStateAction } from 'react'; +import React from 'react'; + +import { type Address } from 'viem'; +import { useChainId } from 'wagmi'; + +import { useFusePoolData } from '@ui/hooks/useFusePoolData'; +import type { MarketData } from '@ui/types/TokensDataMap'; + +import Amount from '../manage-dialog/Amount'; +import Range from '../Range'; +import ResultHandler from '../ResultHandler'; + +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 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 BorrowActions; diff --git a/packages/ui/app/_components/loop-dialog/LoopHealthRatioDisplay.tsx b/packages/ui/app/_components/loop-dialog/LoopHealthRatioDisplay.tsx new file mode 100644 index 000000000..3541c112d --- /dev/null +++ b/packages/ui/app/_components/loop-dialog/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/loop-dialog/LoopInfoDisplay.tsx b/packages/ui/app/_components/loop-dialog/LoopInfoDisplay.tsx new file mode 100644 index 000000000..0cc957c37 --- /dev/null +++ b/packages/ui/app/_components/loop-dialog/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/loop-dialog/SupplyActions.tsx b/packages/ui/app/_components/loop-dialog/SupplyActions.tsx new file mode 100644 index 000000000..81eeea2d4 --- /dev/null +++ b/packages/ui/app/_components/loop-dialog/SupplyActions.tsx @@ -0,0 +1,167 @@ +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-dialog/Amount'; +import SliderComponent from '../manage-dialog/Slider'; + +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 + )} + selectedMarketData={selectedCollateralAsset} + symbol={selectedCollateralAsset.underlyingSymbol} + /> + +
+ $ + {( + 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/manage-dialog/Loop.tsx b/packages/ui/app/_components/loop-dialog/index.tsx similarity index 70% rename from packages/ui/app/_components/manage-dialog/Loop.tsx rename to packages/ui/app/_components/loop-dialog/index.tsx index 23b21d780..538b3cebb 100644 --- a/packages/ui/app/_components/manage-dialog/Loop.tsx +++ b/packages/ui/app/_components/loop-dialog/index.tsx @@ -1,5 +1,4 @@ -import type { Dispatch, SetStateAction } from 'react'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import dynamic from 'next/dynamic'; import Image from 'next/image'; @@ -25,20 +24,20 @@ 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'; +} from '../manage-dialog/TransactionStepsHandler'; import Modal from '../Modal'; -import Range from '../Range'; import ResultHandler from '../ResultHandler'; import type { OpenPosition } from '@ionicprotocol/types'; +import BorrowActions from './BorrowActions'; +import LoopHealthRatioDisplay from './LoopHealthRatioDisplay'; +import LoopInfoDisplay from './LoopInfoDisplay'; +import SupplyActions from './SupplyActions'; const SwapWidget = dynamic(() => import('../markets/SwapWidget'), { ssr: false @@ -53,412 +52,6 @@ export type LoopProps = { 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, diff --git a/packages/ui/app/dashboard/page.tsx b/packages/ui/app/dashboard/page.tsx index 0ea75627e..c6c359ab1 100644 --- a/packages/ui/app/dashboard/page.tsx +++ b/packages/ui/app/dashboard/page.tsx @@ -34,7 +34,7 @@ import CollateralSwapPopup from '../_components/dashboards/CollateralSwapPopup'; import InfoRows, { InfoMode } from '../_components/dashboards/InfoRows'; import LoopRewards from '../_components/dashboards/LoopRewards'; import ManageDialog from '../_components/manage-dialog'; -import Loop from '../_components/manage-dialog/Loop'; +import Loop from '../_components/loop-dialog'; import NetworkSelector from '../_components/markets/NetworkSelector'; import ResultHandler from '../_components/ResultHandler'; diff --git a/packages/ui/app/market/page.tsx b/packages/ui/app/market/page.tsx index f2258c5b7..f38c98a6a 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -17,7 +17,7 @@ import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; import CommonTable from '../_components/CommonTable'; import ManageDialog from '../_components/manage-dialog'; -import Loop from '../_components/manage-dialog/Loop'; +import Loop from '../_components/loop-dialog'; import Swap from '../_components/manage-dialog/Swap'; import APRCell from '../_components/markets/APRCell'; import FeaturedMarketTile from '../_components/markets/FeaturedMarketTile'; From 5653d352e60292821ff51bfb169bc85d779926a7 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Fri, 22 Nov 2024 19:07:30 +0100 Subject: [PATCH 28/66] move dialogs to their own folder --- .../dashboards/CollateralSwapPopup.tsx | 4 ++-- .../loop}/BorrowActions.tsx | 6 +++--- .../loop}/LoopHealthRatioDisplay.tsx | 0 .../loop}/LoopInfoDisplay.tsx | 2 +- .../loop}/SupplyActions.tsx | 4 ++-- .../{loop-dialog => dialogs/loop}/index.tsx | 16 ++++++++-------- .../{manage-dialog => dialogs/manage}/Amount.tsx | 2 +- .../manage}/Approved.tsx | 0 .../manage}/BorrowTab.tsx | 2 +- .../manage}/DonutChart.tsx | 0 .../manage}/RepayTab.tsx | 2 +- .../{manage-dialog => dialogs/manage}/Slider.tsx | 0 .../manage}/SupplyTab.tsx | 2 +- .../{manage-dialog => dialogs/manage}/Swap.tsx | 4 ++-- .../manage}/TransactionStepsHandler.tsx | 0 .../manage}/WithdrawTab.tsx | 2 +- .../{manage-dialog => dialogs/manage}/index.tsx | 2 +- packages/ui/app/dashboard/page.tsx | 6 +++--- packages/ui/app/market/details/[asset]/page.tsx | 4 ++-- packages/ui/app/market/page.tsx | 6 +++--- packages/ui/app/stake/page.tsx | 2 +- packages/ui/context/ManageDialogContext.tsx | 4 ++-- 22 files changed, 35 insertions(+), 35 deletions(-) rename packages/ui/app/_components/{loop-dialog => dialogs/loop}/BorrowActions.tsx (97%) rename packages/ui/app/_components/{loop-dialog => dialogs/loop}/LoopHealthRatioDisplay.tsx (100%) rename packages/ui/app/_components/{loop-dialog => dialogs/loop}/LoopInfoDisplay.tsx (97%) rename packages/ui/app/_components/{loop-dialog => dialogs/loop}/SupplyActions.tsx (97%) rename packages/ui/app/_components/{loop-dialog => dialogs/loop}/index.tsx (99%) rename packages/ui/app/_components/{manage-dialog => dialogs/manage}/Amount.tsx (99%) rename packages/ui/app/_components/{manage-dialog => dialogs/manage}/Approved.tsx (100%) rename packages/ui/app/_components/{manage-dialog => dialogs/manage}/BorrowTab.tsx (99%) rename packages/ui/app/_components/{manage-dialog => dialogs/manage}/DonutChart.tsx (100%) rename packages/ui/app/_components/{manage-dialog => dialogs/manage}/RepayTab.tsx (99%) rename packages/ui/app/_components/{manage-dialog => dialogs/manage}/Slider.tsx (100%) rename packages/ui/app/_components/{manage-dialog => dialogs/manage}/SupplyTab.tsx (99%) rename packages/ui/app/_components/{manage-dialog => dialogs/manage}/Swap.tsx (99%) rename packages/ui/app/_components/{manage-dialog => dialogs/manage}/TransactionStepsHandler.tsx (100%) rename packages/ui/app/_components/{manage-dialog => dialogs/manage}/WithdrawTab.tsx (99%) rename packages/ui/app/_components/{manage-dialog => dialogs/manage}/index.tsx (99%) diff --git a/packages/ui/app/_components/dashboards/CollateralSwapPopup.tsx b/packages/ui/app/_components/dashboards/CollateralSwapPopup.tsx index 9fcfee70c..be98917b9 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/manage-dialog/Slider'; +import SliderComponent from '@ui/app/_components/dialogs/manage/Slider'; import TransactionStepsHandler, { useTransactionSteps -} from '@ui/app/_components/manage-dialog/TransactionStepsHandler'; +} from '@ui/app/_components/dialogs/manage/TransactionStepsHandler'; import type { IBal } from '@ui/app/_components/stake/MaxDeposit'; import { donutoptions, getDonutData } from '@ui/app/_constants/mock'; import { INFO_MESSAGES } from '@ui/constants/index'; diff --git a/packages/ui/app/_components/loop-dialog/BorrowActions.tsx b/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx similarity index 97% rename from packages/ui/app/_components/loop-dialog/BorrowActions.tsx rename to packages/ui/app/_components/dialogs/loop/BorrowActions.tsx index 4e8fc04e8..aa7b1a97b 100644 --- a/packages/ui/app/_components/loop-dialog/BorrowActions.tsx +++ b/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx @@ -7,9 +7,9 @@ import { useChainId } from 'wagmi'; import { useFusePoolData } from '@ui/hooks/useFusePoolData'; import type { MarketData } from '@ui/types/TokensDataMap'; -import Amount from '../manage-dialog/Amount'; -import Range from '../Range'; -import ResultHandler from '../ResultHandler'; +import Range from '../../Range'; +import ResultHandler from '../../ResultHandler'; +import Amount from '../manage/Amount'; export type LoopProps = { borrowableAssets: Address[]; diff --git a/packages/ui/app/_components/loop-dialog/LoopHealthRatioDisplay.tsx b/packages/ui/app/_components/dialogs/loop/LoopHealthRatioDisplay.tsx similarity index 100% rename from packages/ui/app/_components/loop-dialog/LoopHealthRatioDisplay.tsx rename to packages/ui/app/_components/dialogs/loop/LoopHealthRatioDisplay.tsx diff --git a/packages/ui/app/_components/loop-dialog/LoopInfoDisplay.tsx b/packages/ui/app/_components/dialogs/loop/LoopInfoDisplay.tsx similarity index 97% rename from packages/ui/app/_components/loop-dialog/LoopInfoDisplay.tsx rename to packages/ui/app/_components/dialogs/loop/LoopInfoDisplay.tsx index 0cc957c37..51b0f622e 100644 --- a/packages/ui/app/_components/loop-dialog/LoopInfoDisplay.tsx +++ b/packages/ui/app/_components/dialogs/loop/LoopInfoDisplay.tsx @@ -6,7 +6,7 @@ import { type Address } from 'viem'; import type { MarketData } from '@ui/types/TokensDataMap'; -import ResultHandler from '../ResultHandler'; +import ResultHandler from '../../ResultHandler'; export type LoopProps = { borrowableAssets: Address[]; diff --git a/packages/ui/app/_components/loop-dialog/SupplyActions.tsx b/packages/ui/app/_components/dialogs/loop/SupplyActions.tsx similarity index 97% rename from packages/ui/app/_components/loop-dialog/SupplyActions.tsx rename to packages/ui/app/_components/dialogs/loop/SupplyActions.tsx index 81eeea2d4..3a5b45fc8 100644 --- a/packages/ui/app/_components/loop-dialog/SupplyActions.tsx +++ b/packages/ui/app/_components/dialogs/loop/SupplyActions.tsx @@ -6,8 +6,8 @@ import { useChainId } from 'wagmi'; import { useMaxSupplyAmount } from '@ui/hooks/useMaxSupplyAmount'; import type { MarketData } from '@ui/types/TokensDataMap'; -import Amount from '../manage-dialog/Amount'; -import SliderComponent from '../manage-dialog/Slider'; +import Amount from '../manage/Amount'; +import SliderComponent from '../manage/Slider'; export type LoopProps = { borrowableAssets: Address[]; diff --git a/packages/ui/app/_components/loop-dialog/index.tsx b/packages/ui/app/_components/dialogs/loop/index.tsx similarity index 99% rename from packages/ui/app/_components/loop-dialog/index.tsx rename to packages/ui/app/_components/dialogs/loop/index.tsx index 538b3cebb..98822b45c 100644 --- a/packages/ui/app/_components/loop-dialog/index.tsx +++ b/packages/ui/app/_components/dialogs/loop/index.tsx @@ -27,19 +27,19 @@ import { useFusePoolData } from '@ui/hooks/useFusePoolData'; import type { MarketData } from '@ui/types/TokensDataMap'; import { getScanUrlByChainId } from '@ui/utils/networkData'; -import TransactionStepsHandler, { - useTransactionSteps -} from '../manage-dialog/TransactionStepsHandler'; -import Modal from '../Modal'; -import ResultHandler from '../ResultHandler'; - -import type { OpenPosition } from '@ionicprotocol/types'; import BorrowActions from './BorrowActions'; import LoopHealthRatioDisplay from './LoopHealthRatioDisplay'; import LoopInfoDisplay from './LoopInfoDisplay'; import SupplyActions from './SupplyActions'; +import Modal from '../../Modal'; +import ResultHandler from '../../ResultHandler'; +import TransactionStepsHandler, { + useTransactionSteps +} from '../manage/TransactionStepsHandler'; + +import type { OpenPosition } from '@ionicprotocol/types'; -const SwapWidget = dynamic(() => import('../markets/SwapWidget'), { +const SwapWidget = dynamic(() => import('../../markets/SwapWidget'), { ssr: false }); diff --git a/packages/ui/app/_components/manage-dialog/Amount.tsx b/packages/ui/app/_components/dialogs/manage/Amount.tsx similarity index 99% rename from packages/ui/app/_components/manage-dialog/Amount.tsx rename to packages/ui/app/_components/dialogs/manage/Amount.tsx index eb1e360ec..7f31b4225 100644 --- a/packages/ui/app/_components/manage-dialog/Amount.tsx +++ b/packages/ui/app/_components/dialogs/manage/Amount.tsx @@ -9,7 +9,7 @@ import { parseUnits } from 'viem'; import type { MarketData } from '@ui/types/TokensDataMap'; -import ResultHandler from '../ResultHandler'; +import ResultHandler from '../../ResultHandler'; interface IAmount { amount?: string; diff --git a/packages/ui/app/_components/manage-dialog/Approved.tsx b/packages/ui/app/_components/dialogs/manage/Approved.tsx similarity index 100% rename from packages/ui/app/_components/manage-dialog/Approved.tsx rename to packages/ui/app/_components/dialogs/manage/Approved.tsx diff --git a/packages/ui/app/_components/manage-dialog/BorrowTab.tsx b/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx similarity index 99% rename from packages/ui/app/_components/manage-dialog/BorrowTab.tsx rename to packages/ui/app/_components/dialogs/manage/BorrowTab.tsx index b2d66b0a4..98eb7c4c4 100644 --- a/packages/ui/app/_components/manage-dialog/BorrowTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx @@ -17,7 +17,7 @@ import SliderComponent from './Slider'; import TransactionStepsHandler, { useTransactionSteps } from './TransactionStepsHandler'; -import ResultHandler from '../ResultHandler'; +import ResultHandler from '../../ResultHandler'; interface BorrowTabProps { maxAmount: bigint; diff --git a/packages/ui/app/_components/manage-dialog/DonutChart.tsx b/packages/ui/app/_components/dialogs/manage/DonutChart.tsx similarity index 100% rename from packages/ui/app/_components/manage-dialog/DonutChart.tsx rename to packages/ui/app/_components/dialogs/manage/DonutChart.tsx diff --git a/packages/ui/app/_components/manage-dialog/RepayTab.tsx b/packages/ui/app/_components/dialogs/manage/RepayTab.tsx similarity index 99% rename from packages/ui/app/_components/manage-dialog/RepayTab.tsx rename to packages/ui/app/_components/dialogs/manage/RepayTab.tsx index 694263547..8c6440afb 100644 --- a/packages/ui/app/_components/manage-dialog/RepayTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/RepayTab.tsx @@ -14,7 +14,7 @@ import SliderComponent from './Slider'; import TransactionStepsHandler, { useTransactionSteps } from './TransactionStepsHandler'; -import ResultHandler from '../ResultHandler'; +import ResultHandler from '../../ResultHandler'; interface RepayTabProps { maxAmount: bigint; diff --git a/packages/ui/app/_components/manage-dialog/Slider.tsx b/packages/ui/app/_components/dialogs/manage/Slider.tsx similarity index 100% rename from packages/ui/app/_components/manage-dialog/Slider.tsx rename to packages/ui/app/_components/dialogs/manage/Slider.tsx diff --git a/packages/ui/app/_components/manage-dialog/SupplyTab.tsx b/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx similarity index 99% rename from packages/ui/app/_components/manage-dialog/SupplyTab.tsx rename to packages/ui/app/_components/dialogs/manage/SupplyTab.tsx index 66d5d9032..606a38df1 100644 --- a/packages/ui/app/_components/manage-dialog/SupplyTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx @@ -14,7 +14,7 @@ import SliderComponent from './Slider'; import TransactionStepsHandler, { useTransactionSteps } from './TransactionStepsHandler'; -import ResultHandler from '../ResultHandler'; +import ResultHandler from '../../ResultHandler'; interface SupplyTabProps { maxAmount: bigint; diff --git a/packages/ui/app/_components/manage-dialog/Swap.tsx b/packages/ui/app/_components/dialogs/manage/Swap.tsx similarity index 99% rename from packages/ui/app/_components/manage-dialog/Swap.tsx rename to packages/ui/app/_components/dialogs/manage/Swap.tsx index 58918d4f6..419815216 100644 --- a/packages/ui/app/_components/manage-dialog/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/manage-dialog/TransactionStepsHandler.tsx b/packages/ui/app/_components/dialogs/manage/TransactionStepsHandler.tsx similarity index 100% rename from packages/ui/app/_components/manage-dialog/TransactionStepsHandler.tsx rename to packages/ui/app/_components/dialogs/manage/TransactionStepsHandler.tsx diff --git a/packages/ui/app/_components/manage-dialog/WithdrawTab.tsx b/packages/ui/app/_components/dialogs/manage/WithdrawTab.tsx similarity index 99% rename from packages/ui/app/_components/manage-dialog/WithdrawTab.tsx rename to packages/ui/app/_components/dialogs/manage/WithdrawTab.tsx index a36b9bcad..878196ee8 100644 --- a/packages/ui/app/_components/manage-dialog/WithdrawTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/WithdrawTab.tsx @@ -15,7 +15,7 @@ import SliderComponent from './Slider'; import TransactionStepsHandler, { useTransactionSteps } from './TransactionStepsHandler'; -import ResultHandler from '../ResultHandler'; +import ResultHandler from '../../ResultHandler'; interface WithdrawTabProps { maxAmount: bigint; diff --git a/packages/ui/app/_components/manage-dialog/index.tsx b/packages/ui/app/_components/dialogs/manage/index.tsx similarity index 99% rename from packages/ui/app/_components/manage-dialog/index.tsx rename to packages/ui/app/_components/dialogs/manage/index.tsx index 5de5533e3..d06f3efbe 100644 --- a/packages/ui/app/_components/manage-dialog/index.tsx +++ b/packages/ui/app/_components/dialogs/manage/index.tsx @@ -30,7 +30,7 @@ import RepayTab from './RepayTab'; import SupplyTab from './SupplyTab'; import WithdrawTab from './WithdrawTab'; -const SwapWidget = dynamic(() => import('../markets/SwapWidget'), { +const SwapWidget = dynamic(() => import('../../markets/SwapWidget'), { ssr: false }); diff --git a/packages/ui/app/dashboard/page.tsx b/packages/ui/app/dashboard/page.tsx index c6c359ab1..8f2cc8428 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 ManageDialog from '../_components/manage-dialog'; -import Loop from '../_components/loop-dialog'; +import Loop from '../_components/dialogs/loop'; +import ManageDialog from '../_components/dialogs/manage'; import NetworkSelector from '../_components/markets/NetworkSelector'; import ResultHandler from '../_components/ResultHandler'; -import type { ActiveTab } from '../_components/manage-dialog'; +import type { ActiveTab } from '../_components/dialogs/manage'; import type { FlywheelReward, diff --git a/packages/ui/app/market/details/[asset]/page.tsx b/packages/ui/app/market/details/[asset]/page.tsx index c7a2616eb..41a449dff 100644 --- a/packages/ui/app/market/details/[asset]/page.tsx +++ b/packages/ui/app/market/details/[asset]/page.tsx @@ -61,7 +61,7 @@ import { import { INFO } from '@ui/constants/index'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; -import Swap from '@ui/app/_components/manage-dialog/Swap'; +import Swap from '@ui/app/_components/dialogs/manage/Swap'; import { MarketData, PoolData } from '@ui/types/TokensDataMap'; import { useFusePoolData } from '@ui/hooks/useFusePoolData'; import { useLoopMarkets } from '@ui/hooks/useLoopMarkets'; @@ -71,7 +71,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/manage-dialog'; +import ManageDialog from '@ui/app/_components/dialogs/manage'; // import { useBorrowAPYs } from '@ui/hooks/useBorrowAPYs'; // import { useSupplyAPYs } from '@ui/hooks/useSupplyAPYs'; diff --git a/packages/ui/app/market/page.tsx b/packages/ui/app/market/page.tsx index f38c98a6a..37a15407b 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -16,9 +16,9 @@ import { useMarketData } from '@ui/hooks/market/useMarketData'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; import CommonTable from '../_components/CommonTable'; -import ManageDialog from '../_components/manage-dialog'; -import Loop from '../_components/loop-dialog'; -import Swap from '../_components/manage-dialog/Swap'; +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 StakingTile from '../_components/markets/StakingTile'; diff --git a/packages/ui/app/stake/page.tsx b/packages/ui/app/stake/page.tsx index 540b27e41..0ba5ee2a7 100644 --- a/packages/ui/app/stake/page.tsx +++ b/packages/ui/app/stake/page.tsx @@ -38,7 +38,7 @@ import { } from '@ui/utils/getStakingTokens'; import { handleSwitchOriginChain } from '@ui/utils/NetworkChecker'; -import SliderComponent from '../_components/manage-dialog/Slider'; +import SliderComponent from '../_components/dialogs/manage/Slider'; import ResultHandler from '../_components/ResultHandler'; import ClaimRewards from '../_components/stake/ClaimRewards'; import MaxDeposit from '../_components/stake/MaxDeposit'; diff --git a/packages/ui/context/ManageDialogContext.tsx b/packages/ui/context/ManageDialogContext.tsx index 26cb63eae..ae92fac0f 100644 --- a/packages/ui/context/ManageDialogContext.tsx +++ b/packages/ui/context/ManageDialogContext.tsx @@ -20,8 +20,8 @@ import { } from 'viem'; import { useChainId } from 'wagmi'; -import type { TransactionStep } from '@ui/app/_components/manage-dialog/TransactionStepsHandler'; -import { useTransactionSteps } from '@ui/app/_components/manage-dialog/TransactionStepsHandler'; +import type { TransactionStep } from '@ui/app/_components/dialogs/manage/TransactionStepsHandler'; +import { useTransactionSteps } from '@ui/app/_components/dialogs/manage/TransactionStepsHandler'; import { INFO_MESSAGES } from '@ui/constants/index'; import { useMultiIonic } from '@ui/context/MultiIonicContext'; import useUpdatedUserAssets from '@ui/hooks/ionic/useUpdatedUserAssets'; From 4d2430f4d5c472bc9aa68ae549822b84edd13e1a Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Fri, 22 Nov 2024 19:19:34 +0100 Subject: [PATCH 29/66] use shadcn for Loop dialog --- packages/ui/app/_components/Modal.tsx | 58 -- .../ui/app/_components/dialogs/loop/index.tsx | 552 +++++++++--------- packages/ui/app/dashboard/page.tsx | 10 +- packages/ui/app/market/page.tsx | 2 +- 4 files changed, 283 insertions(+), 339 deletions(-) delete mode 100644 packages/ui/app/_components/Modal.tsx 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/dialogs/loop/index.tsx b/packages/ui/app/_components/dialogs/loop/index.tsx index 98822b45c..4642de872 100644 --- a/packages/ui/app/_components/dialogs/loop/index.tsx +++ b/packages/ui/app/_components/dialogs/loop/index.tsx @@ -14,6 +14,12 @@ import { } 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'; @@ -31,7 +37,6 @@ import BorrowActions from './BorrowActions'; import LoopHealthRatioDisplay from './LoopHealthRatioDisplay'; import LoopInfoDisplay from './LoopInfoDisplay'; import SupplyActions from './SupplyActions'; -import Modal from '../../Modal'; import ResultHandler from '../../ResultHandler'; import TransactionStepsHandler, { useTransactionSteps @@ -44,21 +49,21 @@ const SwapWidget = dynamic(() => import('../../markets/SwapWidget'), { }); export type LoopProps = { + isOpen: boolean; + setIsOpen: (open: boolean) => void; borrowableAssets: Address[]; - closeLoop: () => void; comptrollerAddress: Address; currentBorrowAsset?: MarketData; - isOpen: boolean; selectedCollateralAsset: MarketData; }; export default function Loop({ + isOpen, + setIsOpen, borrowableAssets, - closeLoop, comptrollerAddress, currentBorrowAsset, - selectedCollateralAsset, - isOpen + selectedCollateralAsset }: LoopProps) { const chainId = useChainId(); const [amount, setAmount] = useState(); @@ -570,82 +575,87 @@ export default function Loop({ return ( <> - {isOpen && ( - <> - setSwapWidgetOpen(false)} - open={swapWidgetOpen} - fromChain={chainId} - toChain={chainId} - toToken={selectedCollateralAsset.underlyingToken} - /> - closeLoop()}> -
- - - {selectedCollateralAsset.underlyingSymbol} -
- -
- + 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 + - Position Value - - - ${positionValueMillified} - - -
-
+ ${positionValueMillified} + + +
+
+ Net APR + - Net APR - - - {positionNetApy?.toFixed(2) ?? '0.00'}% - - -
- - {/*
+ {positionNetApy?.toFixed(2) ?? '0.00'}% + + +
+ + {/*
Annual yield @@ -653,32 +663,31 @@ export default function Loop({ TODO
*/} -
- -
- -
- - 0 || - (!!currentPositionLeverageRatio && - Math.round(currentPositionLeverageRatio) !== - currentLeverage) - ? projectedHealthRatio - : undefined - } - />
-
- -
- + +
+ + 0 || + (!!currentPositionLeverageRatio && + Math.round(currentPositionLeverageRatio) !== currentLeverage) + ? projectedHealthRatio + : undefined + } + /> +
+ +
+ +
+ - -
- -
- - + +
+ +
+ + -
- -
+ aprText={'Borrow APR'} + isLoading={isFetchingPositionInfo} + nativeAmount={borrowedAssetAmount.toFixed( + selectedBorrowAsset?.underlyingDecimals ?? 18 + )} + symbol={selectedBorrowAsset?.underlyingSymbol ?? ''} + title={'My Borrow'} + usdAmount={millify( + borrowedAssetAmount * selectedBorrowAssetUSDPrice + )} + /> +
+ +
+ + {currentPosition && ( + <> +
+ + +
+ +
+ + +
- {currentPosition && ( +
+ + )} + +
+ + +
+ +
+ + +
+ +
+ <> -
- - -
- -
- - -
- -
- - )} - -
- - -
- -
- - -
- -
- - <> - {transactionSteps.length > 0 ? ( -
- -
- ) : ( - <> - {currentPosition ? ( -
- - - -
- ) : ( + {transactionSteps.length > 0 ? ( +
+ +
+ ) : ( + <> + {currentPosition ? ( +
- )} - - )} - - -
- - - )} + + +
+ ) : ( + + )} + + )} + + +
+ +
); } diff --git a/packages/ui/app/dashboard/page.tsx b/packages/ui/app/dashboard/page.tsx index 8f2cc8428..a392100bb 100644 --- a/packages/ui/app/dashboard/page.tsx +++ b/packages/ui/app/dashboard/page.tsx @@ -174,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 @@ -720,7 +720,7 @@ export default function Dashboard() { usdPrice={usdPrice ?? undefined} setSelectedLoopBorrowData={setSelectedLoopBorrowData} setSelectedSymbol={setSelectedSymbol} - setLoopOpen={setLoopOpen} + setLoopOpen={setIsLoopDialogOpen} chain={+chain} /> ); @@ -739,12 +739,10 @@ export default function Dashboard() { {selectedMarketData && ( { - setLoopOpen(false); - }} + isOpen={isLoopDialogOpen} + setIsOpen={setIsLoopDialogOpen} comptrollerAddress={marketData?.comptroller ?? ('' as Address)} currentBorrowAsset={selectedLoopBorrowData} - isOpen={loopOpen} selectedCollateralAsset={selectedMarketData} /> )} diff --git a/packages/ui/app/market/page.tsx b/packages/ui/app/market/page.tsx index 37a15407b..79eb7cc12 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -293,7 +293,7 @@ export default function Market() { {loopProps && ( setIsLoopDialogOpen(false)} + setIsOpen={setIsLoopDialogOpen} isOpen={isLoopDialogOpen} /> )} From cd261839766f59dcec6a304d1612069ae4db6f5b Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Fri, 22 Nov 2024 19:56:21 +0100 Subject: [PATCH 30/66] fix dialog styling lint fix --- .../ui/app/_components/dialogs/loop/index.tsx | 6 +- .../app/_components/dialogs/manage/index.tsx | 6 +- packages/ui/components/ui/dialog.tsx | 135 +++++++++++++----- 3 files changed, 112 insertions(+), 35 deletions(-) diff --git a/packages/ui/app/_components/dialogs/loop/index.tsx b/packages/ui/app/_components/dialogs/loop/index.tsx index 4642de872..ad5fc289a 100644 --- a/packages/ui/app/_components/dialogs/loop/index.tsx +++ b/packages/ui/app/_components/dialogs/loop/index.tsx @@ -586,7 +586,11 @@ export default function Loop({ open={isOpen} onOpenChange={setIsOpen} > - +
diff --git a/packages/ui/app/_components/dialogs/manage/index.tsx b/packages/ui/app/_components/dialogs/manage/index.tsx index d06f3efbe..12a4cbb23 100644 --- a/packages/ui/app/_components/dialogs/manage/index.tsx +++ b/packages/ui/app/_components/dialogs/manage/index.tsx @@ -176,7 +176,11 @@ const ManageDialog = ({ open={isOpen} onOpenChange={setIsOpen} > - +
modlogo { + 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, From dc22b5277090cb44e45823d6eb5978f50ca702f4 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Mon, 25 Nov 2024 16:46:34 +0100 Subject: [PATCH 31/66] ui updates lint fix --- packages/ui/app/_components/CommonTable.tsx | 38 ++- packages/ui/app/_components/ResultHandler.tsx | 4 +- .../_components/dialogs/manage/BorrowTab.tsx | 23 +- .../_components/dialogs/manage/RepayTab.tsx | 20 +- .../app/_components/dialogs/manage/Slider.tsx | 40 +-- .../_components/dialogs/manage/SupplyTab.tsx | 84 ++++--- .../dialogs/manage/WithdrawTab.tsx | 23 +- .../app/_components/dialogs/manage/index.tsx | 148 ++++++----- packages/ui/components/ui/tooltip.tsx | 7 +- packages/ui/context/ManageDialogContext.tsx | 234 +++++++++--------- 10 files changed, 366 insertions(+), 255 deletions(-) diff --git a/packages/ui/app/_components/CommonTable.tsx b/packages/ui/app/_components/CommonTable.tsx index 9cd968911..0c0eb2990 100644 --- a/packages/ui/app/_components/CommonTable.tsx +++ b/packages/ui/app/_components/CommonTable.tsx @@ -52,7 +52,7 @@ export type EnhancedColumnDef = Omit, 'sortingFn'> & { header: string; sortingFn?: SortingType | ((rowA: any, rowB: any) => number); enableSorting?: boolean; - accessorFn?: (row: T) => any; // Allow custom accessor function to be passed + accessorFn?: (row: T) => any; }; interface CommonTableProps { @@ -103,10 +103,19 @@ const SortableHeader = ({ function CommonTable({ data, columns, - isLoading = false, + isLoading: externalIsLoading = false, renderRow }: CommonTableProps) { const [sorting, setSorting] = useState([]); + const [hasInitialized, setHasInitialized] = useState(false); + + // Consider the table as loading if it hasn't initialized yet or if external loading state is true + const isLoading = !hasInitialized || externalIsLoading; + + // Once we get data for the first time, mark as initialized + if (!hasInitialized && data.length > 0) { + setHasInitialized(true); + } const processedColumns = columns.map( (col): ColumnDef => ({ @@ -137,7 +146,10 @@ function CommonTable({ }); return ( - + {table.getHeaderGroups().map((headerGroup) => ( @@ -162,7 +174,16 @@ function CommonTable({ ))} - {table.getRowModel().rows?.length ? ( + {!isLoading && table.getRowModel().rows?.length === 0 ? ( + + + No results. + + + ) : ( table.getRowModel().rows.map((row) => renderRow ? ( renderRow(row) @@ -182,15 +203,6 @@ function CommonTable({ ) ) - ) : ( - - - No results. - - )}
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/dialogs/manage/BorrowTab.tsx b/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx index 98eb7c4c4..f44929466 100644 --- a/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx @@ -4,6 +4,7 @@ import { formatUnits } from 'viem'; import { Alert, AlertDescription } from '@ui/components/ui/alert'; import { Button } from '@ui/components/ui/button'; +import { Separator } from '@ui/components/ui/separator'; import { INFO_MESSAGES } from '@ui/constants'; import { HFPStatus, @@ -204,6 +205,8 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { )} + +
MIN BORROW @@ -220,7 +223,11 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => {
{updatedValues.borrowBalanceFrom} → - + {updatedValues.borrowBalanceTo}
@@ -231,7 +238,11 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => {
{updatedValues.borrowAPR?.toFixed(2)}% → - + {updatedValues.updatedBorrowAPR?.toFixed(2)}%
@@ -242,13 +253,19 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => {
{healthFactor.current} → - + {healthFactor.predicted}
+ + {totalStats && (
diff --git a/packages/ui/app/_components/dialogs/manage/RepayTab.tsx b/packages/ui/app/_components/dialogs/manage/RepayTab.tsx index 8c6440afb..9a293a14e 100644 --- a/packages/ui/app/_components/dialogs/manage/RepayTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/RepayTab.tsx @@ -5,6 +5,7 @@ import { formatUnits } from 'viem'; import { Alert, AlertDescription } from '@ui/components/ui/alert'; import { Button } from '@ui/components/ui/button'; +import { Separator } from '@ui/components/ui/separator'; import { INFO_MESSAGES } from '@ui/constants'; import { useManageDialogContext } from '@ui/context/ManageDialogContext'; import { useMultiIonic } from '@ui/context/MultiIonicContext'; @@ -196,6 +197,7 @@ const RepayTab = ({ maxAmount, isLoadingMax }: RepayTabProps) => { Health factor too low. )} +
@@ -205,7 +207,11 @@ const RepayTab = ({ maxAmount, isLoadingMax }: RepayTabProps) => { {updatedValues.borrowBalanceFrom} → - + {updatedValues.borrowBalanceTo} @@ -218,7 +224,11 @@ const RepayTab = ({ maxAmount, isLoadingMax }: RepayTabProps) => {
{updatedValues.borrowAPR?.toFixed(2)}% → - + {updatedValues.updatedBorrowAPR?.toFixed(2)}%
@@ -229,7 +239,11 @@ const RepayTab = ({ maxAmount, isLoadingMax }: RepayTabProps) => {
{healthFactor.current} → - + {healthFactor.predicted}
diff --git a/packages/ui/app/_components/dialogs/manage/Slider.tsx b/packages/ui/app/_components/dialogs/manage/Slider.tsx index 0cc0fb9cc..c31b64582 100644 --- a/packages/ui/app/_components/dialogs/manage/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/SupplyTab.tsx b/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx index 606a38df1..0319c87a1 100644 --- a/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx @@ -3,6 +3,7 @@ import toast from 'react-hot-toast'; import { formatUnits } from 'viem'; import { Button } from '@ui/components/ui/button'; +import { Separator } from '@ui/components/ui/separator'; import { Switch } from '@ui/components/ui/switch'; import { INFO_MESSAGES } from '@ui/constants'; import { useManageDialogContext } from '@ui/context/ManageDialogContext'; @@ -239,12 +240,18 @@ const SupplyTab = ({ {collateralApr}%
-
+ + +
Market Supply Balance
{updatedValues.supplyBalanceFrom} → - + {updatedValues.supplyBalanceTo}
@@ -255,49 +262,58 @@ const SupplyTab = ({
{updatedValues.supplyAPY?.toFixed(2)}% → - + {updatedValues.updatedSupplyAPY?.toFixed(2)}%
- -
- - Enable Collateral - - 0 || !selectedMarketData.supplyBalance - } - /> -
{totalStats && ( -
-
- -
-
-
Total Supplied:
-
- - {millify(totalStats.totalAmount)} of{' '} - {millify(totalStats.capAmount)}{' '} - {selectedMarketData.underlyingSymbol} - + <> + +
+
+
-
- ${millify(totalStats.totalFiat)} of ${millify(totalStats.capFiat)} +
+
Total Supplied:
+
+ + {millify(totalStats.totalAmount)} of{' '} + {millify(totalStats.capAmount)}{' '} + {selectedMarketData.underlyingSymbol} + +
+
+ ${millify(totalStats.totalFiat)} of $ + {millify(totalStats.capFiat)} +
-
+ )} + + +
+ + Enable Collateral + + 0 || !selectedMarketData.supplyBalance + } + /> +
{transactionSteps.length > 0 ? ( { max={formatUnits(maxAmount, selectedMarketData.underlyingDecimals)} selectedMarketData={selectedMarketData} symbol={selectedMarketData.underlyingSymbol} + hintText="Max Withdraw" /> {hfpStatus === 'WARNING' && ( @@ -178,13 +181,19 @@ const WithdrawTab = ({ maxAmount, isLoadingMax }: WithdrawTabProps) => { )} + +
Market Supply Balance
{updatedValues.supplyBalanceFrom} → - + {updatedValues.supplyBalanceTo}
@@ -195,7 +204,11 @@ const WithdrawTab = ({ maxAmount, isLoadingMax }: WithdrawTabProps) => {
{updatedValues.supplyAPY?.toFixed(2)}% → - + {updatedValues.updatedSupplyAPY?.toFixed(2)}%
@@ -206,7 +219,11 @@ const WithdrawTab = ({ maxAmount, isLoadingMax }: WithdrawTabProps) => {
{healthFactor.current} → - + {healthFactor.predicted}
diff --git a/packages/ui/app/_components/dialogs/manage/index.tsx b/packages/ui/app/_components/dialogs/manage/index.tsx index 12a4cbb23..255e7dba7 100644 --- a/packages/ui/app/_components/dialogs/manage/index.tsx +++ b/packages/ui/app/_components/dialogs/manage/index.tsx @@ -14,7 +14,10 @@ import { TabsTrigger, TabsContent } from '@ui/components/ui/tabs'; -import { ManageDialogProvider } from '@ui/context/ManageDialogContext'; +import { + ManageDialogProvider, + 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'; @@ -166,6 +169,82 @@ const ManageDialog = ({ * Fade in animation */ + const TabsWithContext = ({ + activeTab, + isBorrowDisabled + }: { + activeTab: ActiveTab; + isBorrowDisabled: boolean; + }) => { + const { setActive } = useManageDialogContext(); + + return ( + setActive(value as ActiveTab)} + > + + Supply + Withdraw + + Borrow + + + Repay + + + + + + + + + + + + + + + + + + + + ); + }; + return (
- setActive(value as ActiveTab)} - > - - Supply - Withdraw - - Borrow - - - Repay - - - - - - - - - - - - - - - - - - - + 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 index ae92fac0f..81211d07f 100644 --- a/packages/ui/context/ManageDialogContext.tsx +++ b/packages/ui/context/ManageDialogContext.tsx @@ -1,6 +1,7 @@ 'use client'; import { createContext, + useCallback, useContext, useEffect, useMemo, @@ -131,20 +132,26 @@ export const ManageDialogProvider: React.FC<{ chainId ); const [active, setActive] = useState('supply'); - const operationMap: Record = { - supply: FundOperationMode.SUPPLY, - withdraw: FundOperationMode.WITHDRAW, - borrow: FundOperationMode.BORROW, - repay: FundOperationMode.REPAY - }; + const operationMap = useMemo>( + () => ({ + supply: FundOperationMode.SUPPLY, + withdraw: FundOperationMode.WITHDRAW, + borrow: FundOperationMode.BORROW, + repay: FundOperationMode.REPAY + }), + [] + ); - useEffect(() => { + const resetTabState = useCallback(() => { setAmount('0'); setCurrentUtilizationPercentage(0); upsertTransactionStep(undefined); + }, [upsertTransactionStep]); + + useEffect(() => { + resetTabState(); setCurrentFundOperation(operationMap[active]); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [active, upsertTransactionStep]); + }, [active, resetTabState, operationMap]); const [amount, setAmount] = useReducer( (_: string | undefined, value: string | undefined): string | undefined => @@ -229,7 +236,18 @@ export const ManageDialogProvider: React.FC<{ }, [_predictedHealthFactor, updatedAsset, amountAsBInt, healthFactor]); const hfpStatus = useMemo(() => { - if (!predictedHealthFactor) { + // 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; } @@ -242,7 +260,7 @@ export const ManageDialogProvider: React.FC<{ } const predictedHealthFactorNumber = Number( - formatEther(predictedHealthFactor) + formatEther(predictedHealthFactor ?? 0n) ); if (predictedHealthFactorNumber <= 1.1) { @@ -254,7 +272,14 @@ export const ManageDialogProvider: React.FC<{ } return HFPStatus.NORMAL; - }, [predictedHealthFactor, updatedAsset]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + predictedHealthFactor, + updatedAsset, + healthFactor, + isLoadingPredictedHealthFactor + ]); + const queryClient = useQueryClient(); /** @@ -282,128 +307,101 @@ export const ManageDialogProvider: React.FC<{ * Update utilization percentage when amount changes */ useEffect(() => { - switch (active) { - case 'supply': { - const div = - Number(formatEther(amountAsBInt)) / - (maxSupplyAmount?.bigNumber && maxSupplyAmount.number > 0 - ? Number(formatEther(maxSupplyAmount?.bigNumber)) - : 1); - setCurrentUtilizationPercentage(Math.round(div * 100)); - break; - } - - case 'withdraw': { - const div = - Number(formatEther(amountAsBInt)) / - (maxWithdrawAmount && maxWithdrawAmount > 0n - ? Number(formatEther(maxWithdrawAmount)) - : 1); - setCurrentUtilizationPercentage(Math.round(div * 100)); - break; - } - - case 'borrow': { - const div = - Number(formatEther(amountAsBInt)) / - (maxBorrowAmount?.bigNumber && maxBorrowAmount.number > 0 - ? Number(formatEther(maxBorrowAmount?.bigNumber)) - : 1); - setCurrentUtilizationPercentage(Math.round(div * 100)); - break; - } - - case 'repay': { - const div = - Number(formatEther(amountAsBInt)) / - (maxRepayAmount && maxRepayAmount > 0n - ? Number(formatEther(maxRepayAmount)) - : 1); - setCurrentUtilizationPercentage(Math.round(div * 100)); - break; - } + if (amount === '0' || !amount) { + setCurrentUtilizationPercentage(0); + return; } - }, [ - amountAsBInt, - active, - maxBorrowAmount, - maxRepayAmount, - maxSupplyAmount, - maxWithdrawAmount - ]); - - useEffect(() => { - setAmount('0'); - setCurrentUtilizationPercentage(0); - upsertTransactionStep(undefined); + let maxAmount: bigint; switch (active) { case 'supply': - setCurrentFundOperation(FundOperationMode.SUPPLY); + maxAmount = maxSupplyAmount?.bigNumber ?? 0n; break; - case 'withdraw': - setCurrentFundOperation(FundOperationMode.WITHDRAW); + maxAmount = maxWithdrawAmount ?? 0n; break; - case 'borrow': - setCurrentFundOperation(FundOperationMode.BORROW); + maxAmount = maxBorrowAmount?.bigNumber ?? 0n; break; - case 'repay': - setCurrentFundOperation(FundOperationMode.REPAY); + maxAmount = maxRepayAmount ?? 0n; break; + default: + maxAmount = 0n; } - }, [active, upsertTransactionStep]); - const initiateCloseAnimation = () => setIsMounted(false); + if (maxAmount === 0n) { + setCurrentUtilizationPercentage(0); + return; + } - const handleUtilization = (utilizationPercentage: number) => { - let maxAmountNumber = 0; + const utilization = (Number(amountAsBInt) * 100) / Number(maxAmount); + setCurrentUtilizationPercentage(Math.min(Math.round(utilization), 100)); + }, [ + active, + amountAsBInt, + maxSupplyAmount?.bigNumber, + maxWithdrawAmount, + maxBorrowAmount?.bigNumber, + maxRepayAmount, + amount + ]); - switch (active) { - case 'supply': - maxAmountNumber = Number( - formatUnits( - maxSupplyAmount?.bigNumber ?? 0n, - selectedMarketData.underlyingDecimals - ) - ); - break; - case 'withdraw': - maxAmountNumber = Number( - formatUnits( - maxWithdrawAmount ?? 0n, - selectedMarketData.underlyingDecimals - ) - ); - break; - case 'borrow': - maxAmountNumber = Number( - formatUnits( - maxBorrowAmount?.bigNumber ?? 0n, - selectedMarketData.underlyingDecimals - ) - ); - break; - case 'repay': - maxAmountNumber = Number( - formatUnits( - maxRepayAmount ?? 0n, - selectedMarketData.underlyingDecimals - ) - ); - break; - default: - break; - } + const initiateCloseAnimation = () => setIsMounted(false); - const calculatedAmount = ( - (utilizationPercentage / 100) * - maxAmountNumber - ).toFixed(parseInt(selectedMarketData.underlyingDecimals.toString())); - setAmount(calculatedAmount); - }; + const handleUtilization = useCallback( + (utilizationPercentage: number) => { + let maxAmountNumber = 0; + switch (active) { + case 'supply': + maxAmountNumber = Number( + formatUnits( + maxSupplyAmount?.bigNumber ?? 0n, + selectedMarketData.underlyingDecimals + ) + ); + break; + case 'withdraw': + maxAmountNumber = Number( + formatUnits( + maxWithdrawAmount ?? 0n, + selectedMarketData.underlyingDecimals + ) + ); + break; + case 'borrow': + maxAmountNumber = Number( + formatUnits( + maxBorrowAmount?.bigNumber ?? 0n, + selectedMarketData.underlyingDecimals + ) + ); + break; + case 'repay': + maxAmountNumber = Number( + formatUnits( + maxRepayAmount ?? 0n, + selectedMarketData.underlyingDecimals + ) + ); + break; + default: + break; + } + + const calculatedAmount = ( + (utilizationPercentage / 100) * + maxAmountNumber + ).toFixed(parseInt(selectedMarketData.underlyingDecimals.toString())); + setAmount(calculatedAmount); + // Don't set utilization here - let the effect handle it + }, + [ + active, + maxSupplyAmount?.bigNumber, + selectedMarketData.underlyingDecimals /* add other dependencies */ + ] + ); const resetTransactionSteps = () => { refetchUsedQueries(); From 87ddc864b13d89015a17b8b6824d4de185d55bdf Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Wed, 27 Nov 2024 15:40:17 +0100 Subject: [PATCH 32/66] add nektar to multipliers type --- packages/ui/utils/multipliers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/utils/multipliers.ts b/packages/ui/utils/multipliers.ts index f16fb5c77..437a33276 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< From 0455be9a8040ff6726e4ca9440d3a310771150c0 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Wed, 27 Nov 2024 20:19:29 +0100 Subject: [PATCH 33/66] improve ux ux update --- packages/ui/app/_components/AnimateHeight.tsx | 30 ++ packages/ui/app/_components/ProgressBar.tsx | 33 +++ .../ui/app/_components/UtilizationStats.tsx | 51 ++++ .../dialogs/loop/SupplyActions.tsx | 8 +- .../app/_components/dialogs/manage/Amount.tsx | 280 ++++++++++-------- .../_components/dialogs/manage/BorrowTab.tsx | 177 +++++------ .../_components/dialogs/manage/RepayTab.tsx | 130 ++++---- .../dialogs/manage/StatusAlerts.tsx | 56 ++++ .../_components/dialogs/manage/SupplyTab.tsx | 119 +++----- .../dialogs/manage/WithdrawTab.tsx | 145 ++++----- .../app/_components/dialogs/manage/index.tsx | 165 ++++++----- packages/ui/components/ui/slider.tsx | 61 +++- packages/ui/components/ui/tabs.tsx | 36 ++- packages/ui/context/ManageDialogContext.tsx | 1 + 14 files changed, 745 insertions(+), 547 deletions(-) create mode 100644 packages/ui/app/_components/AnimateHeight.tsx create mode 100644 packages/ui/app/_components/ProgressBar.tsx create mode 100644 packages/ui/app/_components/UtilizationStats.tsx create mode 100644 packages/ui/app/_components/dialogs/manage/StatusAlerts.tsx 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/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/UtilizationStats.tsx b/packages/ui/app/_components/UtilizationStats.tsx new file mode 100644 index 000000000..2f6e2544f --- /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/dialogs/loop/SupplyActions.tsx b/packages/ui/app/_components/dialogs/loop/SupplyActions.tsx index 3a5b45fc8..ee395e971 100644 --- a/packages/ui/app/_components/dialogs/loop/SupplyActions.tsx +++ b/packages/ui/app/_components/dialogs/loop/SupplyActions.tsx @@ -7,7 +7,6 @@ import { useMaxSupplyAmount } from '@ui/hooks/useMaxSupplyAmount'; import type { MarketData } from '@ui/types/TokensDataMap'; import Amount from '../manage/Amount'; -import SliderComponent from '../manage/Slider'; export type LoopProps = { borrowableAssets: Address[]; @@ -129,6 +128,8 @@ function SupplyActions({ )} selectedMarketData={selectedCollateralAsset} symbol={selectedCollateralAsset.underlyingSymbol} + currentUtilizationPercentage={utilization} + handleUtilization={handleSupplyUtilization} />
@@ -137,11 +138,6 @@ function SupplyActions({ selectedCollateralAssetUSDPrice * parseFloat(amount ?? '0') ).toFixed(2)}
- - )} diff --git a/packages/ui/app/_components/dialogs/manage/Amount.tsx b/packages/ui/app/_components/dialogs/manage/Amount.tsx index 7f31b4225..f9001c502 100644 --- a/packages/ui/app/_components/dialogs/manage/Amount.tsx +++ b/packages/ui/app/_components/dialogs/manage/Amount.tsx @@ -1,16 +1,9 @@ -/* 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 { Slider } from '@ui/components/ui/slider'; +import { cn } from '@ui/lib/utils'; import type { MarketData } from '@ui/types/TokensDataMap'; -import ResultHandler from '../../ResultHandler'; - interface IAmount { amount?: string; availableAssets?: MarketData[]; @@ -23,6 +16,8 @@ interface IAmount { selectedMarketData: MarketData; setSelectedAsset?: (asset: MarketData) => void; symbol: string; + currentUtilizationPercentage?: number; + handleUtilization?: (val: number) => void; } const Amount = ({ @@ -36,151 +31,176 @@ const Amount = ({ symbol, isLoading = false, readonly, - setSelectedAsset + setSelectedAsset, + currentUtilizationPercentage, + handleUtilization }: 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); - } + const [availableAssetsOpen, setAvailableAssetsOpen] = useState(false); + const percentages = [0, 20, 40, 60, 80, 100]; return ( -
-
- {mainText} +
+ {/* Mobile Layout */} +
+
+
+
{mainText}
+ handleInput(e.target.value)} + placeholder={`${selectedMarketData.underlyingSymbol} Amount`} + readOnly={!!readonly} + type="number" + value={amount} + /> +
- {!readonly && ( -
- - <> - +
+ {!readonly && !isLoading && ( +
+ {hintText} {max} - - +
+ )} +
setAvailableAssetsOpen(!availableAssetsOpen)} + > + {symbol} + {symbol} + {availableAssets && ( + dropdown + )} +
- )} +
+ +
+ {currentUtilizationPercentage !== undefined && ( + handleUtilization?.(value[0])} + className="w-full" + /> + )} +
-
- -
setAvailableAssetsOpen(!availableAssetsOpen)} - > - link +
+
{mainText}
+ handleInput(e.target.value)} + placeholder={`${selectedMarketData.underlyingSymbol} Amount`} + readOnly={!!readonly} + type="number" + value={amount} /> - {symbol} +
- {availableAssets && ( - link + {currentUtilizationPercentage !== undefined && ( + handleUtilization?.(value[0])} + className="w-full" /> )}
- {availableAssets && ( +
+ {!readonly && !isLoading && ( +
+ + {hintText} {max} + + +
+ )}
setAvailableAssetsOpen(!availableAssetsOpen)} > - {availableAssets.map((asset) => ( -
{ - setSelectedAsset && setSelectedAsset(asset); - setAvailableAssetsOpen(false); - }} - > - link - - {asset.underlyingSymbol} - -
- ))} + {symbol} + {symbol} + {availableAssets && ( + dropdown + )}
- )} +
+ + {availableAssets && ( +
+ {availableAssets.map((asset) => ( +
{ + setSelectedAsset?.(asset); + setAvailableAssetsOpen(false); + }} + > + {asset.underlyingSymbol} + {asset.underlyingSymbol} +
+ ))} +
+ )}
); }; -// export default Amount -export default dynamic(() => Promise.resolve(Amount), { ssr: false }); -{ - /*
*/ -} +export default Amount; diff --git a/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx b/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx index f44929466..f0a975022 100644 --- a/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx @@ -1,10 +1,8 @@ -import millify from 'millify'; import toast from 'react-hot-toast'; import { formatUnits } from 'viem'; import { Alert, AlertDescription } from '@ui/components/ui/alert'; import { Button } from '@ui/components/ui/button'; -import { Separator } from '@ui/components/ui/separator'; import { INFO_MESSAGES } from '@ui/constants'; import { HFPStatus, @@ -13,12 +11,13 @@ import { import { useMultiIonic } from '@ui/context/MultiIonicContext'; import Amount from './Amount'; -import MemoizedDonutChart from './DonutChart'; import SliderComponent from './Slider'; +import StatusAlerts from './StatusAlerts'; import TransactionStepsHandler, { useTransactionSteps } from './TransactionStepsHandler'; import ResultHandler from '../../ResultHandler'; +import MemoizedUtilizationStats from '../../UtilizationStats'; interface BorrowTabProps { maxAmount: bigint; @@ -158,7 +157,7 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { }; return ( -
+
{ max={formatUnits(maxAmount, selectedMarketData.underlyingDecimals)} selectedMarketData={selectedMarketData} symbol={selectedMarketData.underlyingSymbol} - /> - - + {/* */} + {isUnderMinBorrow && ( @@ -182,113 +180,86 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { )} - {hfpStatus === 'UNKNOWN' && ( - - - Unable to calculate health factor. - - - )} - - {hfpStatus === 'WARNING' && ( - - - You are close to the liquidation threshold. Manage your health - factor carefully. - - - )} - - {hfpStatus === 'CRITICAL' && ( - - Health factor too low. - - )} - - - -
-
- MIN BORROW - {borrowLimits.min} -
- -
- MAX BORROW - {borrowLimits.max} -
+ -
- CURRENTLY BORROWING -
- {updatedValues.borrowBalanceFrom} - → - - {updatedValues.borrowBalanceTo} - +
+ {/* Left Column */} +
+
+ MIN BORROW + {borrowLimits.min}
-
-
- Market Borrow APR -
- {updatedValues.borrowAPR?.toFixed(2)}% - → - - {updatedValues.updatedBorrowAPR?.toFixed(2)}% - +
+ MAX BORROW + {borrowLimits.max}
-
-
- Health Factor -
- {healthFactor.current} - → - - {healthFactor.predicted} - +
+ CURRENTLY BORROWING +
+ {updatedValues.borrowBalanceFrom} + → + + {updatedValues.borrowBalanceTo} + +
-
-
- - - {totalStats && ( -
-
- -
-
-
Total Borrowed:
-
- - {millify(totalStats.totalAmount)} of{' '} - {millify(totalStats.capAmount)}{' '} - {selectedMarketData.underlyingSymbol} - +
+ Market Borrow APR +
+ {updatedValues.borrowAPR?.toFixed(2)}% + → + + {updatedValues.updatedBorrowAPR?.toFixed(2)}% +
-
- ${millify(totalStats.totalFiat)} of ${millify(totalStats.capFiat)} +
+ +
+ Health Factor +
+ {healthFactor.current} + → + + {healthFactor.predicted} +
- )} + + {/* Right Column */} + {totalStats && ( + + )} +
{transactionSteps.length > 0 ? ( { +const RepayTab = ({ maxAmount, isLoadingMax, totalStats }: RepayTabProps) => { const { selectedMarketData, amount, @@ -177,7 +185,7 @@ const RepayTab = ({ maxAmount, isLoadingMax }: RepayTabProps) => { }; return ( -
+
{ max={formatUnits(maxAmount, selectedMarketData.underlyingDecimals)} selectedMarketData={selectedMarketData} symbol={selectedMarketData.underlyingSymbol} - /> - - - {hfpStatus === 'CRITICAL' && ( - - Health factor too low. - - )} - + -
-
- CURRENTLY BORROWING -
- - {updatedValues.borrowBalanceFrom} - - → - - - {updatedValues.borrowBalanceTo} +
+
+
+ CURRENTLY BORROWING +
+ + {updatedValues.borrowBalanceFrom} - + → + + + {updatedValues.borrowBalanceTo} + + +
-
-
- Market Borrow APR -
- {updatedValues.borrowAPR?.toFixed(2)}% - → - - {updatedValues.updatedBorrowAPR?.toFixed(2)}% - +
+ Market Borrow APR +
+ {updatedValues.borrowAPR?.toFixed(2)}% + → + + {updatedValues.updatedBorrowAPR?.toFixed(2)}% + +
-
-
- Health Factor -
- {healthFactor.current} - → - - {healthFactor.predicted} - +
+ Health Factor +
+ {healthFactor.current} + → + + {healthFactor.predicted} + +
+ + {totalStats && ( + + )}
{transactionSteps.length > 0 ? ( 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..08a338acf --- /dev/null +++ b/packages/ui/app/_components/dialogs/manage/StatusAlerts.tsx @@ -0,0 +1,56 @@ +import { Alert, AlertDescription } from '@ui/components/ui/alert'; +import { HFPStatus } from '@ui/context/ManageDialogContext'; + +interface StatusAlert { + status: HFPStatus; + message: string; +} + +const alerts: Record = { + [HFPStatus.WARNING]: { + status: HFPStatus.WARNING, + message: + 'You are close to the liquidation threshold. Manage your health factor carefully.' + }, + [HFPStatus.CRITICAL]: { + status: HFPStatus.CRITICAL, + message: 'Health factor too low.' + }, + [HFPStatus.UNKNOWN]: { + status: HFPStatus.UNKNOWN, + message: 'Unable to calculate health factor.' + }, + [HFPStatus.NORMAL]: { + status: HFPStatus.NORMAL, + message: 'Your health factor is normal.' + } +}; + +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; + + 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 index 0319c87a1..3814e2e55 100644 --- a/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx @@ -1,4 +1,3 @@ -import millify from 'millify'; import toast from 'react-hot-toast'; import { formatUnits } from 'viem'; @@ -10,12 +9,12 @@ import { useManageDialogContext } from '@ui/context/ManageDialogContext'; import { useMultiIonic } from '@ui/context/MultiIonicContext'; import Amount from './Amount'; -import MemoizedDonutChart from './DonutChart'; import SliderComponent from './Slider'; import TransactionStepsHandler, { useTransactionSteps } from './TransactionStepsHandler'; import ResultHandler from '../../ResultHandler'; +import MemoizedUtilizationStats from '../../UtilizationStats'; interface SupplyTabProps { maxAmount: bigint; @@ -27,15 +26,13 @@ interface SupplyTabProps { totalFiat: number; }; setSwapWidgetOpen: (open: boolean) => void; - collateralApr: number; } const SupplyTab = ({ maxAmount, isLoadingMax, totalStats, - setSwapWidgetOpen, - collateralApr + setSwapWidgetOpen }: SupplyTabProps) => { const { selectedMarketData, @@ -210,10 +207,10 @@ const SupplyTab = ({ }; return ( -
+
-
+ )}
-
{mainText}
+
{mainText}
handleInput(e.target.value)} placeholder={`${selectedMarketData.underlyingSymbol} Amount`} readOnly={!!readonly} @@ -135,17 +129,12 @@ const Amount = ({
{!readonly && !isLoading && ( -
- - {hintText} {max} - - -
+ )}
{ }; return ( -
+
{ {/* */} {isUnderMinBorrow && ( - - - Amount must be greater than minimum borrow amount ( - {borrowLimits.min} {selectedMarketData.underlyingSymbol}) - + +
+ + + Amount must be greater than minimum borrow amount ( + {borrowLimits.min} {selectedMarketData.underlyingSymbol}) + +
)} @@ -269,7 +275,7 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { /> ) : (
{transactionSteps.length > 0 ? ( @@ -298,7 +316,7 @@ const SupplyTab = ({ /> ) : (
-
- logo +
+

Resources

+ + +
+ +
+

Tools

+ + +
+ +
+ logo +
-
- + - -
- + }} + /> +
+ + From 9e65f29c341db349045141798e825353b8f3aae3 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Thu, 28 Nov 2024 14:02:12 +0100 Subject: [PATCH 35/66] disable if max is 0 & add info tooltip --- .../app/_components/dialogs/manage/Amount.tsx | 347 +++++++++++------- 1 file changed, 212 insertions(+), 135 deletions(-) diff --git a/packages/ui/app/_components/dialogs/manage/Amount.tsx b/packages/ui/app/_components/dialogs/manage/Amount.tsx index 54a8e7114..be1cf84ba 100644 --- a/packages/ui/app/_components/dialogs/manage/Amount.tsx +++ b/packages/ui/app/_components/dialogs/manage/Amount.tsx @@ -1,6 +1,12 @@ 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'; @@ -20,6 +26,125 @@ interface IAmount { handleUtilization?: (val: number) => void; } +// Extracted common input component +const AmountInput = ({ + mainText, + handleInput, + selectedMarketData, + readonly, + amount, + max +}: { + mainText?: string; + handleInput: (val?: string) => void; + selectedMarketData: MarketData; + readonly?: boolean; + amount?: string; + max?: string; +}) => { + const isDisabled = readonly || max === '0'; + + return ( +
+
{mainText}
+ handleInput(e.target.value)} + placeholder={`${selectedMarketData.underlyingSymbol} Amount`} + readOnly={isDisabled} + disabled={isDisabled} + type="number" + value={amount} + /> +
+ ); +}; + +// Extracted common asset selector component +const AssetSelector = ({ + symbol, + availableAssets, + onClick, + children +}: { + symbol: string; + availableAssets: any; + onClick: () => void; + children: React.ReactNode; +}) => ( +
+
+ {symbol} + {symbol} + {availableAssets && ( + dropdown + )} +
+ {children} +
+); + +// Extracted slider component with tooltip +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 = ({ selectedMarketData, handleInput, @@ -37,157 +162,109 @@ const Amount = ({ }: IAmount) => { const [availableAssetsOpen, setAvailableAssetsOpen] = useState(false); + // Extracted common max button component + const MaxButton = !readonly && !isLoading && ( + + ); + + // Extracted available assets dropdown + const AssetsDropdown = availableAssets && ( +
+ {availableAssets.map((asset) => ( +
{ + setSelectedAsset?.(asset); + setAvailableAssetsOpen(false); + }} + > + {asset.underlyingSymbol} + {asset.underlyingSymbol} +
+ ))} +
+ ); + return (
{/* Mobile Layout */}
-
-
{mainText}
- handleInput(e.target.value)} - placeholder={`${selectedMarketData.underlyingSymbol} Amount`} - readOnly={!!readonly} - type="number" - value={amount} - /> -
- -
- {!readonly && !isLoading && ( - - )} -
setAvailableAssetsOpen(!availableAssetsOpen)} - > - {symbol} - {symbol} - {availableAssets && ( - dropdown - )} -
-
+ + setAvailableAssetsOpen(!availableAssetsOpen)} + > + {MaxButton} +
-
- {currentUtilizationPercentage !== undefined && ( - handleUtilization?.(value[0])} - className="w-full" - /> - )} -
+ {currentUtilizationPercentage !== undefined && ( + + )}
{/* Desktop Layout */}
-
-
{mainText}
- handleInput(e.target.value)} - placeholder={`${selectedMarketData.underlyingSymbol} Amount`} - readOnly={!!readonly} - type="number" - value={amount} - /> -
+ -
- {currentUtilizationPercentage !== undefined && ( - handleUtilization?.(value[0])} - className="w-full" - /> - )} -
- -
- {!readonly && !isLoading && ( - - )} -
setAvailableAssetsOpen(!availableAssetsOpen)} - > - {symbol} + - {symbol} - {availableAssets && ( - dropdown - )}
-
-
+ )} - {availableAssets && ( -
setAvailableAssetsOpen(!availableAssetsOpen)} > - {availableAssets.map((asset) => ( -
{ - setSelectedAsset?.(asset); - setAvailableAssetsOpen(false); - }} - > - {asset.underlyingSymbol} - {asset.underlyingSymbol} -
- ))} -
- )} + {MaxButton} + +
+ + {AssetsDropdown}
); }; From 9ee78ffaafe77e2928615d499da70b39a444d7f6 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Thu, 28 Nov 2024 14:58:34 +0100 Subject: [PATCH 36/66] add search to market --- packages/ui/app/_components/CommonTable.tsx | 1 + .../app/_components/markets/MarketSearch.tsx | 81 +++++++++++++++++++ .../ui/app/_components/markets/PoolToggle.tsx | 10 +-- packages/ui/app/market/page.tsx | 29 +++++-- 4 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 packages/ui/app/_components/markets/MarketSearch.tsx diff --git a/packages/ui/app/_components/CommonTable.tsx b/packages/ui/app/_components/CommonTable.tsx index 0c0eb2990..7c28e3af1 100644 --- a/packages/ui/app/_components/CommonTable.tsx +++ b/packages/ui/app/_components/CommonTable.tsx @@ -149,6 +149,7 @@ function CommonTable({ diff --git a/packages/ui/app/_components/markets/MarketSearch.tsx b/packages/ui/app/_components/markets/MarketSearch.tsx new file mode 100644 index 000000000..a52990948 --- /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/PoolToggle.tsx b/packages/ui/app/_components/markets/PoolToggle.tsx index 7421db837..618848b53 100644 --- a/packages/ui/app/_components/markets/PoolToggle.tsx +++ b/packages/ui/app/_components/markets/PoolToggle.tsx @@ -9,16 +9,14 @@ import { pools } from '@ui/constants/index'; const PoolToggle = ({ chain, pool }: { chain: number; pool: string }) => { const pathname = usePathname(); return ( -
+
{pools[+chain].pools.map((poolx, idx) => { return ( (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(() => { + setFilteredMarketData(marketData); + }, [marketData]); const columns: EnhancedColumnDef[] = [ { @@ -265,15 +272,23 @@ export default function Market() {
-
- +
+
+ +
+
+ +
From 2bf8a97377a6f238a495f2f7d5fe43a0ce544b67 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Thu, 28 Nov 2024 15:05:05 +0100 Subject: [PATCH 37/66] fix reward --- packages/ui/app/_components/markets/StakingTile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ? ( ) : ( From dd123456350e20f8052cda55fda092d34a31530a Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Thu, 28 Nov 2024 16:08:15 +0100 Subject: [PATCH 38/66] update values when supplying/repaying/borrowing/withdrawing lint fix --- .../dialogs/loop/BorrowActions.tsx | 1 - .../dialogs/loop/SupplyActions.tsx | 1 - .../app/_components/dialogs/manage/Amount.tsx | 64 +++++++++----- .../_components/dialogs/manage/BorrowTab.tsx | 83 ++++++++++++++----- .../_components/dialogs/manage/RepayTab.tsx | 66 +++++++++++---- .../_components/dialogs/manage/SupplyTab.tsx | 77 ++++++++++++----- .../dialogs/manage/WithdrawTab.tsx | 70 ++++++++++++---- packages/ui/hooks/useBalancePolling.ts | 83 +++++++++++++++++++ 8 files changed, 345 insertions(+), 100 deletions(-) create mode 100644 packages/ui/hooks/useBalancePolling.ts diff --git a/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx b/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx index aa7b1a97b..d06cce575 100644 --- a/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx +++ b/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx @@ -69,7 +69,6 @@ function BorrowActions({ mainText="AMOUNT TO BORROW" max={''} readonly - selectedMarketData={selectedBorrowAsset} setSelectedAsset={(asset: MarketData) => setSelectedBorrowAsset(asset) } diff --git a/packages/ui/app/_components/dialogs/loop/SupplyActions.tsx b/packages/ui/app/_components/dialogs/loop/SupplyActions.tsx index ee395e971..e9dbfe9d1 100644 --- a/packages/ui/app/_components/dialogs/loop/SupplyActions.tsx +++ b/packages/ui/app/_components/dialogs/loop/SupplyActions.tsx @@ -126,7 +126,6 @@ function SupplyActions({ maxSupplyAmount?.bigNumber ?? 0n, selectedCollateralAsset.underlyingDecimals )} - selectedMarketData={selectedCollateralAsset} symbol={selectedCollateralAsset.underlyingSymbol} currentUtilizationPercentage={utilization} handleUtilization={handleSupplyUtilization} diff --git a/packages/ui/app/_components/dialogs/manage/Amount.tsx b/packages/ui/app/_components/dialogs/manage/Amount.tsx index be1cf84ba..e8e0679c9 100644 --- a/packages/ui/app/_components/dialogs/manage/Amount.tsx +++ b/packages/ui/app/_components/dialogs/manage/Amount.tsx @@ -10,6 +10,8 @@ import { import { cn } from '@ui/lib/utils'; import type { MarketData } from '@ui/types/TokensDataMap'; +import ResultHandler from '../../ResultHandler'; + interface IAmount { amount?: string; availableAssets?: MarketData[]; @@ -19,30 +21,46 @@ interface IAmount { mainText?: string; max?: string; readonly?: boolean; - selectedMarketData: MarketData; setSelectedAsset?: (asset: MarketData) => void; symbol: string; currentUtilizationPercentage?: number; handleUtilization?: (val: number) => void; } -// Extracted common input component const AmountInput = ({ mainText, handleInput, - selectedMarketData, readonly, amount, - max + max, + isLoading }: { mainText?: string; handleInput: (val?: string) => void; - selectedMarketData: MarketData; readonly?: boolean; amount?: string; max?: string; + isLoading?: boolean; }) => { - const isDisabled = readonly || max === '0'; + 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 (
@@ -52,18 +70,19 @@ const AmountInput = ({ 'w-full bg-transparent text-md border border-white/10 rounded px-2 py-1 focus:outline-none focus:border-white/20', isDisabled && 'opacity-50 cursor-not-allowed' )} - onChange={(e) => handleInput(e.target.value)} - placeholder={`${selectedMarketData.underlyingSymbol} Amount`} + onChange={handleChange} + placeholder="0,00" readOnly={isDisabled} disabled={isDisabled} type="number" value={amount} + min="0" + max={max} />
); }; -// Extracted common asset selector component const AssetSelector = ({ symbol, availableAssets, @@ -76,6 +95,8 @@ const AssetSelector = ({ children: React.ReactNode; }) => (
+ {children} +
)}
- {children}
); -// Extracted slider component with tooltip const UtilizationSlider = ({ currentUtilizationPercentage, handleUtilization, @@ -146,7 +165,6 @@ const UtilizationSlider = ({ }; const Amount = ({ - selectedMarketData, handleInput, amount, availableAssets, @@ -162,17 +180,21 @@ const Amount = ({ }: IAmount) => { const [availableAssetsOpen, setAvailableAssetsOpen] = useState(false); - // Extracted common max button component - const MaxButton = !readonly && !isLoading && ( - + + ); - // Extracted available assets dropdown const AssetsDropdown = availableAssets && (
{ + const [txHash, setTxHash] = useState
(); + const [isWaitingForIndexing, setIsWaitingForIndexing] = useState(false); + const { selectedMarketData, amount, @@ -48,9 +57,32 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { maxBorrowAmount, isLoadingPredictedHealthFactor, isLoadingUpdatedAssets, - updatedValues + updatedValues, + comptrollerAddress } = useManageDialogContext(); + const { refetch: refetchMaxBorrow } = useMaxBorrowAmount( + selectedMarketData, + comptrollerAddress, + chainId + ); + const { currentSdk, address } = useMultiIonic(); + + const { isPolling } = useBalancePolling({ + address, + chainId, + txHash, + enabled: isWaitingForIndexing, + onSuccess: () => { + setIsWaitingForIndexing(false); + setTxHash(undefined); + refetchMaxBorrow(); + toast.success( + `Borrowed ${amount} ${selectedMarketData.underlyingSymbol}` + ); + } + }); + const isDisabled = !amount || amountAsBInt === 0n || @@ -79,8 +111,6 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { borrowLimits.min && parseFloat(amount) < parseFloat(borrowLimits.min); - const { currentSdk, address } = useMultiIonic(); - const { addStepsForAction, upsertTransactionStep } = useTransactionSteps(); const borrowAmount = async () => { @@ -124,24 +154,30 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { } }); - tx && - (await currentSdk.publicClient.waitForTransactionReceipt({ + if (tx) { + await currentSdk.publicClient.waitForTransactionReceipt({ hash: tx - })); - - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); - - toast.success( - `Borrowed ${amount} ${selectedMarketData.underlyingSymbol}` - ); + }); + + setTxHash(tx); + setIsWaitingForIndexing(true); + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + success: true + } + }); + + toast.success( + `Borrowed ${amount} ${selectedMarketData.underlyingSymbol}` + ); + } } catch (error) { console.error(error); + setIsWaitingForIndexing(false); + setTxHash(undefined); upsertTransactionStep({ index: currentTransactionStep, @@ -161,9 +197,8 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { { {updatedValues.borrowBalanceFrom} → {updatedValues.borrowBalanceTo} @@ -276,10 +311,12 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { ) : ( )}
diff --git a/packages/ui/app/_components/dialogs/manage/RepayTab.tsx b/packages/ui/app/_components/dialogs/manage/RepayTab.tsx index aafa8acf7..68ca52502 100644 --- a/packages/ui/app/_components/dialogs/manage/RepayTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/RepayTab.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import { formatUnits } from 'viem'; @@ -10,6 +10,8 @@ import { useManageDialogContext } from '@ui/context/ManageDialogContext'; import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import { useBalancePolling } from '@ui/hooks/useBalancePolling'; +import { useMaxRepayAmount } from '@ui/hooks/useMaxRepayAmount'; import Amount from './Amount'; import StatusAlerts from './StatusAlerts'; @@ -19,6 +21,8 @@ import TransactionStepsHandler, { import ResultHandler from '../../ResultHandler'; import MemoizedUtilizationStats from '../../UtilizationStats'; +import type { Address } from 'viem'; + interface RepayTabProps { maxAmount: bigint; isLoadingMax: boolean; @@ -31,6 +35,8 @@ interface RepayTabProps { } const RepayTab = ({ maxAmount, isLoadingMax, totalStats }: RepayTabProps) => { + const [txHash, setTxHash] = useState
(); + const [isWaitingForIndexing, setIsWaitingForIndexing] = useState(false); const { selectedMarketData, amount, @@ -47,14 +53,31 @@ const RepayTab = ({ maxAmount, isLoadingMax, totalStats }: RepayTabProps) => { isLoadingUpdatedAssets, updatedValues } = useManageDialogContext(); - const { currentSdk, address } = useMultiIonic(); + const { refetch: refetchMaxRepay } = useMaxRepayAmount( + selectedMarketData, + chainId + ); + const { currentSdk, address } = useMultiIonic(); const { addStepsForAction, upsertTransactionStep } = useTransactionSteps(); const currentBorrowAmountAsFloat = useMemo( () => parseFloat(selectedMarketData.borrowBalance.toString()), [selectedMarketData] ); + const { isPolling } = useBalancePolling({ + address, + chainId, + txHash, + enabled: isWaitingForIndexing, + onSuccess: () => { + setIsWaitingForIndexing(false); + setTxHash(undefined); + refetchMaxRepay(); + toast.success(`Repaid ${amount} ${selectedMarketData.underlyingSymbol}`); + } + }); + const repayAmount = async () => { if ( !transactionSteps.length && @@ -151,20 +174,30 @@ const RepayTab = ({ maxAmount, isLoadingMax, totalStats }: RepayTabProps) => { } }); - tx && - (await currentSdk.publicClient.waitForTransactionReceipt({ + if (tx) { + await currentSdk.publicClient.waitForTransactionReceipt({ hash: tx - })); + }); - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); + setTxHash(tx); + setIsWaitingForIndexing(true); + + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + success: true + } + }); + + toast.success( + `Repaid ${amount} ${selectedMarketData.underlyingSymbol}` + ); + } } catch (error) { console.error(error); + setIsWaitingForIndexing(false); + setTxHash(undefined); upsertTransactionStep({ index: currentTransactionStep, @@ -189,9 +222,8 @@ const RepayTab = ({ maxAmount, isLoadingMax, totalStats }: RepayTabProps) => { { {updatedValues.borrowAPR?.toFixed(2)}% → {updatedValues.updatedBorrowAPR?.toFixed(2)}% @@ -280,7 +312,9 @@ const RepayTab = ({ maxAmount, isLoadingMax, totalStats }: RepayTabProps) => { } onClick={repayAmount} > - Repay {selectedMarketData.underlyingSymbol} + {isWaitingForIndexing + ? 'Updating Balances...' + : `Repay ${selectedMarketData.underlyingSymbol}`} )}
diff --git a/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx b/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx index cae8762d4..791e6fa3d 100644 --- a/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx @@ -1,3 +1,5 @@ +import { useState } from 'react'; + import toast from 'react-hot-toast'; import { formatUnits } from 'viem'; @@ -11,6 +13,8 @@ import { import { INFO_MESSAGES } from '@ui/constants'; import { useManageDialogContext } from '@ui/context/ManageDialogContext'; import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import { useBalancePolling } from '@ui/hooks/useBalancePolling'; +import { useMaxSupplyAmount } from '@ui/hooks/useMaxSupplyAmount'; import Amount from './Amount'; import TransactionStepsHandler, { @@ -19,6 +23,8 @@ import TransactionStepsHandler, { import ResultHandler from '../../ResultHandler'; import MemoizedUtilizationStats from '../../UtilizationStats'; +import type { Address } from 'viem'; + interface SupplyTabProps { maxAmount: bigint; isLoadingMax: boolean; @@ -37,6 +43,9 @@ const SupplyTab = ({ totalStats, setSwapWidgetOpen }: SupplyTabProps) => { + const [txHash, setTxHash] = useState
(); + const [isWaitingForIndexing, setIsWaitingForIndexing] = useState(false); + const { selectedMarketData, amount, @@ -54,10 +63,31 @@ const SupplyTab = ({ isLoadingUpdatedAssets } = useManageDialogContext(); + const { refetch: refetchMaxSupply } = useMaxSupplyAmount( + selectedMarketData, + comptrollerAddress, + chainId + ); + const isDisabled = !amount || amountAsBInt === 0n; const { currentSdk, address } = useMultiIonic(); const { addStepsForAction, upsertTransactionStep } = useTransactionSteps(); + const { isPolling } = useBalancePolling({ + address, + chainId, + txHash, + enabled: isWaitingForIndexing, + onSuccess: () => { + setIsWaitingForIndexing(false); + setTxHash(undefined); + refetchMaxSupply(); + toast.success( + `Supplied ${amount} ${selectedMarketData.underlyingSymbol}` + ); + } + }); + const supplyAmount = async () => { if ( !transactionSteps.length && @@ -179,24 +209,30 @@ const SupplyTab = ({ } }); - tx && - (await currentSdk.publicClient.waitForTransactionReceipt({ + if (tx) { + await currentSdk.publicClient.waitForTransactionReceipt({ hash: tx - })); + }); - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); + setTxHash(tx); + setIsWaitingForIndexing(true); - toast.success( - `Supplied ${amount} ${selectedMarketData.underlyingSymbol}` - ); + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + success: true + } + }); + + toast.success( + `Supplied ${amount} ${selectedMarketData.underlyingSymbol}` + ); + } } catch (error) { - toast.error('Error while supplying!'); + console.error(error); + setIsWaitingForIndexing(false); + setTxHash(undefined); upsertTransactionStep({ index: currentTransactionStep, @@ -205,6 +241,8 @@ const SupplyTab = ({ error: true } }); + + toast.error('Error while supplying!'); } } }; @@ -223,9 +261,8 @@ const SupplyTab = ({ {updatedValues.supplyBalanceFrom} → @@ -317,10 +354,12 @@ const SupplyTab = ({ ) : ( )}
diff --git a/packages/ui/app/_components/dialogs/manage/WithdrawTab.tsx b/packages/ui/app/_components/dialogs/manage/WithdrawTab.tsx index 9b95fe2f3..914fd4db3 100644 --- a/packages/ui/app/_components/dialogs/manage/WithdrawTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/WithdrawTab.tsx @@ -1,3 +1,5 @@ +import { useState } from 'react'; + import toast from 'react-hot-toast'; import { formatUnits } from 'viem'; @@ -8,6 +10,8 @@ import { useManageDialogContext } from '@ui/context/ManageDialogContext'; import { useMultiIonic } from '@ui/context/MultiIonicContext'; +import { useBalancePolling } from '@ui/hooks/useBalancePolling'; +import { useMaxWithdrawAmount } from '@ui/hooks/useMaxWithdrawAmount'; import Amount from './Amount'; import StatusAlerts from './StatusAlerts'; @@ -17,6 +21,8 @@ import TransactionStepsHandler, { import ResultHandler from '../../ResultHandler'; import MemoizedUtilizationStats from '../../UtilizationStats'; +import type { Address } from 'viem'; + interface WithdrawTabProps { maxAmount: bigint; isLoadingMax: boolean; @@ -33,6 +39,9 @@ const WithdrawTab = ({ isLoadingMax, totalStats }: WithdrawTabProps) => { + const [txHash, setTxHash] = useState
(); + const [isWaitingForIndexing, setIsWaitingForIndexing] = useState(false); + const { selectedMarketData, amount, @@ -50,6 +59,10 @@ const WithdrawTab = ({ updatedValues, isLoadingUpdatedAssets } = useManageDialogContext(); + const { refetch: refetchMaxWithdraw } = useMaxWithdrawAmount( + selectedMarketData, + chainId + ); const isDisabled = !amount || @@ -63,9 +76,23 @@ const WithdrawTab = ({ predicted: normalizedPredictedHealthFactor ?? '0' }; const { currentSdk, address } = useMultiIonic(); - const { addStepsForAction, upsertTransactionStep } = useTransactionSteps(); + const { isPolling } = useBalancePolling({ + address, + chainId, + txHash, + enabled: isWaitingForIndexing, + onSuccess: () => { + setIsWaitingForIndexing(false); + setTxHash(undefined); + refetchMaxWithdraw(); + toast.success( + `Withdrawn ${amount} ${selectedMarketData.underlyingSymbol}` + ); + } + }); + const withdrawAmount = async () => { if ( !transactionSteps.length && @@ -117,24 +144,30 @@ const WithdrawTab = ({ } }); - tx && - (await currentSdk.publicClient.waitForTransactionReceipt({ + if (tx) { + await currentSdk.publicClient.waitForTransactionReceipt({ hash: tx - })); + }); - upsertTransactionStep({ - index: currentTransactionStep, - transactionStep: { - ...transactionSteps[currentTransactionStep], - success: true - } - }); + setTxHash(tx); + setIsWaitingForIndexing(true); - toast.success( - `Withdrawn ${amount} ${selectedMarketData.underlyingSymbol}` - ); + upsertTransactionStep({ + index: currentTransactionStep, + transactionStep: { + ...transactionSteps[currentTransactionStep], + success: true + } + }); + + toast.success( + `Withdrawn ${amount} ${selectedMarketData.underlyingSymbol}` + ); + } } catch (error) { console.error(error); + setIsWaitingForIndexing(false); + setTxHash(undefined); upsertTransactionStep({ index: currentTransactionStep, @@ -154,9 +187,8 @@ const WithdrawTab = ({ {updatedValues.supplyAPY?.toFixed(2)}% → {updatedValues.updatedSupplyAPY?.toFixed(2)}% @@ -246,7 +278,9 @@ const WithdrawTab = ({ disabled={isDisabled} onClick={withdrawAmount} > - Withdraw {selectedMarketData.underlyingSymbol} + {isWaitingForIndexing + ? 'Updating Balances...' + : `Withdraw ${selectedMarketData.underlyingSymbol}`} )}
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 }; +}; From 63a452ac0f45df27db5432184ba6528ca4c854cf Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Fri, 29 Nov 2024 11:27:26 +0100 Subject: [PATCH 39/66] improve filter bar --- .../ui/app/_components/markets/FilterBar.tsx | 59 +++++++++++++++++++ .../app/_components/markets/MarketSearch.tsx | 2 +- .../ui/app/_components/markets/PoolToggle.tsx | 51 ++++++++++------ packages/ui/app/market/page.tsx | 38 ++++-------- 4 files changed, 103 insertions(+), 47 deletions(-) create mode 100644 packages/ui/app/_components/markets/FilterBar.tsx diff --git a/packages/ui/app/_components/markets/FilterBar.tsx b/packages/ui/app/_components/markets/FilterBar.tsx new file mode 100644 index 000000000..92646e8e3 --- /dev/null +++ b/packages/ui/app/_components/markets/FilterBar.tsx @@ -0,0 +1,59 @@ +'use client'; + +import dynamic from 'next/dynamic'; + +import { type MarketRowData } from '@ui/hooks/market/useMarketData'; + +import MarketSearch from './MarketSearch'; +import CommonTable from '../CommonTable'; + +const PoolToggle = dynamic( + () => import('../../_components/markets/PoolToggle'), + { + ssr: false + } +); +interface FilterBarProps { + chain: number; + pool: string; + marketData: MarketRowData[]; + onSearch: (filteredData: MarketRowData[]) => void; + filteredData: MarketRowData[]; + columns: any[]; + isLoading: boolean; +} + +export default function FilterBar({ + chain, + pool, + marketData, + onSearch, + filteredData, + columns, + isLoading +}: FilterBarProps) { + return ( +
+
+
+ +
+
+ +
+
+ + +
+ ); +} diff --git a/packages/ui/app/_components/markets/MarketSearch.tsx b/packages/ui/app/_components/markets/MarketSearch.tsx index a52990948..1559ed386 100644 --- a/packages/ui/app/_components/markets/MarketSearch.tsx +++ b/packages/ui/app/_components/markets/MarketSearch.tsx @@ -65,7 +65,7 @@ const MarketSearch = ({ data, onSearch }: MarketSearchProps) => { }, [debouncedSearchTerm, filterMarkets]); return ( -
+
diff --git a/packages/ui/app/_components/markets/PoolToggle.tsx b/packages/ui/app/_components/markets/PoolToggle.tsx index 618848b53..cc2c66a17 100644 --- a/packages/ui/app/_components/markets/PoolToggle.tsx +++ b/packages/ui/app/_components/markets/PoolToggle.tsx @@ -4,29 +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/market/page.tsx b/packages/ui/app/market/page.tsx index 34eab9331..cd8aeb54b 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -15,13 +15,12 @@ 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 MarketSearch from '../_components/markets/MarketSearch'; +import FilterBar from '../_components/markets/FilterBar'; import StakingTile from '../_components/markets/StakingTile'; import TotalTvlTile from '../_components/markets/TotalTvlTile'; import TvlTile from '../_components/markets/TvlTile'; @@ -34,10 +33,6 @@ const NetworkSelector = dynamic( { ssr: false } ); -const PoolToggle = dynamic(() => import('../_components/markets/PoolToggle'), { - ssr: false -}); - interface MarketCellProps { row: Row; getValue: () => any; @@ -271,28 +266,15 @@ export default function Market() { />
-
-
-
- -
-
- -
-
- - -
+
{selectedMarketData && poolData && ( From 9228d90039a1c33f01af92ff993469332f661efc Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Fri, 29 Nov 2024 12:55:57 +0100 Subject: [PATCH 40/66] spacing & $total supplied/borrowed --- packages/ui/app/market/page.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/ui/app/market/page.tsx b/packages/ui/app/market/page.tsx index cd8aeb54b..79d253498 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -86,7 +86,7 @@ export default function Market() { selectedSymbol: row.original.asset } }} - className="flex gap-3 items-center" + className="flex gap-3 items-center pl-6" > {row.original.asset}
- Supplied: {row.original.supply.total.split(' ')[0]} - Borrowed: {row.original.borrow.total.split(' ')[0]} + + Supplied: ${row.original.supply.totalUSD.split(' ')[0]} + + + Borrowed: ${row.original.borrow.totalUSD.split(' ')[0]} +
@@ -182,7 +186,7 @@ export default function Market() { header: 'ACTIONS', enableSorting: false, cell: ({ row }: MarketCellProps) => ( -
+
); }; @@ -107,15 +107,13 @@ function CommonTable({ data, columns, isLoading: externalIsLoading = false, - renderRow + getRowStyle }: CommonTableProps) { const [sorting, setSorting] = useState([]); const [hasInitialized, setHasInitialized] = useState(false); - // Consider the table as loading if it hasn't initialized yet or if external loading state is true const isLoading = !hasInitialized || externalIsLoading; - // Once we get data for the first time, mark as initialized if (!hasInitialized && data.length > 0) { setHasInitialized(true); } @@ -154,12 +152,12 @@ function CommonTable({ center height={80} > -
+
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ({ ) : ( - table.getRowModel().rows.map((row) => - renderRow ? ( - renderRow(row) - ) : ( + table.getRowModel().rows.map((row) => { + const rowStyle = getRowStyle ? getRowStyle(row) : {}; + + return ( {row.getVisibleCells().map((cell) => ( @@ -205,8 +204,8 @@ function CommonTable({ ))} - ) - ) + ); + }) )}
diff --git a/packages/ui/app/_components/markets/FilterBar.tsx b/packages/ui/app/_components/markets/FilterBar.tsx index f401050b6..47c21e505 100644 --- a/packages/ui/app/_components/markets/FilterBar.tsx +++ b/packages/ui/app/_components/markets/FilterBar.tsx @@ -26,7 +26,7 @@ export default function FilterBar({ onSearch }: FilterBarProps) { return ( -
+
( [] ); + console.log('filteredMarketData', filteredMarketData); const { marketData, isLoading, poolData, selectedMarketData, loopProps } = useMarketData(selectedPool, chain, selectedSymbol); @@ -282,6 +284,14 @@ export default function Market() { data={filteredMarketData} columns={columns} isLoading={isLoading} + getRowStyle={(row) => ({ + badge: row.original.membership + ? { text: 'Collateral' } + : undefined, + borderClassName: row.original.membership + ? pools[+chain]?.border + : undefined + })} />
diff --git a/packages/ui/components/ui/table.tsx b/packages/ui/components/ui/table.tsx index ebbdafdc0..097f08c22 100644 --- a/packages/ui/components/ui/table.tsx +++ b/packages/ui/components/ui/table.tsx @@ -14,7 +14,7 @@ const Table = React.forwardRef< ref={ref} className={cn( 'w-full caption-bottom text-sm border-separate', - compact ? 'border-spacing-y-2' : 'border-spacing-y-3', + compact ? 'border-spacing-y-2' : 'border-spacing-y-4', 'border-spacing-x-0', className )} @@ -39,21 +39,13 @@ TableHeader.displayName = 'TableHeader'; const TableBody = React.forwardRef< HTMLTableSectionElement, React.HTMLAttributes ->(({ 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, From a037a2a9b5d49ef1e7aac54a865f7252b6af15fe Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Mon, 2 Dec 2024 11:04:04 +0100 Subject: [PATCH 43/66] fix pool toggle on dashboard --- packages/ui/app/_components/markets/PoolToggle.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/app/_components/markets/PoolToggle.tsx b/packages/ui/app/_components/markets/PoolToggle.tsx index cc2c66a17..09f98c7c7 100644 --- a/packages/ui/app/_components/markets/PoolToggle.tsx +++ b/packages/ui/app/_components/markets/PoolToggle.tsx @@ -13,8 +13,8 @@ const PoolToggle = ({ chain, pool }: { chain: number; pool: string }) => { const poolsData = pools[+chain].pools; return ( -
-
+
+
{poolsData.map((poolx, idx) => { const isActive = pool === poolx.id; const isMain = poolx.name.toLowerCase().includes('main'); @@ -24,7 +24,7 @@ const PoolToggle = ({ chain, pool }: { chain: number; pool: string }) => { key={idx} href={`${pathname}?chain=${chain}${poolx.id ? `&pool=${poolx.id}` : ''}`} className={` - flex items-center gap-2 px-3 h-full rounded-md text-sm font-medium transition-all + inline-flex items-center gap-2 px-3 h-full rounded-md text-sm font-medium transition-all ${ isActive ? `${pools[+chain].bg} ${pools[+chain].text}` From cb19e4af8853b0b90ee25e9b9ef244ce5ed18905 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Mon, 2 Dec 2024 11:21:27 +0100 Subject: [PATCH 44/66] accent bg on switch --- packages/ui/app/market/page.tsx | 1 - packages/ui/components/ui/switch.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ui/app/market/page.tsx b/packages/ui/app/market/page.tsx index 7023799ba..d97835746 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -60,7 +60,6 @@ export default function Market() { const [filteredMarketData, setFilteredMarketData] = useState( [] ); - console.log('filteredMarketData', filteredMarketData); const { marketData, isLoading, poolData, selectedMarketData, loopProps } = useMarketData(selectedPool, chain, selectedSymbol); 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) => ( Date: Mon, 2 Dec 2024 14:57:29 +0100 Subject: [PATCH 45/66] fix transaction steps handler --- .../_components/dialogs/manage/BorrowTab.tsx | 16 +++++++++------- .../_components/dialogs/manage/RepayTab.tsx | 16 +++++++++------- .../_components/dialogs/manage/SupplyTab.tsx | 17 ++++++++++------- .../dialogs/manage/WithdrawTab.tsx | 19 ++++++++++--------- .../app/_components/dialogs/manage/index.tsx | 12 ++---------- 5 files changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx b/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx index c80060cf3..7a41e373b 100644 --- a/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx @@ -47,7 +47,6 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { currentUtilizationPercentage, handleUtilization, hfpStatus, - transactionSteps, resetTransactionSteps, chainId, normalizedHealthFactor, @@ -111,7 +110,8 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { borrowLimits.min && parseFloat(amount) < parseFloat(borrowLimits.min); - const { addStepsForAction, upsertTransactionStep } = useTransactionSteps(); + const { addStepsForAction, transactionSteps, upsertTransactionStep } = + useTransactionSteps(); const borrowAmount = async () => { if ( @@ -303,11 +303,13 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => {
{transactionSteps.length > 0 ? ( - +
+ +
) : (
{transactionSteps.length > 0 ? ( - +
+ +
) : (
) : (
) : (
) : ( + Continue + +
)}
From 97f60a4e5430bf0293469cec048387a392b0698b Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Tue, 3 Dec 2024 18:09:50 +0100 Subject: [PATCH 55/66] remove redundant logic --- .../app/_components/dialogs/manage/index.tsx | 40 ++----------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/packages/ui/app/_components/dialogs/manage/index.tsx b/packages/ui/app/_components/dialogs/manage/index.tsx index cb5c3c129..ab9d5be45 100644 --- a/packages/ui/app/_components/dialogs/manage/index.tsx +++ b/packages/ui/app/_components/dialogs/manage/index.tsx @@ -37,11 +37,6 @@ const SwapWidget = dynamic(() => import('../../markets/SwapWidget'), { ssr: false }); -export enum PopupMode { - MANAGE = 1, - LOOP = 2 -} - export type ActiveTab = 'borrow' | 'repay' | 'supply' | 'withdraw'; export enum HFPStatus { @@ -71,37 +66,8 @@ const ManageDialog = ({ const [swapWidgetOpen, setSwapWidgetOpen] = useState(false); const chainId = useChainId(); const { data: usdPrice } = useUsdPrice(chainId.toString()); - - // Store active tab in localStorage to persist across tab switches - const [currentActiveTab, setCurrentActiveTab] = useState(() => { - if (typeof window !== 'undefined') { - const stored = localStorage.getItem('lastActiveTab'); - return (stored as ActiveTab) || activeTab; - } - return activeTab; - }); - - // Update localStorage when tab changes - useEffect(() => { - if (typeof window !== 'undefined') { - localStorage.setItem('lastActiveTab', currentActiveTab); - } - }, [currentActiveTab]); - - // Component visibility state - const [isVisible, setIsVisible] = useState(true); - - // Handle visibility change - useEffect(() => { - const handleVisibilityChange = () => { - setIsVisible(!document.hidden); - }; - - document.addEventListener('visibilitychange', handleVisibilityChange); - return () => { - document.removeEventListener('visibilitychange', handleVisibilityChange); - }; - }, []); + const [currentActiveTab, setCurrentActiveTab] = + useState(activeTab); const pricePerSingleAsset = useMemo( () => @@ -281,7 +247,7 @@ const ManageDialog = ({ selectedMarketData={selectedMarketData} > Date: Wed, 4 Dec 2024 17:14:55 +0100 Subject: [PATCH 56/66] cleanup --- .../_components/dialogs/manage/BorrowTab.tsx | 6 +- .../manage/TransactionStepsHandler.tsx | 9 ++- packages/ui/context/ManageDialogContext.tsx | 63 ------------------- packages/ui/hooks/market/useBorrow.ts | 26 +++----- 4 files changed, 15 insertions(+), 89 deletions(-) diff --git a/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx b/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx index a2c800f3e..c6f92c324 100644 --- a/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx @@ -36,8 +36,6 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { isLoadingUpdatedAssets, updatedValues, comptrollerAddress, - minBorrowAmount, - maxBorrowAmount, setPredictionAmount } = useManageDialogContext(); @@ -56,9 +54,7 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { } = useBorrow({ selectedMarketData, chainId, - comptrollerAddress, - minBorrowAmount, - maxBorrowAmount + comptrollerAddress }); const { isLoadingPredictedHealthFactor, healthFactor, hfpStatus } = useHealth( diff --git a/packages/ui/app/_components/dialogs/manage/TransactionStepsHandler.tsx b/packages/ui/app/_components/dialogs/manage/TransactionStepsHandler.tsx index dcb6fa17b..11b99e8a4 100644 --- a/packages/ui/app/_components/dialogs/manage/TransactionStepsHandler.tsx +++ b/packages/ui/app/_components/dialogs/manage/TransactionStepsHandler.tsx @@ -87,9 +87,12 @@ function TransactionStepsHandler({ chainId }: TransactionStepsHandlerProps) { return ( -
+
{transactionSteps.map((transactionStep, i) => ( -
+
)} From c7a69cfd6d4d75c42dfb7fa83e8c7fed4f8d1f34 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Thu, 5 Dec 2024 08:52:52 +0100 Subject: [PATCH 61/66] make loop dialog overflow y auto --- packages/ui/app/_components/dialogs/loop/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/app/_components/dialogs/loop/index.tsx b/packages/ui/app/_components/dialogs/loop/index.tsx index ad5fc289a..00c48a6d3 100644 --- a/packages/ui/app/_components/dialogs/loop/index.tsx +++ b/packages/ui/app/_components/dialogs/loop/index.tsx @@ -588,7 +588,7 @@ export default function Loop({ > From cda880a8205edec9c886bbd763fcbe5180d86556 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Thu, 5 Dec 2024 08:56:56 +0100 Subject: [PATCH 62/66] fix alignment on mobile --- packages/ui/app/_components/dialogs/manage/BorrowTab.tsx | 2 +- packages/ui/app/_components/dialogs/manage/RepayTab.tsx | 2 +- packages/ui/app/_components/dialogs/manage/SupplyTab.tsx | 2 +- packages/ui/app/_components/dialogs/manage/WithdrawTab.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx b/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx index f817c3bee..a4bf608c9 100644 --- a/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/BorrowTab.tsx @@ -122,7 +122,7 @@ const BorrowTab = ({ maxAmount, isLoadingMax, totalStats }: BorrowTabProps) => { ]} /> -
+
MIN BORROW diff --git a/packages/ui/app/_components/dialogs/manage/RepayTab.tsx b/packages/ui/app/_components/dialogs/manage/RepayTab.tsx index 56a95ee72..7c0707d0b 100644 --- a/packages/ui/app/_components/dialogs/manage/RepayTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/RepayTab.tsx @@ -91,7 +91,7 @@ const RepayTab = ({ maxAmount, isLoadingMax, totalStats }: RepayTabProps) => { availableStates={[HFPStatus.CRITICAL]} /> -
+
CURRENTLY BORROWING diff --git a/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx b/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx index 77be2ef14..bc739c806 100644 --- a/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/SupplyTab.tsx @@ -107,7 +107,7 @@ const SupplyTab = ({ handleUtilization={handleUtilization} /> -
+
diff --git a/packages/ui/app/_components/dialogs/manage/WithdrawTab.tsx b/packages/ui/app/_components/dialogs/manage/WithdrawTab.tsx index f3a6b5b24..8ae10643a 100644 --- a/packages/ui/app/_components/dialogs/manage/WithdrawTab.tsx +++ b/packages/ui/app/_components/dialogs/manage/WithdrawTab.tsx @@ -108,7 +108,7 @@ const WithdrawTab = ({ ]} /> -
+
Market Supply Balance From c2a6c7aa5f87ac7a6fbf47256073c5bc24cae1c7 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Thu, 5 Dec 2024 09:35:09 +0100 Subject: [PATCH 63/66] hide 0 option --- .../ui/app/_components/dialogs/loop/BorrowActions.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx b/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx index d06cce575..1277af869 100644 --- a/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx +++ b/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx @@ -94,7 +94,6 @@ function BorrowActions({
{[ - '0x', '1x', '2x', '3x', @@ -118,7 +117,7 @@ function BorrowActions({ onClick={() => setCurrentLeverage(i > maxLoop ? maxLoop + 1 : i + 1) } - style={{ left: `${(i / 10) * 100}%` }} + style={{ left: `${(i / 9) * 100}%` }} > {label} @@ -127,8 +126,8 @@ function BorrowActions({ setCurrentLeverage(val > maxLoop ? maxLoop + 1 : val + 1) } @@ -137,7 +136,6 @@ function BorrowActions({
{'<'} Repay - Borrow {'>'}
From f08bf33ab231a0e4b9ecaeec60641c9d53b686f6 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Thu, 5 Dec 2024 11:50:17 +0100 Subject: [PATCH 64/66] start loop leverage at 2x --- .../dialogs/loop/BorrowActions.tsx | 72 +++++------ .../ui/app/_components/dialogs/loop/index.tsx | 2 +- packages/ui/components/ui/slider.tsx | 121 +++++++++++------- 3 files changed, 107 insertions(+), 88 deletions(-) diff --git a/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx b/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx index 1277af869..2db7be5f4 100644 --- a/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx +++ b/packages/ui/app/_components/dialogs/loop/BorrowActions.tsx @@ -4,10 +4,10 @@ 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 Range from '../../Range'; import ResultHandler from '../../ResultHandler'; import Amount from '../manage/Amount'; @@ -49,7 +49,13 @@ function BorrowActions({ chainId, true ); - const maxLoop = 2; + const maxAllowedLoop = 3; + + const marks = Array.from({ length: 8 }, (_, i) => ({ + value: i + 2, + label: `${i + 2}x`, + isDisabled: i + 2 > maxAllowedLoop + })); return ( @@ -87,54 +93,34 @@ function BorrowActions({
LOOP
- {(currentLeverage - 1).toFixed(1)} + {currentLeverage.toFixed(1)}x
-
-
- {[ - '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 / 9) * 100}%` }} - > - {label} - - ))} -
- - + - setCurrentLeverage(val > maxLoop ? maxLoop + 1 : val + 1) - } + min={2} step={1} + marks={marks} + value={[currentLeverage]} + currentPosition={currentPositionLeverage} + onMarkClick={(value) => { + 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 {'>'}
diff --git a/packages/ui/app/_components/dialogs/loop/index.tsx b/packages/ui/app/_components/dialogs/loop/index.tsx index 00c48a6d3..4ef247a21 100644 --- a/packages/ui/app/_components/dialogs/loop/index.tsx +++ b/packages/ui/app/_components/dialogs/loop/index.tsx @@ -125,7 +125,7 @@ export default function Loop({ : undefined, chainId ); - const [currentLeverage, setCurrentLeverage] = useState(1); + const [currentLeverage, setCurrentLeverage] = useState(2); const { data: borrowApr } = useGetPositionBorrowApr({ amount: amountAsBInt, borrowMarket: selectedBorrowAsset?.cToken ?? ('' as Address), diff --git a/packages/ui/components/ui/slider.tsx b/packages/ui/components/ui/slider.tsx index d0439ed44..89b291432 100644 --- a/packages/ui/components/ui/slider.tsx +++ b/packages/ui/components/ui/slider.tsx @@ -6,55 +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, value, ...props }, ref) => { - const percentage = value ? value[0] : 0; - const getColor = () => (percentage <= 50 ? 'bg-accent' : 'bg-lime'); - const getTextColor = () => (percentage <= 50 ? 'text-accent' : 'text-lime'); - const percentages = [0, 20, 40, 60, 80, 100]; - - return ( -
-
- {percentages.map((percent) => ( - - {percent}% - - ))} -
- ( + ( + { 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} + + ); + })} +
)} - value={value} - {...props} - > - - - - - -
- ); -}); + value={value} + {...props} + > + + + + +
+
+ ); + } +); Slider.displayName = SliderPrimitive.Root.displayName; export { Slider }; From fea2f567f342febba841f3aed95021789c870036 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Thu, 5 Dec 2024 13:29:42 +0100 Subject: [PATCH 65/66] fix repay --- packages/ui/hooks/market/useRepay.ts | 291 +++++++++++++++----------- packages/ui/hooks/market/useSupply.ts | 3 +- 2 files changed, 168 insertions(+), 126 deletions(-) diff --git a/packages/ui/hooks/market/useRepay.ts b/packages/ui/hooks/market/useRepay.ts index d592f3dd6..a1eb04893 100644 --- a/packages/ui/hooks/market/useRepay.ts +++ b/packages/ui/hooks/market/useRepay.ts @@ -1,12 +1,19 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useState, useEffect, useMemo } from 'react'; import { toast } from 'react-hot-toast'; -import { type Address, formatUnits, parseUnits } from 'viem'; - +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 { - useManageDialogContext, - TransactionType + TransactionType, + useManageDialogContext } from '@ui/context/ManageDialogContext'; import { useMultiIonic } from '@ui/context/MultiIonicContext'; import type { MarketData } from '@ui/types/TokensDataMap'; @@ -14,6 +21,8 @@ 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; @@ -25,12 +34,13 @@ export const useRepay = ({ 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 { currentSdk, address } = useMultiIonic(); + const { transactionSteps } = useTransactionSteps(); const amountAsBInt = useMemo( () => @@ -46,7 +56,6 @@ export const useRepay = ({ const maxAmountNumber = Number( formatUnits(maxAmount ?? 0n, selectedMarketData.underlyingDecimals) ); - const calculatedAmount = ( (newUtilizationPercentage / 100) * maxAmountNumber @@ -59,159 +68,192 @@ export const useRepay = ({ ); useEffect(() => { - if (amount === '0' || !amount) { + if (amount === '0' || !amount || maxAmount === 0n) { 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 { refetch: refetchMaxRepay } = useMaxRepayAmount( - selectedMarketData, - chainId - ); - const currentBorrowAmountAsFloat = useMemo( () => parseFloat(selectedMarketData.borrowBalance.toString()), [selectedMarketData] ); - const repayAmount = async () => { + const repayAmount = useCallback(async () => { if ( - currentSdk && - address && - amount && - amountAsBInt > 0n && - currentBorrowAmountAsFloat - ) { - let currentTransactionStep = 0; - addStepsForType(TransactionType.REPAY, [ - { - error: false, - message: INFO_MESSAGES.REPAY.APPROVE, - success: false - }, - { - error: false, - message: INFO_MESSAGES.REPAY.REPAYING, - success: false - } + !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 ]); - try { - const token = currentSdk.getEIP20TokenInstance( + if (currentAllowance < amountAsBInt) { + const approveTx = await currentSdk.approve( + selectedMarketData.cToken, selectedMarketData.underlyingToken, - currentSdk.publicClient as any + amountAsBInt ); - const hasApprovedEnough = - (await token.read.allowance([address, selectedMarketData.cToken])) >= - amountAsBInt; - - if (!hasApprovedEnough) { - const tx = await currentSdk.approve( - selectedMarketData.cToken, - selectedMarketData.underlyingToken, - (amountAsBInt * 105n) / 100n - ); - - upsertStepForType(TransactionType.REPAY, { - index: currentTransactionStep, - transactionStep: { - error: false, - message: INFO_MESSAGES.REPAY.APPROVE, - txHash: tx, - success: false - } - }); - - await currentSdk.publicClient.waitForTransactionReceipt({ - hash: tx, - confirmations: 2 - }); - await new Promise((resolve) => setTimeout(resolve, 5000)); - } upsertStepForType(TransactionType.REPAY, { index: currentTransactionStep, transactionStep: { error: false, message: INFO_MESSAGES.REPAY.APPROVE, - success: true + txHash: approveTx, + success: false } }); - currentTransactionStep++; + await currentSdk.publicClient.waitForTransactionReceipt({ + hash: approveTx, + confirmations: 2 + }); - const isRepayingMax = - amountAsBInt >= (selectedMarketData.borrowBalance ?? 0n); + await new Promise((resolve) => setTimeout(resolve, 2000)); + } - const { tx, errorCode } = await currentSdk.repay( - selectedMarketData.cToken, - isRepayingMax, - isRepayingMax ? selectedMarketData.borrowBalance : amountAsBInt - ); + currentTransactionStep++; + + // Check if we're repaying the max amount + const isRepayingMax = + amountAsBInt >= (selectedMarketData.borrowBalance ?? 0n); + const repayAmount = isRepayingMax ? maxUint256 : amountAsBInt; - if (errorCode) { - throw new Error('Error during repaying!'); + // 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 } + }); - upsertStepForType(TransactionType.REPAY, { - index: currentTransactionStep, - transactionStep: { - error: false, - message: INFO_MESSAGES.REPAY.REPAYING, - txHash: tx, - success: false - } - }); + if (!currentSdk || !currentSdk.walletClient) { + console.error('SDK or wallet client is not initialized'); + return; + } - if (tx) { - await currentSdk.publicClient.waitForTransactionReceipt({ hash: tx }); - 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}` - ); + // 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 } - } catch (error) { - console.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 - } - }); + await currentSdk.publicClient.waitForTransactionReceipt({ + hash: tx, + confirmations: 1 + }); - toast.error('Error while repaying!'); - } + 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, @@ -231,6 +273,7 @@ export const useRepay = ({ return { isWaitingForIndexing, repayAmount, + transactionSteps, isPolling, currentBorrowAmountAsFloat, amount, diff --git a/packages/ui/hooks/market/useSupply.ts b/packages/ui/hooks/market/useSupply.ts index b47593cfd..f31147db0 100644 --- a/packages/ui/hooks/market/useSupply.ts +++ b/packages/ui/hooks/market/useSupply.ts @@ -40,8 +40,7 @@ export const useSupply = ({ const [utilizationPercentage, setUtilizationPercentage] = useState(0); const { addStepsForType, upsertStepForType } = useManageDialogContext(); - const { addStepsForAction, transactionSteps, upsertTransactionStep } = - useTransactionSteps(); + const { transactionSteps } = useTransactionSteps(); const amountAsBInt = useMemo( () => From b0b4f1e41e05e23279d4234433db26535d7057b8 Mon Sep 17 00:00:00 2001 From: vidvidvid Date: Thu, 5 Dec 2024 13:32:17 +0100 Subject: [PATCH 66/66] fix setting collateral --- .../ui/hooks/market/useCollateralToggle.ts | 229 +++++++++--------- 1 file changed, 112 insertions(+), 117 deletions(-) diff --git a/packages/ui/hooks/market/useCollateralToggle.ts b/packages/ui/hooks/market/useCollateralToggle.ts index df30d249d..e997454d8 100644 --- a/packages/ui/hooks/market/useCollateralToggle.ts +++ b/packages/ui/hooks/market/useCollateralToggle.ts @@ -4,6 +4,10 @@ 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'; @@ -26,8 +30,8 @@ export const useCollateralToggle = ({ ); const { currentSdk } = useMultiIonic(); - const { addStepsForAction, transactionSteps, upsertTransactionStep } = - useTransactionSteps(); + const { addStepsForType, upsertStepForType } = useManageDialogContext(); + const { transactionSteps } = useTransactionSteps(); const handleCollateralToggle = async () => { if (!transactionSteps.length) { @@ -37,122 +41,104 @@ export const useCollateralToggle = ({ 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; - } + if (enableCollateral) { + const comptrollerContract = currentSdk.createComptroller( + comptrollerAddress, + currentSdk.publicClient + ); - 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( + const exitCode = ( + await comptrollerContract.simulate.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; - } + { account: currentSdk.walletClient!.account!.address } + ) + ).result; - 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; + 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(); @@ -160,13 +146,22 @@ export const useCollateralToggle = ({ } catch (error) { console.error(error); - upsertTransactionStep({ + upsertStepForType(TransactionType.COLLATERAL, { index: currentTransactionStep, transactionStep: { - ...transactionSteps[currentTransactionStep], - error: true + error: true, + message: enableCollateral + ? INFO_MESSAGES.COLLATERAL.DISABLE + : INFO_MESSAGES.COLLATERAL.ENABLE, + success: false } }); + + toast.error( + `Error while ${ + enableCollateral ? 'disabling' : 'enabling' + } collateral!` + ); } }