From 1ab37e16d2a3ac67bf641bc3d069f487b102ae44 Mon Sep 17 00:00:00 2001
From: Kobe Leenders
Date: Thu, 7 Dec 2023 04:55:15 +0100
Subject: [PATCH 1/2] feat: improved action box
---
.../components/common/ActionBox/ActionBox.tsx | 202 +++++++++++-
.../common/ActionBox/ActionBoxActions.tsx | 67 ++++
.../common/ActionBox/ActionBoxTokens.tsx | 288 ++++++++++++------
.../src/components/ui/select.tsx | 10 +-
4 files changed, 456 insertions(+), 111 deletions(-)
create mode 100644 apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxActions.tsx
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 f8d2ec8cdf..befe03660b 100644
--- a/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBox.tsx
+++ b/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBox.tsx
@@ -1,27 +1,83 @@
import React from "react";
import { usdFormatter } from "@mrgnlabs/mrgn-common";
-import { useUiStore } from "~/store";
+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 { LendingModes } from "~/types";
+import { useWalletContext } from "~/hooks/useWalletContext";
import { MrgnLabeledSwitch } from "~/components/common/MrgnLabeledSwitch";
import { ActionBoxTokens } from "~/components/common/ActionBox/ActionBoxTokens";
import { Input } from "~/components/ui/input";
-import { Button } from "~/components/ui/button";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
+import { IconWallet } from "~/components/ui/icons";
-import { LendingModes } from "~/types";
+import { ActionBoxActions } from "./ActionBoxActions";
export const ActionBox = () => {
+ const [mfiClient, nativeSolBalance, setIsRefreshingStore, fetchMrgnlendState, selectedAccount] = useMrgnlendStore(
+ (state) => [
+ state.marginfiClient,
+ state.nativeSolBalance,
+ state.setIsRefreshingStore,
+ state.fetchMrgnlendState,
+ state.selectedAccount,
+ ]
+ );
const [lendingMode, setLendingMode, selectedToken, setSelectedToken] = useUiStore((state) => [
state.lendingMode,
state.setLendingMode,
state.selectedToken,
state.setSelectedToken,
]);
+ const { walletContextState } = useWalletContext();
+ const [selectedMode, setSelectMode] = React.useState();
const [preview, setPreview] = React.useState<{ key: string; value: string }[]>([]);
const [amount, setAmount] = React.useState(null);
const amountInputRef = React.useRef(null);
+ const isDust = React.useMemo(() => selectedToken?.isActive && selectedToken?.position.isDust, [selectedToken]);
+ const showCloseBalance = React.useMemo(() => selectedMode === ActionType.Withdraw && isDust, [selectedMode, isDust]);
+ const maxAmount = React.useMemo(() => {
+ switch (selectedMode) {
+ case ActionType.Deposit:
+ return selectedToken?.userInfo.maxDeposit;
+ case ActionType.Withdraw:
+ return selectedToken?.userInfo.maxWithdraw;
+ case ActionType.Borrow:
+ return selectedToken?.userInfo.maxBorrow;
+ case ActionType.Repay:
+ return selectedToken?.userInfo.maxRepay;
+ }
+ }, [selectedToken, selectedMode]);
+ const isInputDisabled = React.useMemo(() => maxAmount === 0 && !showCloseBalance, [maxAmount, showCloseBalance]);
+ const walletAmount = React.useMemo(
+ () =>
+ selectedToken?.info.state.mint.equals(WSOL_MINT)
+ ? selectedToken?.userInfo.tokenAccount.balance + nativeSolBalance
+ : selectedToken?.userInfo.tokenAccount.balance,
+ [selectedToken]
+ );
+ const hasActivePosition = React.useMemo(
+ () =>
+ selectedToken?.isActive &&
+ ((!selectedToken.isLending && lendingMode === LendingModes.LEND) ||
+ (selectedToken.isLending && lendingMode === LendingModes.BORROW)),
+ [selectedToken, lendingMode]
+ );
+
+ React.useEffect(() => {
+ if (lendingMode === LendingModes.LEND) {
+ setSelectMode(ActionType.Deposit);
+ } else if (lendingMode === LendingModes.BORROW) {
+ setSelectMode(ActionType.Borrow);
+ }
+ }, [lendingMode, setSelectMode, selectedToken]);
+
React.useEffect(() => {
if (!selectedToken || !amount) {
setPreview([]);
@@ -38,11 +94,11 @@ export const ActionBox = () => {
value: usdFormatter.format(amount),
},
{
- key: "Some property",
+ key: "Some propertya",
value: "--",
},
{
- key: "Some property",
+ key: "Some propertyb",
value: "--",
},
]);
@@ -50,9 +106,85 @@ export const ActionBox = () => {
React.useEffect(() => {
if (!selectedToken || !amountInputRef.current) return;
+ setAmount(0);
amountInputRef.current.focus();
}, [selectedToken]);
+ const executeLendingActionCb = React.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 handleCloseBalance = React.useCallback(async () => {
+ try {
+ await closeBalance({ marginfiAccount: selectedAccount, bank: selectedToken });
+ } catch (error) {
+ return;
+ }
+
+ setAmount(0);
+
+ try {
+ setIsRefreshingStore(true);
+ await fetchMrgnlendState();
+ } catch (error: any) {
+ console.log("Error while reloading state");
+ console.log(error);
+ }
+ }, [selectedToken, selectedAccount, fetchMrgnlendState, setIsRefreshingStore]);
+
+ const handleLendingAction = React.useCallback(async () => {
+ // TODO implement LST dialog
+
+ await executeLendingActionCb({
+ mfiClient,
+ actionType: selectedMode,
+ bank: selectedToken,
+ amount: amount,
+ nativeSolBalance,
+ marginfiAccount: selectedAccount,
+ walletContextState,
+ });
+ }, [
+ selectedMode,
+ selectedToken,
+ executeLendingActionCb,
+ mfiClient,
+ amount,
+ nativeSolBalance,
+ selectedAccount,
+ walletContextState,
+ ]);
+
return (
@@ -69,19 +201,75 @@ export const ActionBox = () => {
Supply. Earn interest. Borrow. Repeat.
-
You {lendingMode === LendingModes.LEND ? "supply" : "borrow"}
+
+ {hasActivePosition ? (
+
+ ) : (
+
You {lendingMode === LendingModes.LEND ? "supply" : "borrow"}
+ )}
+ {selectedToken && (
+
+
+
+
+
+ {(walletAmount > 0.01 ? numeralFormatter(walletAmount) : "< 0.01").concat(
+ " ",
+ selectedToken?.meta.tokenSymbol
+ )}
+
+
setAmount(maxAmount)} className="text-base font-bold cursor-pointer">
+ MAX
+
+
+ )}
+
-
+
(showCloseBalance ? handleCloseBalance() : handleLendingAction())}
+ />
{selectedToken !== null && amount !== null && preview.length > 0 && (
{preview.map((item) => (
diff --git a/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxActions.tsx b/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxActions.tsx
new file mode 100644
index 0000000000..ff19e707e1
--- /dev/null
+++ b/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxActions.tsx
@@ -0,0 +1,67 @@
+import React from "react";
+
+import { ActionType } from "@mrgnlabs/marginfi-v2-ui-state";
+
+import { useUiStore } from "~/store";
+
+import { Button } from "~/components/ui/button";
+
+type ActionBoxActionsProps = {
+ selectedMode: ActionType;
+ amount: number;
+ maxAmount: number;
+ showCloseBalance: boolean;
+ handleAction: () => void;
+};
+
+export const ActionBoxActions = ({
+ selectedMode,
+ amount,
+ maxAmount,
+ showCloseBalance,
+ handleAction,
+}: ActionBoxActionsProps) => {
+ const [selectedToken] = useUiStore((state) => [state.selectedToken]);
+
+ const isActionDisabled = React.useMemo(() => {
+ const isValidInput = amount > 0;
+ return (maxAmount === 0 || !isValidInput) && !showCloseBalance;
+ }, [amount, showCloseBalance, maxAmount]);
+
+ const actionText = React.useMemo(() => {
+ if (!selectedToken) {
+ return "Select token and amount";
+ }
+
+ if (showCloseBalance) {
+ return "Close account";
+ }
+
+ if (maxAmount === 0) {
+ switch (selectedMode) {
+ case ActionType.Deposit:
+ return `Insufficient ${selectedToken.meta.tokenSymbol} in wallet`;
+ case ActionType.Withdraw:
+ return "Nothing to withdraw";
+ case ActionType.Borrow:
+ return "Deposit a collateral first (lend)";
+ case ActionType.Repay:
+ return `Insufficient ${selectedToken.meta.tokenSymbol} in wallet for loan repayment`;
+ default:
+ return "Invalid action";
+ }
+ }
+
+ if (amount <= 0) {
+ return "Add an amount";
+ }
+
+ return selectedMode;
+ }, [selectedMode, 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 2f49077d7d..588dd56265 100644
--- a/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxTokens.tsx
+++ b/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxTokens.tsx
@@ -35,16 +35,42 @@ export const ActionBoxTokens = ({ currentToken, setCurrentToken }: ActionBoxToke
const [searchQuery, setSearchQuery] = React.useState("");
const [isTokenPopoverOpen, setIsTokenPopoverOpen] = React.useState(false);
+ const calculateRate = React.useCallback(
+ (bank: ExtendedBankInfo) =>
+ percentFormatter.format(
+ (lendingMode === LendingModes.LEND ? bank.info.state.lendingRate : bank.info.state.borrowingRate) +
+ (lendingMode === LendingModes.LEND && bank.info.state.emissions == Emissions.Lending
+ ? bank.info.state.emissionsRate
+ : 0) +
+ (lendingMode !== LendingModes.LEND && bank.info.state.emissions == Emissions.Borrowing
+ ? bank.info.state.emissionsRate
+ : 0)
+ ),
+ [lendingMode]
+ );
+
const filteredBanks = React.useMemo(() => {
const lowerCaseSearchQuery = searchQuery.toLowerCase();
return extendedBankInfos
- .filter((bankInfo) => {
- return bankInfo.meta.tokenSymbol.toLowerCase().includes(lowerCaseSearchQuery);
- })
- .filter((bankInfo) => {
- return lendingMode === LendingModes.LEND ? bankInfo.userInfo.tokenAccount.balance === 0 : true;
- });
+ .filter((bankInfo) => bankInfo.meta.tokenSymbol.toLowerCase().includes(lowerCaseSearchQuery))
+ .filter((bankInfo) => (lendingMode === LendingModes.LEND ? bankInfo.userInfo.tokenAccount.balance === 0 : true));
+ }, [extendedBankInfos, searchQuery]);
+
+ const filteredBanksActiveLending = React.useMemo(() => {
+ const lowerCaseSearchQuery = searchQuery.toLowerCase();
+ return extendedBankInfos
+ .filter((bankInfo) => bankInfo.meta.tokenSymbol.toLowerCase().includes(lowerCaseSearchQuery))
+ .filter((bankInfo) => bankInfo.isActive && bankInfo.position?.isLending)
+ .sort((a, b) => b.userInfo.position?.amount - a.userInfo.position?.amount);
+ }, [extendedBankInfos, searchQuery]);
+
+ const filteredBanksActiveBorrowing = React.useMemo(() => {
+ const lowerCaseSearchQuery = searchQuery.toLowerCase();
+ return extendedBankInfos
+ .filter((bankInfo) => bankInfo.meta.tokenSymbol.toLowerCase().includes(lowerCaseSearchQuery))
+ .filter((bankInfo) => bankInfo.isActive && !bankInfo.position?.isLending)
+ .sort((a, b) => b.userInfo.position?.amount - a.userInfo.position?.amount);
}, [extendedBankInfos, searchQuery]);
const filteredBanksUserOwns = React.useMemo(() => {
@@ -59,7 +85,16 @@ export const ActionBoxTokens = ({ currentToken, setCurrentToken }: ActionBoxToke
return balance > 0 && bankInfo.meta.tokenSymbol.toLowerCase().includes(lowerCaseSearchQuery);
})
.sort((a, b) => {
- return b.userInfo.tokenAccount.balance - a.userInfo.tokenAccount.balance;
+ const isFirstWSOL = a.info.state.mint?.equals ? a.info.state.mint.equals(WSOL_MINT) : false;
+ const isSecondWSOL = b.info.state.mint?.equals ? b.info.state.mint.equals(WSOL_MINT) : false;
+ const firstBalance =
+ (isFirstWSOL ? a.userInfo.tokenAccount.balance + nativeSolBalance : a.userInfo.tokenAccount.balance) *
+ a.info.state.price;
+ const secondBalance =
+ (isSecondWSOL ? b.userInfo.tokenAccount.balance + nativeSolBalance : b.userInfo.tokenAccount.balance) *
+ b.info.state.price;
+
+ return secondBalance - firstBalance;
});
}, [extendedBankInfos, searchQuery]);
@@ -74,7 +109,7 @@ export const ActionBoxTokens = ({ currentToken, setCurrentToken }: ActionBoxToke
- {(walletAmount > 0.01 ? numeralFormatter(walletAmount) : "< 0.01").concat(
+ {(walletAmount && walletAmount > 0.01 ? numeralFormatter(walletAmount) : "< 0.01").concat(
" ",
selectedToken?.meta.tokenSymbol
)}
@@ -265,9 +274,9 @@ export const ActionBox = () => {
(showCloseBalance ? handleCloseBalance() : handleLendingAction())}
/>
{selectedToken !== null && amount !== null && preview.length > 0 && (
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 ff19e707e1..a6b57396b7 100644
--- a/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxActions.tsx
+++ b/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxActions.tsx
@@ -7,7 +7,7 @@ import { useUiStore } from "~/store";
import { Button } from "~/components/ui/button";
type ActionBoxActionsProps = {
- selectedMode: ActionType;
+ selectedMode?: ActionType;
amount: number;
maxAmount: number;
showCloseBalance: boolean;
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 588dd56265..c05179a481 100644
--- a/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxTokens.tsx
+++ b/apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBoxTokens.tsx
@@ -27,11 +27,7 @@ export const ActionBoxTokens = ({ currentToken, setCurrentToken }: ActionBoxToke
state.extendedBankInfos,
state.nativeSolBalance,
]);
- const [lendingMode, selectedToken, setSelectedToken] = useUiStore((state) => [
- state.lendingMode,
- state.selectedToken,
- state.setSelectedToken,
- ]);
+ const [lendingMode] = useUiStore((state) => [state.lendingMode]);
const [searchQuery, setSearchQuery] = React.useState("");
const [isTokenPopoverOpen, setIsTokenPopoverOpen] = React.useState(false);
@@ -62,7 +58,7 @@ export const ActionBoxTokens = ({ currentToken, setCurrentToken }: ActionBoxToke
return extendedBankInfos
.filter((bankInfo) => bankInfo.meta.tokenSymbol.toLowerCase().includes(lowerCaseSearchQuery))
.filter((bankInfo) => bankInfo.isActive && bankInfo.position?.isLending)
- .sort((a, b) => b.userInfo.position?.amount - a.userInfo.position?.amount);
+ .sort((a, b) => (b.isActive ? b?.position?.amount : 0) - (a.isActive ? a?.position?.amount : 0));
}, [extendedBankInfos, searchQuery]);
const filteredBanksActiveBorrowing = React.useMemo(() => {
@@ -70,7 +66,7 @@ export const ActionBoxTokens = ({ currentToken, setCurrentToken }: ActionBoxToke
return extendedBankInfos
.filter((bankInfo) => bankInfo.meta.tokenSymbol.toLowerCase().includes(lowerCaseSearchQuery))
.filter((bankInfo) => bankInfo.isActive && !bankInfo.position?.isLending)
- .sort((a, b) => b.userInfo.position?.amount - a.userInfo.position?.amount);
+ .sort((a, b) => (b.isActive ? b?.position?.amount : 0) - (a.isActive ? a?.position?.amount : 0));
}, [extendedBankInfos, searchQuery]);
const filteredBanksUserOwns = React.useMemo(() => {
@@ -131,7 +127,7 @@ export const ActionBoxTokens = ({ currentToken, setCurrentToken }: ActionBoxToke
lendingMode === LendingModes.BORROW && "text-error"
)}
>
- {calculateRate(selectedToken) + ` ${lendingMode === LendingModes.LEND ? "APY" : "APR"}`}
+ {calculateRate(currentToken) + ` ${lendingMode === LendingModes.LEND ? "APY" : "APR"}`}
>
@@ -150,9 +146,9 @@ export const ActionBoxTokens = ({ currentToken, setCurrentToken }: ActionBoxToke
- setSelectedToken(extendedBankInfos.find((bank) => bank.address.toString() === value) || selectedToken)
+ setCurrentToken(extendedBankInfos.find((bank) => bank.address.toString() === value) || currentToken)
}
>