From c3109e7aed8586ad48ec5b46f365eeb23af278d2 Mon Sep 17 00:00:00 2001 From: Kobe Leenders Date: Fri, 8 Dec 2023 14:53:37 +0100 Subject: [PATCH] feat: improved action box --- .../components/common/ActionBox/ActionBox.tsx | 256 +++++++++++------- .../common/ActionBox/ActionBoxActions.tsx | 20 +- .../common/ActionBox/ActionBoxTokens.tsx | 4 +- .../common/AssetList/NewAsssetBanner.tsx | 74 ++++- .../desktop/AssetsList/AssetRow/AssetRow.tsx | 17 +- .../desktop/AssetsList/AssetsList.tsx | 1 + 6 files changed, 240 insertions(+), 132 deletions(-) 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 3bc6bd9da8..15a4201ee8 100644 --- a/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBox.tsx +++ b/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBox.tsx @@ -11,6 +11,7 @@ import { useWalletContext } from "~/hooks/useWalletContext"; import { MrgnLabeledSwitch } from "~/components/common/MrgnLabeledSwitch"; import { ActionBoxTokens } from "~/components/common/ActionBox/ActionBoxTokens"; +import { LSTDialog, LSTDialogVariants } from "~/components/common/AssetList"; import { Input } from "~/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"; @@ -40,6 +41,12 @@ export const ActionBox = () => { ); const { walletContextState } = useWalletContext(); + const [isLoading, setIsLoading] = React.useState(false); + const [isLSTDialogOpen, setIsLSTDialogOpen] = React.useState(false); + const [lstDialogVariant, setLSTDialogVariant] = React.useState(null); + const [hasLSTDialogShown, setHasLSTDialogShown] = React.useState([]); + const [lstDialogCallback, setLSTDialogCallback] = React.useState<(() => void) | null>(null); + const [preview, setPreview] = React.useState<{ key: string; value: string }[]>([]); const [amount, setAmount] = React.useState(null); const amountInputRef = React.useRef(null); @@ -63,7 +70,7 @@ export const ActionBox = () => { const isInputDisabled = React.useMemo(() => maxAmount === 0 && !showCloseBalance, [maxAmount, showCloseBalance]); const walletAmount = React.useMemo( () => - selectedToken?.info.state.mint.equals(WSOL_MINT) + selectedToken?.info.state.mint?.equals && selectedToken?.info.state.mint?.equals(WSOL_MINT) ? selectedToken?.userInfo.tokenAccount.balance + nativeSolBalance : selectedToken?.userInfo.tokenAccount.balance, [selectedToken] @@ -78,7 +85,12 @@ export const ActionBox = () => { React.useEffect(() => { setAmount(0); - }, [lendingMode, setAmount, selectedToken]); + if (lendingMode === LendingModes.LEND) { + setActionMode(ActionType.Deposit); + } else { + setActionMode(ActionType.Borrow); + } + }, [lendingMode, selectedToken, setAmount, setActionMode]); React.useEffect(() => { if (!selectedToken || !amount) { @@ -122,6 +134,7 @@ export const ActionBox = () => { marginfiAccount, walletContextState, }: MarginfiActionParams) => { + setIsLoading(true); await executeLendingAction({ mfiClient, actionType: currentAction, @@ -132,6 +145,7 @@ export const ActionBox = () => { walletContextState, }); + setIsLoading(false); setAmount(0); // -------- Refresh state @@ -168,20 +182,46 @@ export const ActionBox = () => { }, [selectedToken, selectedAccount, fetchMrgnlendState, setIsRefreshingStore]); const handleLendingAction = React.useCallback(async () => { - // TODO implement LST dialog if (!actionMode || !selectedToken || !selectedAccount || !amount) { return; } - await executeLendingActionCb({ - mfiClient, - actionType: actionMode, - bank: selectedToken, - amount: amount, - nativeSolBalance, - marginfiAccount: selectedAccount, - walletContextState, - }); + const action = async () => { + executeLendingActionCb({ + mfiClient, + actionType: actionMode, + bank: selectedToken, + amount: amount, + nativeSolBalance, + marginfiAccount: selectedAccount, + walletContextState, + }); + }; + + if ( + actionMode === ActionType.Deposit && + (selectedToken.meta.tokenSymbol === "SOL" || selectedToken.meta.tokenSymbol === "stSOL") && + !hasLSTDialogShown.includes(selectedToken.meta.tokenSymbol as LSTDialogVariants) + ) { + setHasLSTDialogShown((prev) => [...prev, selectedToken.meta.tokenSymbol as LSTDialogVariants]); + setLSTDialogVariant(selectedToken.meta.tokenSymbol); + setIsLSTDialogOpen(true); + setLSTDialogCallback(() => action); + + return; + } + + await action(); + + if ( + actionMode === ActionType.Withdraw && + (selectedToken.meta.tokenSymbol === "SOL" || selectedToken.meta.tokenSymbol === "stSOL") && + !hasLSTDialogShown.includes(selectedToken.meta.tokenSymbol as LSTDialogVariants) + ) { + setHasLSTDialogShown((prev) => [...prev, selectedToken.meta.tokenSymbol as LSTDialogVariants]); + setLSTDialogVariant(selectedToken.meta.tokenSymbol); + return; + } }, [ actionMode, selectedToken, @@ -194,101 +234,117 @@ export const ActionBox = () => { ]); return ( -
-
-
- { - setLendingMode(lendingMode === LendingModes.LEND ? LendingModes.BORROW : LendingModes.LEND); - }} - /> -
-

Supply. Earn interest. Borrow. Repeat.

-
-
-
- {hasActivePosition ? ( - { + setActionMode(value as ActionType); + }} > -
- -
- + } + > +
+ +
+
- {lendingMode === LendingModes.LEND ? ( - - You supply - You withdraw - - ) : ( - - You borrow - You repay - - )} - - ) : ( -

You {lendingMode === LendingModes.LEND ? "supply" : "borrow"}

- )} - {selectedToken && ( -
-
- -
- - {(walletAmount && walletAmount > 0.01 ? numeralFormatter(walletAmount) : "< 0.01").concat( - " ", - selectedToken?.meta.tokenSymbol + {lendingMode === LendingModes.LEND ? ( + + You supply + You withdraw + + ) : ( + + You borrow + You repay + )} - -
setAmount(maxAmount)} className="text-base font-bold cursor-pointer"> - MAX + + ) : ( +

You {lendingMode === LendingModes.LEND ? "supply" : "borrow"}

+ )} + {selectedToken && ( +
+
+ +
+ + {(walletAmount && walletAmount > 0.01 + ? numeralFormatter(walletAmount) + : walletAmount == 0 + ? "0" + : "< 0.01" + ).concat(" ", selectedToken?.meta.tokenSymbol)} + +
setAmount(maxAmount)} className="text-base font-bold cursor-pointer"> + MAX +
-
- )} -
-
- - setAmount(Number(e.target.value))} - placeholder="0" - className="bg-transparent w-full text-right outline-none focus-visible:outline-none focus-visible:ring-0 border-none text-3xl font-medium" + )} +
+
+ + setAmount(Number(e.target.value))} + placeholder="0" + className="bg-transparent w-full text-right outline-none focus-visible:outline-none focus-visible:ring-0 border-none text-3xl font-medium" + /> +
+ (showCloseBalance ? handleCloseBalance() : handleLendingAction())} + isLoading={isLoading} /> + {selectedToken !== null && amount !== null && preview.length > 0 && ( +
+ {preview.map((item) => ( + +
{item.key}
+
{item.value}
+
+ ))} +
+ )}
- (showCloseBalance ? handleCloseBalance() : handleLendingAction())} - /> - {selectedToken !== null && amount !== null && preview.length > 0 && ( -
- {preview.map((item) => ( - -
{item.key}
-
{item.value}
-
- ))} -
- )}
-
+ { + setIsLSTDialogOpen(false); + setLSTDialogVariant(null); + if (lstDialogCallback) { + lstDialogCallback(); + setLSTDialogCallback(null); + } + }} + /> + ); }; 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 a6b57396b7..e45344f546 100644 --- a/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxActions.tsx +++ b/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxActions.tsx @@ -5,28 +5,29 @@ import { ActionType } from "@mrgnlabs/marginfi-v2-ui-state"; import { useUiStore } from "~/store"; import { Button } from "~/components/ui/button"; +import { IconLoader } from "~/components/ui/icons"; type ActionBoxActionsProps = { - selectedMode?: ActionType; amount: number; maxAmount: number; showCloseBalance: boolean; + isLoading: boolean; handleAction: () => void; }; export const ActionBoxActions = ({ - selectedMode, amount, maxAmount, showCloseBalance, + isLoading, handleAction, }: ActionBoxActionsProps) => { - const [selectedToken] = useUiStore((state) => [state.selectedToken]); + const [actionMode, selectedToken] = useUiStore((state) => [state.actionMode, state.selectedToken]); const isActionDisabled = React.useMemo(() => { const isValidInput = amount > 0; - return (maxAmount === 0 || !isValidInput) && !showCloseBalance; - }, [amount, showCloseBalance, maxAmount]); + return ((maxAmount === 0 || !isValidInput) && !showCloseBalance) || isLoading; + }, [amount, showCloseBalance, maxAmount, isLoading]); const actionText = React.useMemo(() => { if (!selectedToken) { @@ -36,9 +37,10 @@ export const ActionBoxActions = ({ if (showCloseBalance) { return "Close account"; } + console.log({ actionMode }); if (maxAmount === 0) { - switch (selectedMode) { + switch (actionMode) { case ActionType.Deposit: return `Insufficient ${selectedToken.meta.tokenSymbol} in wallet`; case ActionType.Withdraw: @@ -56,12 +58,12 @@ export const ActionBoxActions = ({ return "Add an amount"; } - return selectedMode; - }, [selectedMode, amount, selectedToken, maxAmount, showCloseBalance]); + return actionMode; + }, [actionMode, amount, selectedToken, maxAmount, showCloseBalance]); return ( ); }; diff --git a/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxTokens.tsx b/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxTokens.tsx index c05179a481..f1a179465c 100644 --- a/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxTokens.tsx +++ b/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxTokens.tsx @@ -172,7 +172,6 @@ export const ActionBoxTokens = ({ currentToken, setCurrentToken }: ActionBoxToke (bankInfo) => bankInfo.address.toString().toLowerCase() === currentValue ) ?? null ); - setIsTokenPopoverOpen(false); }} className="cursor-pointer h-[60px] px-3 font-medium flex items-center justify-between gap-2 data-[selected=true]:bg-background-gray-light data-[selected=true]:text-white" @@ -200,6 +199,7 @@ export const ActionBoxTokens = ({ currentToken, setCurrentToken }: ActionBoxToke (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", @@ -229,6 +229,7 @@ export const ActionBoxTokens = ({ currentToken, setCurrentToken }: ActionBoxToke (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", @@ -258,6 +259,7 @@ export const ActionBoxTokens = ({ currentToken, setCurrentToken }: ActionBoxToke (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", diff --git a/apps/marginfi-v2-ui/src/components/common/AssetList/NewAsssetBanner.tsx b/apps/marginfi-v2-ui/src/components/common/AssetList/NewAsssetBanner.tsx index 9abaee1a13..25f0ad8d54 100644 --- a/apps/marginfi-v2-ui/src/components/common/AssetList/NewAsssetBanner.tsx +++ b/apps/marginfi-v2-ui/src/components/common/AssetList/NewAsssetBanner.tsx @@ -1,25 +1,36 @@ import React from "react"; import Image from "next/image"; -import { useUiStore } from "~/store"; +import { useMrgnlendStore, useUiStore } from "~/store"; +import { ActionBoxDialog } from "~/components/common/ActionBox"; import { Button } from "~/components/ui/button"; import { IconX } from "~/components/ui/icons"; -import { LendingModes, PoolTypes } from "~/types"; +import { LendingModes, PoolTypes, UserMode } from "~/types"; type NewAssetBannerProps = { asset: string; + symbol: string; image: string; }; -export const NewAssetBanner = ({ asset, image }: NewAssetBannerProps) => { - const [poolFilter, setLendingMode, setPoolFilter, setIsFilteredUserPositions] = useUiStore((state) => [ - state.poolFilter, - state.setLendingMode, - state.setPoolFilter, - state.setIsFilteredUserPositions, - ]); +export const NewAssetBanner = ({ asset, symbol, image }: NewAssetBannerProps) => { + const [userMode, poolFilter, setLendingMode, setPoolFilter, setIsFilteredUserPositions, setSelectedToken] = + useUiStore((state) => [ + state.userMode, + state.poolFilter, + state.setLendingMode, + state.setPoolFilter, + state.setIsFilteredUserPositions, + state.setSelectedToken, + ]); + const [extendedBankInfos] = useMrgnlendStore((state) => [state.extendedBankInfos]); + + const renderBank = React.useMemo( + () => extendedBankInfos.find((x) => x.meta.tokenSymbol === symbol), + [extendedBankInfos] + ); const [isHidden, setIsHidden] = React.useState(true); @@ -78,14 +89,46 @@ export const NewAssetBanner = ({ asset, image }: NewAssetBannerProps) => {

{assetTicker} is now available on margnfi

  • - + {userMode === UserMode.LITE ? ( + + + + ) : ( + + )}
  • - + {userMode === UserMode.LITE ? ( + + + + ) : ( + + )}
@@ -100,6 +143,7 @@ export const NewAssetBanner = ({ asset, image }: NewAssetBannerProps) => { type NewAssetBannerListProps = { assets: { asset: string; + symbol: string; image: string; }[]; }; 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 242565ebc6..7f1e05291f 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,9 +1,9 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from "react"; import clsx from "clsx"; import Image from "next/image"; -import { TableCell, TableRow, Tooltip, Typography } from "@mui/material"; -import { useMrgnlendStore, useUserProfileStore, useUiStore } from "~/store"; import Badge from "@mui/material/Badge"; +import { TableCell, TableRow, Tooltip, Typography } from "@mui/material"; + import { WSOL_MINT, numeralFormatter, percentFormatter, usdFormatter } from "@mrgnlabs/mrgn-common"; import { ExtendedBankInfo, @@ -13,16 +13,19 @@ import { ExtendedBankMetadata, } from "@mrgnlabs/marginfi-v2-ui-state"; import { MarginfiAccountWrapper, PriceBias } from "@mrgnlabs/marginfi-client-v2"; -import { MrgnTooltip } from "~/components/common/MrgnTooltip"; -import { AssetRowInputBox, AssetRowAction, LSTDialogVariants } from "~/components/common/AssetList"; -import { ActionBoxDialog } from "~/components/common/ActionBox"; + +import { useMrgnlendStore, useUserProfileStore, useUiStore } from "~/store"; +import { closeBalance, executeLendingAction, MarginfiActionParams, cn } from "~/utils"; +import { LendingModes } from "~/types"; import { useAssetItemData } from "~/hooks/useAssetItemData"; import { useWalletContext } from "~/hooks/useWalletContext"; import { useIsMobile } from "~/hooks/useIsMobile"; -import { closeBalance, executeLendingAction, MarginfiActionParams, cn } from "~/utils"; + +import { MrgnTooltip } from "~/components/common/MrgnTooltip"; +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 { LendingModes } from "~/types"; export const EMISSION_MINT_INFO_MAP = new Map([ [ 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 fad1c9af91..e7294ef3c3 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetsList.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetsList.tsx @@ -178,6 +178,7 @@ const AssetsList = () => {