diff --git a/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBox.tsx b/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBox.tsx index 15a4201ee8..baa542d32d 100644 --- a/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBox.tsx +++ b/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBox.tsx @@ -5,7 +5,7 @@ import { ActionType } from "@mrgnlabs/marginfi-v2-ui-state"; import { WSOL_MINT, numeralFormatter } from "@mrgnlabs/mrgn-common"; import { ChevronDownIcon } from "@radix-ui/react-icons"; import { useMrgnlendStore, useUiStore } from "~/store"; -import { MarginfiActionParams, closeBalance, executeLendingAction } from "~/utils"; +import { MarginfiActionParams, closeBalance, cn, executeLendingAction } from "~/utils"; import { LendingModes } from "~/types"; import { useWalletContext } from "~/hooks/useWalletContext"; @@ -20,15 +20,15 @@ import { IconWallet } from "~/components/ui/icons"; import { ActionBoxActions } from "./ActionBoxActions"; export const ActionBox = () => { - const [mfiClient, nativeSolBalance, setIsRefreshingStore, fetchMrgnlendState, selectedAccount] = useMrgnlendStore( - (state) => [ + const [mfiClient, nativeSolBalance, setIsRefreshingStore, fetchMrgnlendState, selectedAccount, accountSummary] = + useMrgnlendStore((state) => [ state.marginfiClient, state.nativeSolBalance, state.setIsRefreshingStore, state.fetchMrgnlendState, state.selectedAccount, - ] - ); + state.accountSummary, + ]); const [lendingMode, setLendingMode, actionMode, setActionMode, selectedToken, setSelectedToken] = useUiStore( (state) => [ state.lendingMode, @@ -92,31 +92,84 @@ export const ActionBox = () => { } }, [lendingMode, selectedToken, setAmount, setActionMode]); + const liquidationPrice = React.useMemo(() => { + const isActive = selectedToken?.isActive; + const isLending = lendingMode === LendingModes.LEND; + let liquidationPrice = 0; + + if (isActive) { + if (!amount || amount === 0 || isLending || !selectedAccount || !selectedToken) { + liquidationPrice = selectedToken?.position.liquidationPrice ?? 0; + } else { + const borrowed = selectedToken?.position.amount ?? 0; + + liquidationPrice = + selectedAccount.computeLiquidationPriceForBankAmount(selectedToken?.address, isLending, amount + borrowed) ?? + 0; + } + } + + return liquidationPrice; + }, [selectedToken, amount, lendingMode]); + + const healthColorLiquidation = React.useMemo(() => { + const isActive = selectedToken?.isActive; + + if (isActive) { + const price = selectedToken.info.oraclePrice.price.toNumber(); + const safety = liquidationPrice / price; + let color: string; + if (safety >= 0.5) { + color = "#75BA80"; // green color " : "#", + } else if (safety >= 0.25) { + color = "#B8B45F"; // yellow color + } else { + color = "#CF6F6F"; // red color + } + + return color; + } else { + return "#fff"; + } + }, [selectedToken, liquidationPrice]); + React.useEffect(() => { - if (!selectedToken || !amount) { + if (!selectedToken) { setPreview([]); return; } + const isActive = selectedToken?.isActive; + let supplied = 0; + let borrowed = 0; + + if (isActive) { + const isLending = selectedToken?.position?.isLending; + if (isLending) supplied = selectedToken?.position.amount ?? 0; + else borrowed = selectedToken?.position.amount ?? 0; + } + const healthFactor = accountSummary.healthFactor; + setPreview([ { - key: "Your deposited amount", - value: `${amount} ${selectedToken.meta.tokenSymbol}`, + key: "Supplied amount", + value: `${numeralFormatter(supplied)}`, }, { - key: "Liquidation price", - value: usdFormatter.format(amount), + key: "Borrowed amount", + value: `${numeralFormatter(borrowed)}`, }, { - key: "Some propertya", - value: "--", + key: "Liquidation price", + value: + liquidationPrice > 0.01 ? usdFormatter.format(liquidationPrice) : `$${liquidationPrice.toExponential(2)}`, }, { - key: "Some propertyb", - value: "--", + key: "Health factor", + value: `${numeralFormatter(healthFactor * 100)}%`, }, ]); - }, [selectedToken, amount]); + }, [selectedToken, amount, liquidationPrice]); React.useEffect(() => { if (!selectedToken || !amountInputRef.current) return; @@ -204,7 +257,7 @@ export const ActionBox = () => { !hasLSTDialogShown.includes(selectedToken.meta.tokenSymbol as LSTDialogVariants) ) { setHasLSTDialogShown((prev) => [...prev, selectedToken.meta.tokenSymbol as LSTDialogVariants]); - setLSTDialogVariant(selectedToken.meta.tokenSymbol); + setLSTDialogVariant(selectedToken.meta.tokenSymbol as LSTDialogVariants); setIsLSTDialogOpen(true); setLSTDialogCallback(() => action); @@ -219,7 +272,7 @@ export const ActionBox = () => { !hasLSTDialogShown.includes(selectedToken.meta.tokenSymbol as LSTDialogVariants) ) { setHasLSTDialogShown((prev) => [...prev, selectedToken.meta.tokenSymbol as LSTDialogVariants]); - setLSTDialogVariant(selectedToken.meta.tokenSymbol); + setLSTDialogVariant(selectedToken.meta.tokenSymbol as LSTDialogVariants); return; } }, [ @@ -321,12 +374,20 @@ export const ActionBox = () => { handleAction={() => (showCloseBalance ? handleCloseBalance() : handleLendingAction())} isLoading={isLoading} /> - {selectedToken !== null && amount !== null && preview.length > 0 && ( + {selectedToken !== null && preview.length > 0 && (
- {preview.map((item) => ( + {preview.map((item, idx) => (
{item.key}
-
{item.value}
+
+ {item.value} +
))}
diff --git a/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxActions.tsx b/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxActions.tsx index e45344f546..c9e4ca97e8 100644 --- a/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxActions.tsx +++ b/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxActions.tsx @@ -6,6 +6,7 @@ import { useUiStore } from "~/store"; import { Button } from "~/components/ui/button"; import { IconLoader } from "~/components/ui/icons"; +import { useWalletContext } from "~/hooks/useWalletContext"; type ActionBoxActionsProps = { amount: number; @@ -23,13 +24,17 @@ export const ActionBoxActions = ({ handleAction, }: ActionBoxActionsProps) => { const [actionMode, selectedToken] = useUiStore((state) => [state.actionMode, state.selectedToken]); + const { connected } = useWalletContext(); const isActionDisabled = React.useMemo(() => { const isValidInput = amount > 0; - return ((maxAmount === 0 || !isValidInput) && !showCloseBalance) || isLoading; + return ((maxAmount === 0 || !isValidInput) && !showCloseBalance) || isLoading || !connected; }, [amount, showCloseBalance, maxAmount, isLoading]); const actionText = React.useMemo(() => { + if (!connected) { + return "Connect your wallet"; + } if (!selectedToken) { return "Select token and amount"; } @@ -37,7 +42,6 @@ export const ActionBoxActions = ({ if (showCloseBalance) { return "Close account"; } - console.log({ actionMode }); if (maxAmount === 0) { switch (actionMode) { @@ -59,7 +63,7 @@ export const ActionBoxActions = ({ } return actionMode; - }, [actionMode, amount, selectedToken, maxAmount, showCloseBalance]); + }, [actionMode, amount, selectedToken, connected, maxAmount, showCloseBalance]); return ( No tokens found. - {lendingMode === LendingModes.LEND && filteredBanksUserOwns.length > 0 && ( + {lendingMode === LendingModes.LEND && connected && filteredBanksUserOwns.length > 0 && ( {filteredBanksUserOwns.slice(0, searchQuery.length === 0 ? 5 : 3).map((bank, index) => ( )} - {(searchQuery.length > 0 || lendingMode === LendingModes.BORROW) && filteredBanks.length > 0 && ( - - {filteredBanks.slice(0, searchQuery.length === 0 ? 5 : 3).map((bank, index) => ( - { - setCurrentToken( - extendedBankInfos.find( - (bankInfo) => bankInfo.address.toString().toLowerCase() === currentValue - ) ?? null - ); - setIsTokenPopoverOpen(false); - }} - className={cn( - "cursor-pointer font-medium flex items-center justify-between gap-2 data-[selected=true]:bg-background-gray-light data-[selected=true]:text-white", - lendingMode === LendingModes.LEND && "py-2", - lendingMode === LendingModes.BORROW && "h-[60px]" - )} - > - - - ))} - - )} + {(searchQuery.length > 0 || lendingMode === LendingModes.BORROW || !connected) && + filteredBanks.length > 0 && ( + + {filteredBanks.slice(0, searchQuery.length === 0 ? 5 : 3).map((bank, index) => ( + { + setCurrentToken( + extendedBankInfos.find( + (bankInfo) => bankInfo.address.toString().toLowerCase() === currentValue + ) ?? null + ); + setIsTokenPopoverOpen(false); + }} + className={cn( + "cursor-pointer font-medium flex items-center justify-between gap-2 data-[selected=true]:bg-background-gray-light data-[selected=true]:text-white", + lendingMode === LendingModes.LEND && "py-2", + lendingMode === LendingModes.BORROW && "h-[60px]" + )} + > + + + ))} + + )} diff --git a/apps/marginfi-v2-ui/src/components/common/AssetList/AssetListFilters.tsx b/apps/marginfi-v2-ui/src/components/common/AssetList/AssetListFilters.tsx index e5ed81171e..c07686d2bd 100644 --- a/apps/marginfi-v2-ui/src/components/common/AssetList/AssetListFilters.tsx +++ b/apps/marginfi-v2-ui/src/components/common/AssetList/AssetListFilters.tsx @@ -9,11 +9,13 @@ import { MrgnContainedSwitch } from "~/components/common/MrgnContainedSwitch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"; import { IconFilter, IconSortAscending, IconSortDescending } from "~/components/ui/icons"; -import { LendingModes, PoolTypes, SortType, sortDirection, SortAssetOption } from "~/types"; +import { LendingModes, PoolTypes, SortType, sortDirection, SortAssetOption, UserMode } from "~/types"; export const AssetListFilters = () => { const { connected } = useWalletContext(); const [ + userMode, + setUserMode, lendingMode, setLendingMode, poolFilter, @@ -24,6 +26,8 @@ export const AssetListFilters = () => { sortOption, setSortOption, ] = useUiStore((state) => [ + state.userMode, + state.setUserMode, state.lendingMode, state.setLendingMode, state.poolFilter, @@ -38,87 +42,108 @@ export const AssetListFilters = () => { return (
-
- setLendingMode(lendingMode === LendingModes.LEND ? LendingModes.BORROW : LendingModes.LEND)} - /> +
+
+ + setLendingMode(lendingMode === LendingModes.LEND ? LendingModes.BORROW : LendingModes.LEND) + } + /> +
+
+
+ { + setUserMode(userMode === UserMode.PRO ? UserMode.LITE : UserMode.PRO); + }} + inputProps={{ "aria-label": "controlled" }} + className={cn(`${!connected && "pointer-events-none"}`)} + /> +
Pro mode
+
+
-
{ - e.stopPropagation(); - if (connected) return; - setIsWalletAuthDialogOpen(true); - }} - > - { - setIsFilteredUserPositions(!isFilteredUserPositions); - setPoolFilter(PoolTypes.ALL); + {userMode === UserMode.PRO && ( +
{ + e.stopPropagation(); + if (connected) return; + setIsWalletAuthDialogOpen(true); }} - inputProps={{ "aria-label": "controlled" }} - className={cn(!connected && "pointer-events-none")} - /> -
Filter my positions
-
-
-
- + inputProps={{ "aria-label": "controlled" }} + className={cn(!connected && "pointer-events-none")} + /> +
Filter my positions
-
- + )} + {userMode === UserMode.PRO && ( +
+
+ +
+
+ +
-
+ )}
); diff --git a/apps/marginfi-v2-ui/src/components/common/AssetList/AssetRowAction.tsx b/apps/marginfi-v2-ui/src/components/common/AssetList/AssetRowAction.tsx index 7dc50d4290..b5a9becafd 100644 --- a/apps/marginfi-v2-ui/src/components/common/AssetList/AssetRowAction.tsx +++ b/apps/marginfi-v2-ui/src/components/common/AssetList/AssetRowAction.tsx @@ -1,14 +1,15 @@ import { FC, ReactNode } from "react"; import { Button, ButtonProps } from "@mui/material"; +import { cn } from "~/utils"; interface AssetRowActionProps extends ButtonProps { children?: ReactNode; bgColor?: string; } -const AssetRowAction: FC = ({ children, disabled, bgColor, ...otherProps }) => ( +const AssetRowAction: FC = ({ children, disabled, bgColor, className, ...otherProps }) => (
-
-
+
+
Supplied
{accountSupplied}
@@ -141,7 +141,7 @@ export const Portfolio = () => { )}
-
+
Borrowed
{accountBorrowed}
diff --git a/apps/marginfi-v2-ui/src/components/common/Portfolio/UserStats/UserStats.tsx b/apps/marginfi-v2-ui/src/components/common/Portfolio/UserStats/UserStats.tsx index bc16c83d1b..a94ae1c975 100644 --- a/apps/marginfi-v2-ui/src/components/common/Portfolio/UserStats/UserStats.tsx +++ b/apps/marginfi-v2-ui/src/components/common/Portfolio/UserStats/UserStats.tsx @@ -9,7 +9,7 @@ interface props { export const UserStats: FC = ({ supplied, borrowed, netValue, interest }) => { return ( -
+
@@ -19,7 +19,7 @@ export const UserStats: FC = ({ supplied, borrowed, netValue, interest }) }; const Stat = ({ label, value }: { label: string; value: string }) => ( -
+
{label}
{value}
diff --git a/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetRow/AssetRow.tsx b/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetRow/AssetRow.tsx index 7f1e05291f..5c8ae10a33 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetRow/AssetRow.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetRow/AssetRow.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback, useEffect, useMemo, useState } from "react"; +import React from "react"; import clsx from "clsx"; import Image from "next/image"; import Badge from "@mui/material/Badge"; @@ -14,11 +14,9 @@ import { } from "@mrgnlabs/marginfi-v2-ui-state"; import { MarginfiAccountWrapper, PriceBias } from "@mrgnlabs/marginfi-client-v2"; -import { useMrgnlendStore, useUserProfileStore, useUiStore } from "~/store"; -import { closeBalance, executeLendingAction, MarginfiActionParams, cn } from "~/utils"; +import { useUserProfileStore, useUiStore } from "~/store"; import { LendingModes } from "~/types"; import { useAssetItemData } from "~/hooks/useAssetItemData"; -import { useWalletContext } from "~/hooks/useWalletContext"; import { useIsMobile } from "~/hooks/useIsMobile"; import { MrgnTooltip } from "~/components/common/MrgnTooltip"; @@ -26,6 +24,7 @@ import { AssetRowAction, LSTDialogVariants } from "~/components/common/AssetList import { ActionBoxDialog } from "~/components/common/ActionBox"; import { Button } from "~/components/ui/button"; import { IconAlertTriangle } from "~/components/ui/icons"; +import { cn } from "~/utils"; export const EMISSION_MINT_INFO_MAP = new Map([ [ @@ -46,7 +45,7 @@ export const EMISSION_MINT_INFO_MAP = new Map { const [lendZoomLevel, denominationUSD] = useUserProfileStore((state) => [state.lendZoomLevel, state.denominationUSD]); - const setIsRefreshingStore = useMrgnlendStore((state) => state.setIsRefreshingStore); - const [mfiClient, fetchMrgnlendState] = useMrgnlendStore((state) => [state.marginfiClient, state.fetchMrgnlendState]); const [lendingMode, isFilteredUserPositions, setSelectedToken] = useUiStore((state) => [ state.lendingMode, state.isFilteredUserPositions, state.setSelectedToken, ]); const { rateAP, assetWeight, isBankFilled, isBankHigh, bankCap } = useAssetItemData({ bank, isInLendingMode }); - const [hasLSTDialogShown, setHasLSTDialogShown] = useState([]); - const { walletContextState } = useWalletContext(); const isMobile = useIsMobile(); - const isReduceOnly = useMemo( + const isReduceOnly = React.useMemo( () => (bank?.meta?.tokenSymbol ? REDUCE_ONLY_BANKS.includes(bank.meta.tokenSymbol) : false), [bank.meta.tokenSymbol] ); - const isUserPositionPoorHealth = useMemo(() => { + const isUserPositionPoorHealth = React.useMemo(() => { if (!activeBank || !activeBank.position.liquidationPrice) { return false; } @@ -108,7 +103,7 @@ const AssetRow: FC<{ } }, [activeBank]); - const userPositionColSpan = useMemo(() => { + const userPositionColSpan = React.useMemo(() => { if (isMobile) { return 4; } @@ -121,13 +116,13 @@ const AssetRow: FC<{ return 9; }, [isMobile, lendZoomLevel]); - const assetPrice = useMemo( + const assetPrice = React.useMemo( () => bank.info.oraclePrice.priceRealtime ? bank.info.oraclePrice.priceRealtime.toNumber() : bank.info.state.price, [bank.info.oraclePrice.priceRealtime, bank.info.state.price] ); - const assetPriceOffset = useMemo( + const assetPriceOffset = React.useMemo( () => Math.max( bank.info.rawBank.getPrice(bank.info.oraclePrice, PriceBias.Highest).toNumber() - bank.info.state.price, @@ -136,137 +131,13 @@ const AssetRow: FC<{ [bank.info] ); - const [amount, setAmount] = useState(0); - - const currentAction: ActionType = useMemo(() => getCurrentAction(isInLendingMode, bank), [isInLendingMode, bank]); - - const maxAmount = useMemo(() => { - switch (currentAction) { - case ActionType.Deposit: - return bank.userInfo.maxDeposit; - case ActionType.Withdraw: - return bank.userInfo.maxWithdraw; - case ActionType.Borrow: - return bank.userInfo.maxBorrow; - case ActionType.Repay: - return bank.userInfo.maxRepay; - } - }, [bank, currentAction]); - const isDust = bank.isActive && bank.position.isDust; - const showCloseBalance = currentAction === ActionType.Withdraw && isDust; // Only case we should show close balance is when we are withdrawing a dust balance, since user receives 0 tokens back (vs repaying a dust balance where the input box will show the smallest unit of the token) - const isActionDisabled = maxAmount === 0 && !showCloseBalance; - - // Reset b/l amounts on toggle - useEffect(() => { - setAmount(0); - }, [isInLendingMode]); - - const handleCloseBalance = useCallback(async () => { - try { - await closeBalance({ marginfiAccount, bank }); - } catch (error) { - return; - } - - setAmount(0); - - try { - setIsRefreshingStore(true); - await fetchMrgnlendState(); - } catch (error: any) { - console.log("Error while reloading state"); - console.log(error); - } - }, [bank, marginfiAccount, fetchMrgnlendState, setIsRefreshingStore]); - - const executeLendingActionCb = useCallback( - async ({ - mfiClient, - actionType: currentAction, - bank, - amount: borrowOrLendAmount, - nativeSolBalance, - marginfiAccount, - walletContextState, - }: MarginfiActionParams) => { - await executeLendingAction({ - mfiClient, - actionType: currentAction, - bank, - amount: borrowOrLendAmount, - nativeSolBalance, - marginfiAccount, - walletContextState, - }); - - setAmount(0); - - // -------- Refresh state - try { - setIsRefreshingStore(true); - await fetchMrgnlendState(); - } catch (error: any) { - console.log("Error while reloading state"); - console.log(error); - } - }, - [fetchMrgnlendState, setIsRefreshingStore] + const currentAction: ActionType = React.useMemo( + () => getCurrentAction(isInLendingMode, bank), + [isInLendingMode, bank] ); - const handleLendingAction = useCallback(async () => { - if ( - currentAction === ActionType.Deposit && - (bank.meta.tokenSymbol === "SOL" || bank.meta.tokenSymbol === "stSOL") && - !hasLSTDialogShown.includes(bank.meta.tokenSymbol as LSTDialogVariants) && - showLSTDialog - ) { - setHasLSTDialogShown((prev) => [...prev, bank.meta.tokenSymbol as LSTDialogVariants]); - showLSTDialog(bank.meta.tokenSymbol as LSTDialogVariants, async () => { - await executeLendingActionCb({ - mfiClient, - actionType: currentAction, - bank, - amount: amount, - nativeSolBalance, - marginfiAccount, - walletContextState, - }); - }); - return; - } - - await executeLendingActionCb({ - mfiClient, - actionType: currentAction, - bank, - amount: amount, - nativeSolBalance, - marginfiAccount, - walletContextState, - }); - - if ( - currentAction === ActionType.Withdraw && - (bank.meta.tokenSymbol === "SOL" || bank.meta.tokenSymbol === "stSOL") && - !hasLSTDialogShown.includes(bank.meta.tokenSymbol as LSTDialogVariants) && - showLSTDialog - ) { - setHasLSTDialogShown((prev) => [...prev, bank.meta.tokenSymbol as LSTDialogVariants]); - showLSTDialog(bank.meta.tokenSymbol as LSTDialogVariants); - return; - } - }, [ - currentAction, - bank, - hasLSTDialogShown, - showLSTDialog, - executeLendingActionCb, - mfiClient, - amount, - nativeSolBalance, - marginfiAccount, - walletContextState, - ]); + const isDust = React.useMemo(() => bank.isActive && bank.position.isDust, [bank]); + const showCloseBalance = currentAction === ActionType.Withdraw && isDust; // Only case we should show close balance is when we are withdrawing a dust balance, since user receives 0 tokens back (vs repaying a dust balance where the input box will show the smallest unit of the token) return ( <> @@ -597,7 +468,7 @@ const AssetRow: FC<{ ); }; -const LoadingAsset: FC<{ isInLendingMode: boolean; bankMetadata: ExtendedBankMetadata }> = ({ +const LoadingAsset: React.FC<{ isInLendingMode: boolean; bankMetadata: ExtendedBankMetadata }> = ({ isInLendingMode, bankMetadata, }) => ( diff --git a/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetsList.tsx b/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetsList.tsx index e7294ef3c3..b337e0c72e 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetsList.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetsList.tsx @@ -191,7 +191,7 @@ const AssetsList = () => { {poolFilter !== "isolated" && ( <> -
+
Global pool
{ - {globalBanks - .filter((b) => !b.info.state.isIsolated) - .map((bank, i) => { + {globalBanks.length ? ( + globalBanks.map((bank, i) => { if (poolFilter === "stable" && !STABLECOINS.includes(bank.meta.tokenSymbol)) return null; if (poolFilter === "lst" && !LSTS.includes(bank.meta.tokenSymbol)) return null; @@ -247,14 +246,21 @@ const AssetsList = () => { bankMetadata={bank.meta} /> ); - })} + }) + ) : ( + + +
No global banks found.
+
+
+ )}
)} {poolFilter !== "stable" && poolFilter !== "lst" && ( <> -
+
Isolated pools @@ -280,9 +286,8 @@ const AssetsList = () => { - {isolatedBanks - .filter((b) => b.info.state.isIsolated) - .map((bank) => { + {isolatedBanks.length ? ( + isolatedBanks.map((bank) => { const activeBank = activeBankInfos.filter( (activeBankInfo) => activeBankInfo.meta.tokenSymbol === bank.meta.tokenSymbol ); @@ -308,7 +313,14 @@ const AssetsList = () => { bankMetadata={bank.meta} /> ); - })} + }) + ) : ( + + +
No isolated banks found.
+
+
+ )}
diff --git a/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCard.tsx b/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCard.tsx index cb121670f7..4bc5ea7fe0 100644 --- a/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCard.tsx +++ b/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCard.tsx @@ -1,37 +1,29 @@ -import React, { FC, useCallback, useMemo, useState } from "react"; +import React, { FC, useMemo } from "react"; import { WSOL_MINT } from "@mrgnlabs/mrgn-common"; import { ExtendedBankInfo, ActiveBankInfo, ActionType, getCurrentAction } from "@mrgnlabs/marginfi-v2-ui-state"; -import { MarginfiAccountWrapper } from "@mrgnlabs/marginfi-client-v2"; -import { useMrgnlendStore, useUiStore } from "~/store"; -import { executeLendingAction, closeBalance, MarginfiActionParams } from "~/utils"; + +import { useUiStore } from "~/store"; +import { LendingModes } from "~/types"; + import { useAssetItemData } from "~/hooks/useAssetItemData"; -import { useWalletContext } from "~/hooks/useWalletContext"; import { LSTDialogVariants } from "~/components/common/AssetList"; + import { AssetCardStats } from "./AssetCardStats"; import { AssetCardActions } from "./AssetCardActions"; import { AssetCardPosition } from "./AssetCardPosition"; import { AssetCardHeader } from "./AssetCardHeader"; -import { LendingModes } from "~/types"; export const AssetCard: FC<{ bank: ExtendedBankInfo; activeBank?: ActiveBankInfo; nativeSolBalance: number; isInLendingMode: boolean; - isConnected: boolean; - marginfiAccount: MarginfiAccountWrapper | null; - inputRefs?: React.MutableRefObject>; - showLSTDialog?: (variant: LSTDialogVariants, callback?: () => void) => void; -}> = ({ bank, activeBank, nativeSolBalance, isInLendingMode, marginfiAccount, inputRefs, showLSTDialog }) => { +}> = ({ bank, activeBank, nativeSolBalance, isInLendingMode }) => { const { rateAP, assetWeight, isBankFilled, isBankHigh, bankCap } = useAssetItemData({ bank, isInLendingMode }); - const [mfiClient, fetchMrgnlendState] = useMrgnlendStore((state) => [state.marginfiClient, state.fetchMrgnlendState]); - const setIsRefreshingStore = useMrgnlendStore((state) => state.setIsRefreshingStore); const [lendingMode, isFilteredUserPositions] = useUiStore((state) => [ state.lendingMode, state.isFilteredUserPositions, ]); - const [hasLSTDialogShown, setHasLSTDialogShown] = useState([]); - const { walletContextState } = useWalletContext(); const totalDepositsOrBorrows = useMemo( () => @@ -54,111 +46,6 @@ export const AssetCard: FC<{ const currentAction: ActionType = useMemo(() => getCurrentAction(isInLendingMode, bank), [isInLendingMode, bank]); - const handleCloseBalance = useCallback(async () => { - try { - await closeBalance({ marginfiAccount, bank }); - } catch (error) { - return; - } - - try { - setIsRefreshingStore(true); - await fetchMrgnlendState(); - } catch (error: any) { - console.log("Error while reloading state"); - console.log(error); - } - }, [bank, marginfiAccount, fetchMrgnlendState, setIsRefreshingStore]); - - const executeLendingActionCb = useCallback( - async ( - amount: number, - { - mfiClient, - actionType: currentAction, - bank, - nativeSolBalance, - marginfiAccount, - walletContextState, - }: Omit - ) => { - await executeLendingAction({ - mfiClient, - actionType: currentAction, - bank, - amount, - nativeSolBalance, - marginfiAccount, - walletContextState, - }); - - // -------- Refresh state - try { - setIsRefreshingStore(true); - await fetchMrgnlendState(); - } catch (error: any) { - console.log("Error while reloading state"); - console.log(error); - } - }, - [fetchMrgnlendState, setIsRefreshingStore] - ); - - const handleLendingAction = useCallback( - async (borrowOrLendAmount: number) => { - if ( - currentAction === ActionType.Deposit && - (bank.meta.tokenSymbol === "SOL" || bank.meta.tokenSymbol === "stSOL") && - !hasLSTDialogShown.includes(bank.meta.tokenSymbol as LSTDialogVariants) && - showLSTDialog - ) { - setHasLSTDialogShown((prev) => [...prev, bank.meta.tokenSymbol as LSTDialogVariants]); - showLSTDialog(bank.meta.tokenSymbol as LSTDialogVariants, async () => { - await executeLendingActionCb(borrowOrLendAmount, { - mfiClient, - actionType: currentAction, - bank, - nativeSolBalance, - marginfiAccount, - walletContextState, - }); - }); - return; - } - - await executeLendingActionCb(borrowOrLendAmount, { - mfiClient, - actionType: currentAction, - bank, - nativeSolBalance, - marginfiAccount, - walletContextState, - }); - - if ( - currentAction === ActionType.Withdraw && - (bank.meta.tokenSymbol === "SOL" || bank.meta.tokenSymbol === "stSOL") && - !hasLSTDialogShown.includes(bank.meta.tokenSymbol as LSTDialogVariants) && - showLSTDialog - ) { - setHasLSTDialogShown((prev) => [...prev, bank.meta.tokenSymbol as LSTDialogVariants]); - showLSTDialog(bank.meta.tokenSymbol as LSTDialogVariants); - return; - } - }, - [ - currentAction, - bank, - hasLSTDialogShown, - showLSTDialog, - executeLendingActionCb, - mfiClient, - nativeSolBalance, - marginfiAccount, - walletContextState, - ] - ); - return (
)} - +
); }; diff --git a/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCardActions.tsx b/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCardActions.tsx index 5370b86aa0..75beb9fe14 100644 --- a/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCardActions.tsx +++ b/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCardActions.tsx @@ -1,70 +1,38 @@ import React, { FC, useMemo, useState } from "react"; import { ExtendedBankInfo, ActionType } from "@mrgnlabs/marginfi-v2-ui-state"; -import { uiToNative } from "@mrgnlabs/mrgn-common"; + import { AssetRowAction, AssetRowInputBox } from "~/components/common/AssetList"; +import { ActionBoxDialog } from "~/components/common/ActionBox"; +import { useUiStore } from "~/store"; export const AssetCardActions: FC<{ bank: ExtendedBankInfo; - isBankFilled: boolean; - isInLendingMode: boolean; currentAction: ActionType; - inputRefs?: React.MutableRefObject>; - onCloseBalance: () => void; - onBorrowOrLend: (amount: number) => void; -}> = ({ bank, inputRefs, currentAction, onCloseBalance, onBorrowOrLend }) => { - const [borrowOrLendAmount, setBorrowOrLendAmount] = useState(0); - - const maxAmount = useMemo(() => { - switch (currentAction) { - case ActionType.Deposit: - return bank.userInfo.maxDeposit; - case ActionType.Withdraw: - return bank.userInfo.maxWithdraw; - case ActionType.Borrow: - return bank.userInfo.maxBorrow; - case ActionType.Repay: - return bank.userInfo.maxRepay; - } - }, [bank.userInfo, currentAction]); +}> = ({ bank, currentAction }) => { + const [setSelectedToken] = useUiStore((state) => [state.setSelectedToken]); - const isDust = useMemo( - () => bank.isActive && uiToNative(bank.position.amount, bank.info.state.mintDecimals).isZero(), - [bank] - ); - - const isDisabled = useMemo( - () => - (isDust && - uiToNative(bank.userInfo.tokenAccount.balance, bank.info.state.mintDecimals).isZero() && - currentAction == ActionType.Borrow) || - (!isDust && maxAmount === 0), - [currentAction, bank, isDust, maxAmount] - ); + const isDust = React.useMemo(() => bank.isActive && bank.position.isDust, [bank]); + const showCloseBalance = React.useMemo( + () => currentAction === ActionType.Withdraw && isDust, + [currentAction, isDust] + ); // Only case we should show close balance is when we are withdrawing a dust balance, since user receives 0 tokens back (vs repaying a dust balance where the input box will show the smallest unit of the token) return ( <>
- onBorrowOrLend(borrowOrLendAmount)} - /> - (isDust ? onCloseBalance() : onBorrowOrLend(borrowOrLendAmount))} - disabled={isDisabled} - > - {isDust ? "Close" : currentAction} - + + setSelectedToken(bank)} + > + {showCloseBalance ? "Close" : currentAction} + +
); diff --git a/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/MobileAssetsList.tsx b/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/MobileAssetsList.tsx index d6bffb3d5c..9f6be0b96f 100644 --- a/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/MobileAssetsList.tsx +++ b/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/MobileAssetsList.tsx @@ -6,7 +6,6 @@ import { ExtendedBankInfo, ActiveBankInfo } from "@mrgnlabs/marginfi-v2-ui-state import { Skeleton, Typography } from "@mui/material"; import { useMrgnlendStore, useUiStore } from "~/store"; -import { useWalletContext } from "~/hooks/useWalletContext"; import { MrgnTooltip } from "~/components/common"; import { @@ -20,19 +19,21 @@ import { } from "~/components/common/AssetList"; import { AssetCard } from "~/components/mobile/MobileAssetsList/AssetCard"; -import { LendingModes } from "~/types"; +import { LendingModes, UserMode } from "~/types"; +import { useWalletContext } from "~/hooks/useWalletContext"; +import { Portfolio } from "~/components/common/Portfolio"; export const MobileAssetsList = () => { - const { connected } = useWalletContext(); + const { connected, walletAddress } = useWalletContext(); - const [isStoreInitialized, extendedBankInfos, nativeSolBalance, selectedAccount] = useMrgnlendStore((state) => [ + const [isStoreInitialized, extendedBankInfos, nativeSolBalance] = useMrgnlendStore((state) => [ state.initialized, state.extendedBankInfos, state.nativeSolBalance, - state.selectedAccount, ]); - const [lendingMode, isFilteredUserPositions, sortOption, poolFilter] = useUiStore((state) => [ + const [userMode, lendingMode, isFilteredUserPositions, sortOption, poolFilter] = useUiStore((state) => [ + state.userMode, state.lendingMode, state.isFilteredUserPositions, state.sortOption, @@ -40,7 +41,6 @@ export const MobileAssetsList = () => { ]); const isInLendingMode = React.useMemo(() => lendingMode === LendingModes.LEND, [lendingMode]); - const inputRefs = React.useRef>({}); const [isLSTDialogOpen, setIsLSTDialogOpen] = React.useState(false); const [lstDialogVariant, setLSTDialogVariant] = React.useState(null); const [lstDialogCallback, setLSTDialogCallback] = React.useState<(() => void) | null>(null); @@ -94,128 +94,118 @@ export const MobileAssetsList = () => { return ( <> -
- {poolFilter !== "isolated" && ( -
- - Global pool - - {isStoreInitialized && globalBanks ? ( - globalBanks.length > 0 ? ( -
- {globalBanks.map((bank) => { - if (poolFilter === "stable" && !STABLECOINS.includes(bank.meta.tokenSymbol)) return null; - if (poolFilter === "lst" && !LSTS.includes(bank.meta.tokenSymbol)) return null; - - const activeBank = activeBankInfos.filter( - (activeBankInfo) => activeBankInfo.meta.tokenSymbol === bank.meta.tokenSymbol - ); - - return ( - void) => { - setLSTDialogVariant(variant); - setIsLSTDialogOpen(true); - if (onClose) { - setLSTDialogCallback(() => onClose); - } - }} - /> - ); - })} -
+ {userMode === UserMode.PRO && ( +
+ {poolFilter !== "isolated" && ( +
+ + Global pool + + {isStoreInitialized && globalBanks ? ( + globalBanks.length > 0 ? ( +
+ {globalBanks.map((bank) => { + if (poolFilter === "stable" && !STABLECOINS.includes(bank.meta.tokenSymbol)) return null; + if (poolFilter === "lst" && !LSTS.includes(bank.meta.tokenSymbol)) return null; + + const activeBank = activeBankInfos.filter( + (activeBankInfo) => activeBankInfo.meta.tokenSymbol === bank.meta.tokenSymbol + ); + + return ( + + ); + })} +
+ ) : ( + + No {isInLendingMode ? "lending" : "borrowing"} {isFilteredUserPositions ? "positions" : "pools"}{" "} + found. + + ) ) : ( - - No {isInLendingMode ? "lending" : "borrowing"} {isFilteredUserPositions ? "positions" : "pools"}{" "} - found. - - ) - ) : ( -
- {[...Array(6)].map((_, i) => ( - - ))} -
- )} -
- )} - {poolFilter !== "stable" && poolFilter !== "lst" && ( -
- - Isolated pools - - - Isolated pools are risky ⚠️ - - Assets in isolated pools cannot be used as collateral. When you borrow an isolated asset, you cannot - borrow other assets. Isolated pools should be considered particularly risky. As always, remember - that marginfi is a decentralized protocol and all deposited funds are at risk. - - } - placement="top" - > - info - - - - {isStoreInitialized && globalBanks ? ( - isolatedBanks.length > 0 ? ( -
- {isolatedBanks.map((bank, i) => { - const activeBank = activeBankInfos.filter( - (activeBankInfo) => activeBankInfo.meta.tokenSymbol === bank.meta.tokenSymbol - ); - - return ( - - ); - })} +
+ {[...Array(6)].map((_, i) => ( + + ))}
+ )} +
+ )} + {poolFilter !== "stable" && poolFilter !== "lst" && ( +
+ + Isolated pools + + + Isolated pools are risky ⚠️ + + Assets in isolated pools cannot be used as collateral. When you borrow an isolated asset, you + cannot borrow other assets. Isolated pools should be considered particularly risky. As always, + remember that marginfi is a decentralized protocol and all deposited funds are at risk. + + } + placement="top" + > + info + + + + {isStoreInitialized && globalBanks ? ( + isolatedBanks.length > 0 ? ( +
+ {isolatedBanks.map((bank, i) => { + const activeBank = activeBankInfos.filter( + (activeBankInfo) => activeBankInfo.meta.tokenSymbol === bank.meta.tokenSymbol + ); + + return ( + + ); + })} +
+ ) : ( + + No {isInLendingMode ? "lending" : "borrowing"} {isFilteredUserPositions ? "positions" : "pools"}{" "} + found. + + ) ) : ( - - No {isInLendingMode ? "lending" : "borrowing"} {isFilteredUserPositions ? "positions" : "pools"}{" "} - found. - - ) - ) : ( -
- {[...Array(6)].map((_, i) => ( - - ))} -
- )} -
- )} -
+
+ {[...Array(6)].map((_, i) => ( + + ))} +
+ )} +
+ )} +
+ )} + {walletAddress && } {
- + {/* */} {!connected ? null : currentFirebaseUser ? ( ) : hasUser === null ? ( @@ -167,8 +167,6 @@ const PortfolioPage = () => { nativeSolBalance={nativeSolBalance} bank={bank} isInLendingMode={true} - isConnected={connected} - marginfiAccount={selectedAccount} /> ))}
@@ -198,8 +196,6 @@ const PortfolioPage = () => { nativeSolBalance={nativeSolBalance} bank={bank} isInLendingMode={false} - isConnected={connected} - marginfiAccount={selectedAccount} /> ))}
diff --git a/packages/marginfi-client-v2/src/models/account/pure.ts b/packages/marginfi-client-v2/src/models/account/pure.ts index 16315cd88c..f23f794055 100644 --- a/packages/marginfi-client-v2/src/models/account/pure.ts +++ b/packages/marginfi-client-v2/src/models/account/pure.ts @@ -349,6 +349,43 @@ class MarginfiAccount { } } + /** + * Calculate the price at which the user position for the given bank and amount will lead to liquidation, all other prices constant. + */ + public computeLiquidationPriceForBankAmount( + banks: Map, + oraclePrices: Map, + bankAddress: PublicKey, + isLending: boolean, + amount: number, + ): number | null { + const bank = banks.get(bankAddress.toBase58()); + if (!bank) throw Error(`Bank ${bankAddress.toBase58()} not found`); + const priceInfo = oraclePrices.get(bankAddress.toBase58()); + if (!priceInfo) throw Error(`Price info for ${bankAddress.toBase58()} not found`); + + const balance = this.getBalance(bankAddress); + + if (!balance.active) return null; + + const { assets, liabilities } = this.computeHealthComponents(banks, oraclePrices, MarginRequirementType.Maintenance, [bankAddress]); + const amountBn = new BigNumber(amount) + + if (isLending) { + if (liabilities.eq(0)) return null; + + const assetWeight = bank.getAssetWeight(MarginRequirementType.Maintenance); + const priceConfidence = bank.getPrice(priceInfo, PriceBias.None).minus(bank.getPrice(priceInfo, PriceBias.Lowest)); + const liquidationPrice = liabilities.minus(assets).div(amountBn.times(assetWeight)).plus(priceConfidence); + return liquidationPrice.toNumber(); + } else { + const liabWeight = bank.getLiabilityWeight(MarginRequirementType.Maintenance); + const priceConfidence = bank.getPrice(priceInfo, PriceBias.Highest).minus(bank.getPrice(priceInfo, PriceBias.None)); + const liquidationPrice = assets.minus(liabilities).div(amountBn.times(liabWeight)).minus(priceConfidence); + return liquidationPrice.toNumber(); + } + } + // Calculate the max amount of collateral to liquidate to bring an account maint health to 0 (assuming negative health). // // The asset amount is bounded by 2 constraints, diff --git a/packages/marginfi-client-v2/src/models/account/wrapper.ts b/packages/marginfi-client-v2/src/models/account/wrapper.ts index 0251178524..3ec961fa9c 100644 --- a/packages/marginfi-client-v2/src/models/account/wrapper.ts +++ b/packages/marginfi-client-v2/src/models/account/wrapper.ts @@ -166,6 +166,15 @@ class MarginfiAccountWrapper { return this._marginfiAccount.computeLiquidationPriceForBank(this.client.banks, this.client.oraclePrices, bankAddress); } + public computeLiquidationPriceForBankAmount( + bankAddress: PublicKey, + isLending: boolean, + amount: number + ): number | null { + return this._marginfiAccount.computeLiquidationPriceForBankAmount(this.client.banks, this.client.oraclePrices, bankAddress, isLending, amount); + } + + public computeNetApy(): number { return this._marginfiAccount.computeNetApy(this.client.banks, this.client.oraclePrices); }