From fe64f1a14254edc5ba521331e1dd3a7e9412e0cc Mon Sep 17 00:00:00 2001 From: man0s <95379755+losman0s@users.noreply.github.com> Date: Thu, 23 Nov 2023 19:06:56 +0800 Subject: [PATCH 01/14] feat: add liquidation price to position data --- .../src/models/account/pure.ts | 44 ++++++++++++++++++- .../src/models/account/wrapper.ts | 6 +++ .../marginfi-v2-ui-state/src/lib/mrgnlend.ts | 29 ++++++------ 3 files changed, 64 insertions(+), 15 deletions(-) diff --git a/packages/marginfi-client-v2/src/models/account/pure.ts b/packages/marginfi-client-v2/src/models/account/pure.ts index dd0d41566f..16315cd88c 100644 --- a/packages/marginfi-client-v2/src/models/account/pure.ts +++ b/packages/marginfi-client-v2/src/models/account/pure.ts @@ -81,12 +81,16 @@ class MarginfiAccount { computeHealthComponents( banks: Map, oraclePrices: Map, - marginReqType: MarginRequirementType + marginReqType: MarginRequirementType, + excludedBanks: PublicKey[] = [] ): { assets: BigNumber; liabilities: BigNumber; } { - const [assets, liabilities] = this.activeBalances + const filteredBalances = this.activeBalances.filter( + (accountBalance) => !excludedBanks.find(b => b.equals(accountBalance.bankPk)) + ); + const [assets, liabilities] = filteredBalances .map((accountBalance) => { const bank = banks.get(accountBalance.bankPk.toBase58()); if (!bank) throw Error(`Bank ${shortenAddress(accountBalance.bankPk)} not found`); @@ -309,6 +313,42 @@ class MarginfiAccount { return maxWithdraw; } + /** + * Calculate the price at which the user position for the given bank will lead to liquidation, all other prices constant. + */ + public computeLiquidationPriceForBank( + banks: Map, + oraclePrices: Map, + bankAddress: PublicKey, + ): 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 isLending = balance.liabilityShares.isZero(); + const { assets, liabilities } = this.computeHealthComponents(banks, oraclePrices, MarginRequirementType.Maintenance, [bankAddress]); + const { assets: assetQuantityUi, liabilities: liabQuantitiesUi } = balance.computeQuantityUi(bank); + + 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(assetQuantityUi.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(liabQuantitiesUi.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 135c09686f..0251178524 100644 --- a/packages/marginfi-client-v2/src/models/account/wrapper.ts +++ b/packages/marginfi-client-v2/src/models/account/wrapper.ts @@ -160,6 +160,12 @@ class MarginfiAccountWrapper { ); } + public computeLiquidationPriceForBank( + bankAddress: PublicKey, + ): number | null { + return this._marginfiAccount.computeLiquidationPriceForBank(this.client.banks, this.client.oraclePrices, bankAddress); + } + public computeNetApy(): number { return this._marginfiAccount.computeNetApy(this.client.banks, this.client.oraclePrices); } diff --git a/packages/marginfi-v2-ui-state/src/lib/mrgnlend.ts b/packages/marginfi-v2-ui-state/src/lib/mrgnlend.ts index 9ec23192d9..83cd083f75 100644 --- a/packages/marginfi-v2-ui-state/src/lib/mrgnlend.ts +++ b/packages/marginfi-v2-ui-state/src/lib/mrgnlend.ts @@ -283,19 +283,18 @@ function makeExtendedBankInfo( } // Calculate user-specific info relevant to their active position in this bank - const position = makeLendingPosition(positionRaw, bank, bankInfo, oraclePrice); - const maxWithdraw = userData.marginfiAccount - ? floor( - Math.min( - userData.marginfiAccount - .computeMaxWithdrawForBank(bank.address, { volatilityFactor: VOLATILITY_FACTOR }) - .toNumber(), - bankInfo.availableLiquidity - ), - bankInfo.mintDecimals - ) - : 0; + const marginfiAccount = userData.marginfiAccount as MarginfiAccountWrapper; + + const position = makeLendingPosition(positionRaw, bank, bankInfo, oraclePrice, marginfiAccount); + + const maxWithdraw = floor( + Math.min( + marginfiAccount.computeMaxWithdrawForBank(bank.address, { volatilityFactor: VOLATILITY_FACTOR }).toNumber(), + bankInfo.availableLiquidity + ), + bankInfo.mintDecimals + ); let maxRepay = 0; if (position) { @@ -325,7 +324,8 @@ function makeLendingPosition( balance: Balance, bank: Bank, bankInfo: BankState, - oraclePrice: OraclePrice + oraclePrice: OraclePrice, + marginfiAccount: MarginfiAccountWrapper ): LendingPosition { const amounts = balance.computeQuantity(bank); const usdValues = balance.computeUsdValue(bank, oraclePrice, MarginRequirementType.Equity); @@ -338,11 +338,13 @@ function makeLendingPosition( const isDust = uiToNative(amount, bankInfo.mintDecimals).isZero(); const weightedUSDValue = isLending ? weightedUSDValues.assets.toNumber() : weightedUSDValues.liabilities.toNumber(); const usdValue = isLending ? usdValues.assets.toNumber() : usdValues.liabilities.toNumber(); + const liquidationPrice = marginfiAccount.computeLiquidationPriceForBank(bank.address); return { amount, usdValue, weightedUSDValue, + liquidationPrice, isLending, isDust, }; @@ -501,6 +503,7 @@ interface LendingPosition { amount: number; usdValue: number; weightedUSDValue: number; + liquidationPrice: number | null; isDust: boolean; } From b20e346a02e8a918deeabcfc173ebbf3be392beb Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Fri, 24 Nov 2023 19:51:47 -0500 Subject: [PATCH 02/14] chore: fetch and log user positions in asset list --- .../desktop/AssetsList/AssetsList.tsx | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) 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 d54c8debb2..a2d978928a 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetsList.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetsList.tsx @@ -1,15 +1,18 @@ -import React, { FC, useEffect, useRef, useState } from "react"; +import React from "react"; + import dynamic from "next/dynamic"; import Image from "next/image"; + import { useHotkeys } from "react-hotkeys-hook"; -import { ExtendedBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; import { Card, Table, TableHead, TableBody, TableContainer, TableCell, TableRow } from "@mui/material"; import Typography from "@mui/material/Typography"; +import { ExtendedBankInfo, ActiveBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; + import { useMrgnlendStore, useUserProfileStore, useUiStore } from "~/store"; import { useWalletContext } from "~/hooks/useWalletContext"; -import { LoadingAsset, AssetRow } from "./AssetRow"; +import { LoadingAsset, AssetRow } from "~/components/desktop/AssetsList/AssetRow"; import { LSTDialog, LSTDialogVariants, @@ -27,7 +30,7 @@ const UserPositions = dynamic(async () => (await import("~/components/desktop/Us ssr: false, }); -const AssetsList: FC = () => { +const AssetsList = () => { // const { selectedAccount, nativeSolBalance } = useStore(); const { connected, walletAddress } = useWalletContext(); const [isStoreInitialized, extendedBankInfos, nativeSolBalance, selectedAccount] = useMrgnlendStore((state) => [ @@ -50,11 +53,11 @@ const AssetsList: FC = () => { state.setSortOption, ]); - const inputRefs = useRef>({}); - const [isHotkeyMode, setIsHotkeyMode] = useState(false); - const [isLSTDialogOpen, setIsLSTDialogOpen] = useState(false); - const [lstDialogVariant, setLSTDialogVariant] = useState(null); - const [lstDialogCallback, setLSTDialogCallback] = useState<(() => void) | null>(null); + const inputRefs = React.useRef>({}); + const [isHotkeyMode, setIsHotkeyMode] = React.useState(false); + const [isLSTDialogOpen, setIsLSTDialogOpen] = React.useState(false); + const [lstDialogVariant, setLSTDialogVariant] = React.useState(null); + const [lstDialogCallback, setLSTDialogCallback] = React.useState<(() => void) | null>(null); const isInLendingMode = React.useMemo(() => lendingMode === LendingModes.LEND, [lendingMode]); @@ -99,6 +102,22 @@ const AssetsList: FC = () => { } }, [isStoreInitialized, extendedBankInfos, sortOption, isFilteredUserPositions, sortBanks]); + const activeBankInfos = React.useMemo( + () => extendedBankInfos.filter((balance) => balance.isActive), + [extendedBankInfos] + ) as ActiveBankInfo[]; + + const { lendPositions, borrowPositions } = React.useMemo( + () => ({ + lendPositions: activeBankInfos.filter((bankInfo) => bankInfo.position.isLending), + borrowPositions: activeBankInfos.filter((bankInfo) => !bankInfo.position.isLending), + }), + [activeBankInfos] + ); + + console.log("lendPositions", lendPositions); + console.log("borrowPositions", borrowPositions); + // Enter hotkey mode useHotkeys( "meta + k", @@ -154,8 +173,8 @@ const AssetsList: FC = () => { ); // Hack required to circumvent rehydration error - const [hasMounted, setHasMounted] = useState(false); - useEffect(() => { + const [hasMounted, setHasMounted] = React.useState(false); + React.useEffect(() => { setHasMounted(true); }, []); if (!hasMounted) { From 6de3183582c9e1cafb9a57944dd1da5315f885a9 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Fri, 24 Nov 2023 20:38:45 -0500 Subject: [PATCH 03/14] feat: conditionally render position row and print out user position details (wip) --- .../desktop/AssetsList/AssetRow/AssetRow.tsx | 531 ++++++++++-------- .../desktop/AssetsList/AssetsList.tsx | 11 +- apps/marginfi-v2-ui/src/styles/globals.css | 2 +- 3 files changed, 297 insertions(+), 247 deletions(-) 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 37992eef1c..87096c9944 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 @@ -5,7 +5,13 @@ import { TableCell, TableRow, Tooltip, Typography } from "@mui/material"; import { useMrgnlendStore, useUserProfileStore } from "~/store"; import Badge from "@mui/material/Badge"; import { WSOL_MINT, numeralFormatter, percentFormatter, usdFormatter } from "@mrgnlabs/mrgn-common"; -import { ExtendedBankInfo, ActionType, getCurrentAction, ExtendedBankMetadata } from "@mrgnlabs/marginfi-v2-ui-state"; +import { + ExtendedBankInfo, + ActiveBankInfo, + ActionType, + getCurrentAction, + 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"; @@ -40,6 +46,7 @@ const AssetRow: FC<{ hasHotkey: boolean; showHotkeyBadges?: boolean; badgeContent?: string; + userPosition?: ActiveBankInfo; showLSTDialog?: (variant: LSTDialogVariants, callback?: () => void) => void; }> = ({ bank, @@ -50,6 +57,7 @@ const AssetRow: FC<{ hasHotkey, showHotkeyBadges, badgeContent, + userPosition, showLSTDialog, }) => { const [lendZoomLevel, denominationUSD] = useUserProfileStore((state) => [state.lendZoomLevel, state.denominationUSD]); @@ -207,275 +215,310 @@ const AssetRow: FC<{ ]); return ( - - -
- {bank.meta.tokenLogoUri && ( - {bank.meta.tokenSymbol} - )} -
{bank.meta.tokenSymbol}
-
-
- - - - -
- {bank.info.state.emissionsRate > 0 && - EMISSION_MINT_INFO_MAP.get(bank.meta.tokenSymbol) !== undefined && - isInLendingMode && ( -
- - - Liquidity rewards - - {`${percentFormatter.format(bank.info.state.lendingRate)} Supply APY + ${percentFormatter.format( - bank.info.state.emissionsRate - )} ${EMISSION_MINT_INFO_MAP.get(bank.meta.tokenSymbol)!.tokenSymbol} rewards.`} -
- - Learn more. - - - } - placement="left" - > - info -
-
+
+ {bank.meta.tokenLogoUri && ( + {bank.meta.tokenSymbol} )} -
- {rateAP} +
{bank.meta.tokenSymbol}
-
- - - - {assetWeight} - + - + assetPrice * 0.1 ? "⚠️" : ""} + className="bg-transparent" + sx={{ + "& .MuiBadge-badge": { + fontSize: 20, + }, + }} + invisible={assetPriceOffset > assetPrice * 0.1 ? false : true} + > + {assetPrice >= 0.01 + ? lendZoomLevel < 2 + ? `${ + assetPrice > 9999 ? numeralFormatter(assetPrice) : usdFormatter.format(assetPrice) + } ± ${assetPriceOffset.toFixed(2)}` + : usdFormatter.format(assetPrice) + : `$${assetPrice.toExponential(2)}`} + + + - {/*******************************/} - {/* [START]: ZOOM-BASED COLUMNS */} - {/*******************************/} + +
+ {bank.info.state.emissionsRate > 0 && + EMISSION_MINT_INFO_MAP.get(bank.meta.tokenSymbol) !== undefined && + isInLendingMode && ( +
+ + + Liquidity rewards + + {`${percentFormatter.format( + bank.info.state.lendingRate + )} Supply APY + ${percentFormatter.format(bank.info.state.emissionsRate)} ${ + EMISSION_MINT_INFO_MAP.get(bank.meta.tokenSymbol)!.tokenSymbol + } rewards.`} +
+ + Learn more. + + + } + placement="left" + > + info +
+
+ )} +
+ {rateAP} +
+
+
- {lendZoomLevel < 2 && ( - {denominationUSD ? usdFormatter.format(bankCap * bank.info.state.price) : numeralFormatter(bankCap)} + {assetWeight} - )} - {lendZoomLevel < 3 && ( - )} - {/*******************************/} - {/* [END]: ZOOM-BASED COLUMNS */} - {/*******************************/} + {/*******************************/} + {/* [START]: ZOOM-BASED COLUMNS */} + {/*******************************/} - - {denominationUSD - ? usdFormatter.format( - (bank.info.state.mint.equals(WSOL_MINT) - ? bank.userInfo.tokenAccount.balance + nativeSolBalance - : bank.userInfo.tokenAccount.balance) * bank.info.state.price - ) - : numeralFormatter( - bank.info.state.mint.equals(WSOL_MINT) - ? bank.userInfo.tokenAccount.balance + nativeSolBalance - : bank.userInfo.tokenAccount.balance - )} - + {lendZoomLevel < 2 && ( + + {denominationUSD ? usdFormatter.format(bankCap * bank.info.state.price) : numeralFormatter(bankCap)} + + )} - - + {percentFormatter.format(bank.info.state.utilizationRate / 100)} + + )} + + {/*******************************/} + {/* [END]: ZOOM-BASED COLUMNS */} + {/*******************************/} + + - - - + {denominationUSD + ? usdFormatter.format( + (bank.info.state.mint.equals(WSOL_MINT) + ? bank.userInfo.tokenAccount.balance + nativeSolBalance + : bank.userInfo.tokenAccount.balance) * bank.info.state.price + ) + : numeralFormatter( + bank.info.state.mint.equals(WSOL_MINT) + ? bank.userInfo.tokenAccount.balance + nativeSolBalance + : bank.userInfo.tokenAccount.balance + )} + - - + + + + + + + +
+ + {showCloseBalance ? "Close" : currentAction} + +
+
+
+ + {userPosition && ( + -
- - {showCloseBalance ? "Close" : currentAction} - -
- - -
+ +
+

Your position details

+
+
{userPosition.position.isLending ? "Lending" : "Borrowing"}
+
{userPosition.position.amount}
+
USD Value
+
{userPosition.position.usdValue}
+ {userPosition.position.liquidationPrice && userPosition.position.liquidationPrice > 0 && ( + <> +
Liq price
+
{userPosition.position.liquidationPrice}
+ + )} +
+
+
+ + )} + + ); }; 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 a2d978928a..0f0d9caec2 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetsList.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetsList.tsx @@ -195,7 +195,7 @@ const AssetsList = () => { className="table-fixed" style={{ borderCollapse: "separate", - borderSpacing: "0px 8px", + borderSpacing: "0px 0px", }} > @@ -386,6 +386,12 @@ const AssetsList = () => { .map((bank, i) => { if (poolFilter === "stable" && !STABLECOINS.includes(bank.meta.tokenSymbol)) return null; if (poolFilter === "lst" && !LSTS.includes(bank.meta.tokenSymbol)) return null; + + // check to see if bank is in open positions + const userPosition = activeBankInfos.filter( + (activeBankInfo) => activeBankInfo.meta.tokenSymbol === bank.meta.tokenSymbol + ); + return isStoreInitialized ? ( { hasHotkey={true} showHotkeyBadges={showBadges} badgeContent={`${i + 1}`} + userPosition={userPosition.length > 0 ? userPosition[0] : undefined} showLSTDialog={(variant: LSTDialogVariants, onClose?: () => void) => { setLSTDialogVariant(variant); setIsLSTDialogOpen(true); @@ -418,7 +425,7 @@ const AssetsList = () => { )} {poolFilter !== "stable" && poolFilter !== "lst" && ( - +
diff --git a/apps/marginfi-v2-ui/src/styles/globals.css b/apps/marginfi-v2-ui/src/styles/globals.css index 72feecca31..7642522b12 100644 --- a/apps/marginfi-v2-ui/src/styles/globals.css +++ b/apps/marginfi-v2-ui/src/styles/globals.css @@ -27,7 +27,7 @@ --muted-highlight: 203 15% 15%; --muted-foreground: 240 5% 64.9%; - --accent: 240 4% 16%; + --accent: 210 10% 15%; --accent-highlight: 240 4% 21%; --accent-foreground: 0 0% 98%; From f064434d2eb6b555a1a3b7fc9325f64a260096a4 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Fri, 24 Nov 2023 20:54:16 -0500 Subject: [PATCH 04/14] feat: style inline user positions --- .../desktop/AssetsList/AssetRow/AssetRow.tsx | 30 ++++++++++++++----- apps/marginfi-v2-ui/src/styles/globals.css | 2 +- 2 files changed, 23 insertions(+), 9 deletions(-) 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 87096c9944..c52448999d 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 @@ -17,7 +17,7 @@ import { MrgnTooltip } from "~/components/common/MrgnTooltip"; import { AssetRowInputBox, AssetRowAction, LSTDialogVariants } from "~/components/common/AssetList"; import { useAssetItemData } from "~/hooks/useAssetItemData"; import { useWalletContext } from "~/hooks/useWalletContext"; -import { closeBalance, executeLendingAction, MarginfiActionParams } from "~/utils"; +import { closeBalance, executeLendingAction, MarginfiActionParams, cn } from "~/utils"; export const EMISSION_MINT_INFO_MAP = new Map([ [ @@ -501,15 +501,29 @@ const AssetRow: FC<{ >

Your position details

-
-
{userPosition.position.isLending ? "Lending" : "Borrowing"}
-
{userPosition.position.amount}
-
USD Value
-
{userPosition.position.usdValue}
+
+
{userPosition.position.isLending ? "Lending" : "Borrowing"}
+
+ {userPosition.position.amount < 0.01 && "< "} + {numeralFormatter(userPosition.position.amount)} +
+
USD Value
+
0 && + "pr-4 border-accent-foreground/50 border-r" + )} + > + {usdFormatter.format(userPosition.position.usdValue)} +
{userPosition.position.liquidationPrice && userPosition.position.liquidationPrice > 0 && ( <> -
Liq price
-
{userPosition.position.liquidationPrice}
+
Liq price
+
+ {usdFormatter.format(userPosition.position.liquidationPrice)} +
)}
diff --git a/apps/marginfi-v2-ui/src/styles/globals.css b/apps/marginfi-v2-ui/src/styles/globals.css index 7642522b12..9ce4222bb2 100644 --- a/apps/marginfi-v2-ui/src/styles/globals.css +++ b/apps/marginfi-v2-ui/src/styles/globals.css @@ -29,7 +29,7 @@ --accent: 210 10% 15%; --accent-highlight: 240 4% 21%; - --accent-foreground: 0 0% 98%; + --accent-foreground: 213 5% 50%; --destructive: #e06d6f1a; --destructive-foreground: 359 65% 65%; From ca446213d247596df9a78d8e9e657ee7360a6052 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Fri, 24 Nov 2023 20:56:55 -0500 Subject: [PATCH 05/14] chore: override text focus color in select ui component --- .../src/components/ui/select.tsx | 59 +++++++------------ 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/apps/marginfi-v2-ui/src/components/ui/select.tsx b/apps/marginfi-v2-ui/src/components/ui/select.tsx index 3165147fc5..2ed74db199 100644 --- a/apps/marginfi-v2-ui/src/components/ui/select.tsx +++ b/apps/marginfi-v2-ui/src/components/ui/select.tsx @@ -1,14 +1,14 @@ -import * as React from "react" -import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons" -import * as SelectPrimitive from "@radix-ui/react-select" +import * as React from "react"; +import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; +import * as SelectPrimitive from "@radix-ui/react-select"; -import { cn } from "~/utils/themeUtils" +import { cn } from "~/utils/themeUtils"; -const Select = SelectPrimitive.Root +const Select = SelectPrimitive.Root; -const SelectGroup = SelectPrimitive.Group +const SelectGroup = SelectPrimitive.Group; -const SelectValue = SelectPrimitive.Value +const SelectValue = SelectPrimitive.Value; const SelectTrigger = React.forwardRef< React.ElementRef, @@ -27,8 +27,8 @@ const SelectTrigger = React.forwardRef< -)) -SelectTrigger.displayName = SelectPrimitive.Trigger.displayName +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; const SelectContent = React.forwardRef< React.ElementRef, @@ -57,20 +57,16 @@ const SelectContent = React.forwardRef< -)) -SelectContent.displayName = SelectPrimitive.Content.displayName +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; const SelectLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -SelectLabel.displayName = SelectPrimitive.Label.displayName + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; const SelectItem = React.forwardRef< React.ElementRef, @@ -79,7 +75,7 @@ const SelectItem = React.forwardRef< {children} -)) -SelectItem.displayName = SelectPrimitive.Item.displayName +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; const SelectSeparator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; -export { - Select, - SelectGroup, - SelectValue, - SelectTrigger, - SelectContent, - SelectLabel, - SelectItem, - SelectSeparator, -} +export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator }; From e0f01f35337e3521dd91e988614adfbea30e8fd7 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Fri, 24 Nov 2023 21:44:54 -0500 Subject: [PATCH 06/14] feat: add token symbol to inline position / add liq price back to lending pos --- .../desktop/AssetsList/AssetRow/AssetRow.tsx | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) 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 c52448999d..dffe7b694a 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 @@ -505,7 +505,7 @@ const AssetRow: FC<{
{userPosition.position.isLending ? "Lending" : "Borrowing"}
{userPosition.position.amount < 0.01 && "< "} - {numeralFormatter(userPosition.position.amount)} + {numeralFormatter(userPosition.position.amount) + " " + bank.meta.tokenSymbol}
USD Value
( - - -
- {bankMetadata.tokenLogoUri && ( - - )} -
{bankMetadata.tokenSymbol}
-
-
- - - - - - - - - - - - - {}} disabled={true} /> - - -
- {isInLendingMode ? "Supply" : "Borrow"} -
-
-
+ <> + + +
+ {bankMetadata.tokenLogoUri && ( + + )} +
{bankMetadata.tokenSymbol}
+
+
+ + - + - + - + - + - + + {}} disabled={true} /> + + +
+ {isInLendingMode ? "Supply" : "Borrow"} +
+
+
+ + ); export { AssetRow, LoadingAsset }; From ce6f174c41c49c0e964434ae9beb502b0f5da25d Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Fri, 24 Nov 2023 21:48:10 -0500 Subject: [PATCH 07/14] feat: remove user positions and update user positions filter --- .../desktop/AssetsList/AssetRow/AssetRow.tsx | 1 + .../desktop/AssetsList/AssetsList.tsx | 926 +++++++++--------- 2 files changed, 460 insertions(+), 467 deletions(-) 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 dffe7b694a..6a194a6d93 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 @@ -218,6 +218,7 @@ const AssetRow: FC<{ <> (await import("~/components/desktop/UserPositions")).UserPositions, { - ssr: false, -}); - const AssetsList = () => { - // const { selectedAccount, nativeSolBalance } = useStore(); - const { connected, walletAddress } = useWalletContext(); + const { connected } = useWalletContext(); const [isStoreInitialized, extendedBankInfos, nativeSolBalance, selectedAccount] = useMrgnlendStore((state) => [ state.initialized, state.extendedBankInfos, @@ -183,480 +178,477 @@ const AssetsList = () => { return ( <> - <> - - - {!isFilteredUserPositions && ( -
- - - {poolFilter !== "isolated" && ( -
- - - -
- Global pool -
-
- + +
+ + + {poolFilter !== "isolated" && ( +
+ + + +
+ Global pool +
+
+ +
+ Price + + + Realtime prices + + + Powered by Pyth and Switchboard. + + + } + placement="top" > -
- Price - - - Realtime prices - - - Powered by Pyth and Switchboard. - - - } - placement="top" - > - - -
- - +
+
+
+ +
+ {isInLendingMode ? "APY" : "APR"} + + + {isInLendingMode ? "APY" : "APR"} + + + {isInLendingMode + ? "What you'll earn on deposits over a year. This includes compounding. All marginfi deposits are compounded hourly." + : "What you'll pay for your borrows, or the price of a loan. This does not include compounding. All marginfi borrows are compounded hourly."} + + + } + placement="top" > -
- {isInLendingMode ? "APY" : "APR"} - - - {isInLendingMode ? "APY" : "APR"} - - - {isInLendingMode - ? "What you'll earn on deposits over a year. This includes compounding. All marginfi deposits are compounded hourly." - : "What you'll pay for your borrows, or the price of a loan. This does not include compounding. All marginfi borrows are compounded hourly."} - - - } - placement="top" - > - - -
- - +
+
+
+ +
+ {isInLendingMode ? "Weight" : "LTV"} + + + {isInLendingMode ? "Weight" : "LTV"} + + + {isInLendingMode + ? "How much your assets count for collateral, relative to their USD value. The higher the weight, the more collateral you can borrow against it." + : "How much you can borrow against your free collateral. The higher the LTV, the more you can borrow against your free collateral."} + + + } + placement="top" > -
- {isInLendingMode ? "Weight" : "LTV"} - - - {isInLendingMode ? "Weight" : "LTV"} - - - {isInLendingMode - ? "How much your assets count for collateral, relative to their USD value. The higher the weight, the more collateral you can borrow against it." - : "How much you can borrow against your free collateral. The higher the LTV, the more you can borrow against your free collateral."} - - - } - placement="top" - > - - -
- - +
+
+
+ +
+ {isInLendingMode ? "Deposits" : "Available"} + + + {isInLendingMode ? "Total deposits" : "Total available"} + + + {isInLendingMode + ? "Total marginfi deposits for each asset. Everything is denominated in native tokens." + : "The amount of tokens available to borrow for each asset. Calculated as the minimum of the asset's borrow limit and available liquidity that has not yet been borrowed."} + + + } + placement="top" > -
- {isInLendingMode ? "Deposits" : "Available"} - - - {isInLendingMode ? "Total deposits" : "Total available"} - - - {isInLendingMode - ? "Total marginfi deposits for each asset. Everything is denominated in native tokens." - : "The amount of tokens available to borrow for each asset. Calculated as the minimum of the asset's borrow limit and available liquidity that has not yet been borrowed."} - - - } - placement="top" - > - - -
- - - {/*******************************/} - {/* [START]: ZOOM-BASED COLUMNS */} - {/*******************************/} - - {lendZoomLevel < 2 && ( - +
+
+
+ + {/*******************************/} + {/* [START]: ZOOM-BASED COLUMNS */} + {/*******************************/} + + {lendZoomLevel < 2 && ( + +
+ Global limit + + + {isInLendingMode ? "Global deposit cap" : "Global borrow cap"} + + Each marginfi pool has global deposit and borrow limits, also known as caps. This is the + total amount that all users combined can deposit or borrow of a given token. + + } + placement="top" > -
- Global limit - - - {isInLendingMode ? "Global deposit cap" : "Global borrow cap"} - - Each marginfi pool has global deposit and borrow limits, also known as caps. This is - the total amount that all users combined can deposit or borrow of a given token. - - } - placement="top" - > - - -
- - )} - - {lendZoomLevel < 3 && ( - +
+
+
+ )} + + {lendZoomLevel < 3 && ( + +
+ Utilization + + + Pool utilization + + What percentage of supplied tokens have been borrowed. This helps determine interest + rates. This is not based on the global pool limits, which can limit utilization. + + } + placement="top" > -
- Utilization - - - Pool utilization - - What percentage of supplied tokens have been borrowed. This helps determine interest - rates. This is not based on the global pool limits, which can limit utilization. - - } - placement="top" - > - - -
- - )} - - {/*******************************/} - {/* [END]: ZOOM-BASED COLUMNS */} - {/*******************************/} - - +
+
+
+ )} + + {/*******************************/} + {/* [END]: ZOOM-BASED COLUMNS */} + {/*******************************/} + + +
Wallet Amt.
+
+ + +
+
+ + + {globalBanks + .filter((b) => !b.info.state.isIsolated) + .map((bank, i) => { + if (poolFilter === "stable" && !STABLECOINS.includes(bank.meta.tokenSymbol)) return null; + if (poolFilter === "lst" && !LSTS.includes(bank.meta.tokenSymbol)) return null; + + // check to see if bank is in open positions + const userPosition = activeBankInfos.filter( + (activeBankInfo) => activeBankInfo.meta.tokenSymbol === bank.meta.tokenSymbol + ); + + if (isFilteredUserPositions && !userPosition.length) return null; + + return isStoreInitialized ? ( + 0 ? userPosition[0] : undefined} + showLSTDialog={(variant: LSTDialogVariants, onClose?: () => void) => { + setLSTDialogVariant(variant); + setIsLSTDialogOpen(true); + if (onClose) { + setLSTDialogCallback(() => onClose); + } + }} + /> + ) : ( + + ); + })} + +
+ )} + {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" > -
Wallet Amt.
- - - - - - - - {globalBanks - .filter((b) => !b.info.state.isIsolated) - .map((bank, i) => { - if (poolFilter === "stable" && !STABLECOINS.includes(bank.meta.tokenSymbol)) return null; - if (poolFilter === "lst" && !LSTS.includes(bank.meta.tokenSymbol)) return null; - - // check to see if bank is in open positions - const userPosition = activeBankInfos.filter( - (activeBankInfo) => activeBankInfo.meta.tokenSymbol === bank.meta.tokenSymbol - ); - - return isStoreInitialized ? ( - 0 ? userPosition[0] : undefined} - showLSTDialog={(variant: LSTDialogVariants, onClose?: () => void) => { - setLSTDialogVariant(variant); - setIsLSTDialogOpen(true); - if (onClose) { - setLSTDialogCallback(() => onClose); - } - }} - /> - ) : ( - - ); - })} - -
- )} - {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" - > - - -
-
- + + + + +
+ Price + + + Realtime prices + + + Powered by Pyth and Switchboard. + + + } + placement="top" > -
- Price - - - Realtime prices - - - Powered by Pyth and Switchboard. - - - } - placement="top" - > - - -
- - +
+
+
+ +
+ {isInLendingMode ? "APY" : "APR"} + + + {isInLendingMode ? "APY" : "APR"} + + + {isInLendingMode + ? "What you'll earn on deposits over a year. This includes compounding. All marginfi deposits are compounded hourly." + : "What you'll pay for your borrows, or the price of a loan. This does not include compounding. All marginfi borrows are compounded hourly."} + + + } + placement="top" > -
- {isInLendingMode ? "APY" : "APR"} - - - {isInLendingMode ? "APY" : "APR"} - - - {isInLendingMode - ? "What you'll earn on deposits over a year. This includes compounding. All marginfi deposits are compounded hourly." - : "What you'll pay for your borrows, or the price of a loan. This does not include compounding. All marginfi borrows are compounded hourly."} - - - } - placement="top" - > - - -
- - +
+
+
+ +
+ {isInLendingMode ? "Weight" : "LTV"} + + + {isInLendingMode ? "Weight" : "LTV"} + + + {isInLendingMode + ? "How much your assets count for collateral, relative to their USD value. The higher the weight, the more collateral you can borrow against it." + : "How much you can borrow against your free collateral. The higher the LTV, the more you can borrow against your free collateral."} + + + } + placement="top" > -
- {isInLendingMode ? "Weight" : "LTV"} - - - {isInLendingMode ? "Weight" : "LTV"} - - - {isInLendingMode - ? "How much your assets count for collateral, relative to their USD value. The higher the weight, the more collateral you can borrow against it." - : "How much you can borrow against your free collateral. The higher the LTV, the more you can borrow against your free collateral."} - - - } - placement="top" - > - - -
- - +
+
+
+ +
+ {isInLendingMode ? "Deposits" : "Available"} + + + {isInLendingMode ? "Total deposits" : "Total available"} + + + {isInLendingMode + ? "Total marginfi deposits for each asset. Everything is denominated in native tokens." + : "The amount of tokens available to borrow for each asset. Calculated as the minimum of the asset's borrow limit and available liquidity that has not yet been borrowed."} + + + } + placement="top" > -
- {isInLendingMode ? "Deposits" : "Available"} - - - {isInLendingMode ? "Total deposits" : "Total available"} - - - {isInLendingMode - ? "Total marginfi deposits for each asset. Everything is denominated in native tokens." - : "The amount of tokens available to borrow for each asset. Calculated as the minimum of the asset's borrow limit and available liquidity that has not yet been borrowed."} - - - } - placement="top" - > - - -
- - - {/*******************************/} - {/* [START]: ZOOM-BASED COLUMNS */} - {/*******************************/} - - {lendZoomLevel < 2 && ( - +
+
+
+ + {/*******************************/} + {/* [START]: ZOOM-BASED COLUMNS */} + {/*******************************/} + + {lendZoomLevel < 2 && ( + +
+ Global limit + + + {isInLendingMode ? "Global deposit cap" : "Global borrow cap"} + + Each marginfi pool has global deposit and borrow limits, also known as caps. This is the + total amount that all users combined can deposit or borrow of a given token. + + } + placement="top" > -
- Global limit - - - {isInLendingMode ? "Global deposit cap" : "Global borrow cap"} - - Each marginfi pool has global deposit and borrow limits, also known as caps. This is - the total amount that all users combined can deposit or borrow of a given token. - - } - placement="top" - > - - -
- - )} - - {lendZoomLevel < 3 && ( - +
+
+
+ )} + + {lendZoomLevel < 3 && ( + +
+ Utilization + + + Pool utilization + + What percentage of supplied tokens have been borrowed. This helps determine interest + rates. This is not based on the global pool limits, which can limit utilization. + + } + placement="top" > -
- Utilization - - - Pool utilization - - What percentage of supplied tokens have been borrowed. This helps determine interest - rates. This is not based on the global pool limits, which can limit utilization. - - } - placement="top" - > - - -
- - )} - - {/*******************************/} - {/* [END]: ZOOM-BASED COLUMNS */} - {/*******************************/} - - -
Wallet Amt.
-
- - - - - - {isolatedBanks - .filter((b) => b.info.state.isIsolated) - .map((bank) => - isStoreInitialized ? ( - - ) : ( - - ) - )} - -
- )} - - -
- )} - {walletAddress && } - + info + + +
+ )} + + {/*******************************/} + {/* [END]: ZOOM-BASED COLUMNS */} + {/*******************************/} + + +
Wallet Amt.
+
+ + +
+ + + {isolatedBanks + .filter((b) => b.info.state.isIsolated) + .map((bank) => + isStoreInitialized ? ( + + ) : ( + + ) + )} + + + )} + + + Date: Fri, 24 Nov 2023 21:57:30 -0500 Subject: [PATCH 08/14] feat: mobile inline positions --- .../AssetCard/AssetCardPosition.tsx | 57 ++++++++----------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCardPosition.tsx b/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCardPosition.tsx index 3c38831552..4989f28478 100644 --- a/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCardPosition.tsx +++ b/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCardPosition.tsx @@ -1,42 +1,31 @@ -import React, { FC, useState } from "react"; +import React from "react"; -import { usdFormatter } from "@mrgnlabs/mrgn-common"; +import { usdFormatter, numeralFormatter } from "@mrgnlabs/mrgn-common"; import { ActiveBankInfo } from "@mrgnlabs/marginfi-v2-ui-state"; -import ExpandLessIcon from "@mui/icons-material/ExpandLess"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; - -export const AssetCardPosition: FC<{ - activeBank: ActiveBankInfo; -}> = ({ activeBank }) => { - const [isCollapsed, setIsCollapsed] = useState(true); +import { cn } from "~/utils"; +export const AssetCardPosition = ({ activeBank }: { activeBank: ActiveBankInfo }) => { return ( -
-
setIsCollapsed(!isCollapsed)}> -
Your position details
- {isCollapsed ? : } -
- {!isCollapsed && ( -
-
-
- {(activeBank as ActiveBankInfo).position.isLending ? "Lending" : "Borrowing"} -
-
- {(activeBank as ActiveBankInfo).position.amount.toFixed(activeBank.info.state.mintDecimals) + - " " + - activeBank.meta.tokenSymbol} -
-
-
-
USD value
-
- {usdFormatter.format((activeBank as ActiveBankInfo).position.usdValue)} -
-
-
- )} +
+

Your position details

+
+
{activeBank.position.isLending ? "Lending" : "Borrowing"}
+
+ {activeBank.position.amount < 0.01 && "< "} + {numeralFormatter(activeBank.position.amount) + " " + activeBank.meta.tokenSymbol} +
+
USD Value
+
{usdFormatter.format(activeBank.position.usdValue)}
+ {activeBank.position.liquidationPrice && activeBank.position.liquidationPrice > 0 && ( + <> +
Liq price
+
+ {usdFormatter.format(activeBank.position.liquidationPrice)} +
+ + )} +
); }; From 051442dc928b7917211b0573d2576e5823d154f3 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Fri, 24 Nov 2023 22:02:46 -0500 Subject: [PATCH 09/14] feat: more padding around asset row buttons --- .../src/components/desktop/AssetsList/AssetRow/AssetRow.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 6a194a6d93..f6bd1b8d6a 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 @@ -467,7 +467,7 @@ const AssetRow: FC<{ - + -
+

Your position details

{userPosition.position.isLending ? "Lending" : "Borrowing"}
From 4731113f321400ef57ee491dd92e179a52f6dce6 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Fri, 24 Nov 2023 22:23:39 -0500 Subject: [PATCH 10/14] chore: various inline position fixes / improvements --- .../desktop/AssetsList/AssetRow/AssetRow.tsx | 21 ++++++++++++++++--- .../AssetCard/AssetCardPosition.tsx | 6 +++--- apps/marginfi-v2-ui/src/hooks/useIsMobile.ts | 2 +- 3 files changed, 22 insertions(+), 7 deletions(-) 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 f6bd1b8d6a..53a517e3fa 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 @@ -17,6 +17,7 @@ import { MrgnTooltip } from "~/components/common/MrgnTooltip"; import { AssetRowInputBox, AssetRowAction, LSTDialogVariants } from "~/components/common/AssetList"; import { useAssetItemData } from "~/hooks/useAssetItemData"; import { useWalletContext } from "~/hooks/useWalletContext"; +import { useIsMobile } from "~/hooks/useIsMobile"; import { closeBalance, executeLendingAction, MarginfiActionParams, cn } from "~/utils"; export const EMISSION_MINT_INFO_MAP = new Map([ @@ -66,6 +67,20 @@ const AssetRow: FC<{ const { rateAP, assetWeight, isBankFilled, isBankHigh, bankCap } = useAssetItemData({ bank, isInLendingMode }); const [hasLSTDialogShown, setHasLSTDialogShown] = useState([]); const { walletContextState } = useWalletContext(); + const isMobile = useIsMobile(); + + const userPositionColSpan = useMemo(() => { + if (isMobile) { + return 6; + } + if (lendZoomLevel === 3) { + return 9; + } + if (lendZoomLevel === 2) { + return 10; + } + return 11; + }, [isMobile, lendZoomLevel]); const assetPrice = useMemo( () => @@ -219,7 +234,7 @@ const AssetRow: FC<{ {userPosition.position.liquidationPrice && userPosition.position.liquidationPrice > 0 && ( <> -
Liq price
+
Liquidation price
{usdFormatter.format(userPosition.position.liquidationPrice)}
diff --git a/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCardPosition.tsx b/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCardPosition.tsx index 4989f28478..a53e333e29 100644 --- a/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCardPosition.tsx +++ b/apps/marginfi-v2-ui/src/components/mobile/MobileAssetsList/AssetCard/AssetCardPosition.tsx @@ -7,9 +7,9 @@ import { cn } from "~/utils"; export const AssetCardPosition = ({ activeBank }: { activeBank: ActiveBankInfo }) => { return ( -
+

Your position details

-
+
{activeBank.position.isLending ? "Lending" : "Borrowing"}
{activeBank.position.amount < 0.01 && "< "} @@ -19,7 +19,7 @@ export const AssetCardPosition = ({ activeBank }: { activeBank: ActiveBankInfo }
{usdFormatter.format(activeBank.position.usdValue)}
{activeBank.position.liquidationPrice && activeBank.position.liquidationPrice > 0 && ( <> -
Liq price
+
Liquidation price
{usdFormatter.format(activeBank.position.liquidationPrice)}
diff --git a/apps/marginfi-v2-ui/src/hooks/useIsMobile.ts b/apps/marginfi-v2-ui/src/hooks/useIsMobile.ts index f4ec3b5849..b3710743cf 100644 --- a/apps/marginfi-v2-ui/src/hooks/useIsMobile.ts +++ b/apps/marginfi-v2-ui/src/hooks/useIsMobile.ts @@ -6,7 +6,7 @@ export const useIsMobile = (): boolean => { useLayoutEffect(() => { const updateSize = (): void => { - setIsMobile(window.innerWidth < 768); + setIsMobile(window.innerWidth < 1024); }; window.addEventListener("resize", debounce(updateSize, 250)); updateSize(); From 1e4b98988968779ed2427b013edd14839913b29c Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Mon, 27 Nov 2023 09:33:04 -0500 Subject: [PATCH 11/14] feat: higlight inline positions with poor health --- .../desktop/AssetsList/AssetRow/AssetRow.tsx | 47 ++++++++++++++++--- .../desktop/AssetsList/AssetsList.tsx | 3 -- 2 files changed, 41 insertions(+), 9 deletions(-) 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 53a517e3fa..15917462b7 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 @@ -19,6 +19,7 @@ import { useAssetItemData } from "~/hooks/useAssetItemData"; import { useWalletContext } from "~/hooks/useWalletContext"; import { useIsMobile } from "~/hooks/useIsMobile"; import { closeBalance, executeLendingAction, MarginfiActionParams, cn } from "~/utils"; +import { IconAlertTriangle } from "~/components/ui/icons"; export const EMISSION_MINT_INFO_MAP = new Map([ [ @@ -69,6 +70,17 @@ const AssetRow: FC<{ const { walletContextState } = useWalletContext(); const isMobile = useIsMobile(); + const isUserPositionPoorHealth = useMemo(() => { + if (!userPosition || !userPosition.position.liquidationPrice) { + return false; + } + + const alertRange = userPosition.position.liquidationPrice * 0.05; + const lowerBound = userPosition.position.liquidationPrice - alertRange; + + return userPosition.position.isLending ? bank.info.state.price <= lowerBound : bank.info.state.price >= lowerBound; + }, [userPosition]); + const userPositionColSpan = useMemo(() => { if (isMobile) { return 6; @@ -515,13 +527,19 @@ const AssetRow: FC<{ fontWeight: 300, }} > -
+

Your position details

{userPosition.position.isLending ? "Lending" : "Borrowing"}
-
- {userPosition.position.amount < 0.01 && "< "} - {numeralFormatter(userPosition.position.amount) + " " + bank.meta.tokenSymbol} +
+ {userPosition.position.amount < 0.01 && "< 0.00"} + {userPosition.position.amount >= 0.01 && + numeralFormatter(userPosition.position.amount) + " " + bank.meta.tokenSymbol} + {userPosition.position.amount < 0.01 && ( + {userPosition.position.amount}} placement="top"> + info + + )}
USD Value
{userPosition.position.liquidationPrice && userPosition.position.liquidationPrice > 0 && ( <> -
Liquidation price
-
+
+ {isUserPositionPoorHealth && ( + + + + )} + Liquidation price +
+
{usdFormatter.format(userPosition.position.liquidationPrice)}
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 65ef1bea51..f042948fe2 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetsList.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/AssetsList/AssetsList.tsx @@ -110,9 +110,6 @@ const AssetsList = () => { [activeBankInfos] ); - console.log("lendPositions", lendPositions); - console.log("borrowPositions", borrowPositions); - // Enter hotkey mode useHotkeys( "meta + k", From 1b01d03f81a8f025a09cd79c10ba3e513c33fa7f Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Mon, 27 Nov 2023 10:05:25 -0500 Subject: [PATCH 12/14] fix: liq alert range logic --- .../desktop/AssetsList/AssetRow/AssetRow.tsx | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) 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 15917462b7..076b74e2a5 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 @@ -75,10 +75,24 @@ const AssetRow: FC<{ return false; } - const alertRange = userPosition.position.liquidationPrice * 0.05; - const lowerBound = userPosition.position.liquidationPrice - alertRange; - - return userPosition.position.isLending ? bank.info.state.price <= lowerBound : bank.info.state.price >= lowerBound; + const alertRange = bank.info.state.price * 0.05; + const lowerBound = bank.info.state.price - alertRange; + const upperBound = bank.info.state.price + alertRange; + + console.log( + bank.meta.tokenName, + bank.info.state.price, + alertRange, + lowerBound, + upperBound, + userPosition.position.liquidationPrice + ); + + if (userPosition.position.isLending) { + return userPosition.position.liquidationPrice >= lowerBound; + } else { + return userPosition.position.liquidationPrice <= upperBound; + } }, [userPosition]); const userPositionColSpan = useMemo(() => { @@ -552,7 +566,7 @@ const AssetRow: FC<{ > {usdFormatter.format(userPosition.position.usdValue)} - {userPosition.position.liquidationPrice && userPosition.position.liquidationPrice > 0 && ( + {userPosition.position.liquidationPrice && ( <>
Date: Mon, 27 Nov 2023 10:26:50 -0500 Subject: [PATCH 13/14] chore: simplify alert logic --- .../desktop/AssetsList/AssetRow/AssetRow.tsx | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) 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 076b74e2a5..1e541251f8 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 @@ -75,24 +75,7 @@ const AssetRow: FC<{ return false; } - const alertRange = bank.info.state.price * 0.05; - const lowerBound = bank.info.state.price - alertRange; - const upperBound = bank.info.state.price + alertRange; - - console.log( - bank.meta.tokenName, - bank.info.state.price, - alertRange, - lowerBound, - upperBound, - userPosition.position.liquidationPrice - ); - - if (userPosition.position.isLending) { - return userPosition.position.liquidationPrice >= lowerBound; - } else { - return userPosition.position.liquidationPrice <= upperBound; - } + return bank.info.state.price < userPosition.position.liquidationPrice * (1 + 0.05); }, [userPosition]); const userPositionColSpan = useMemo(() => { @@ -546,7 +529,7 @@ const AssetRow: FC<{
{userPosition.position.isLending ? "Lending" : "Borrowing"}
- {userPosition.position.amount < 0.01 && "< 0.00"} + {userPosition.position.amount < 0.01 && "< 0.01"} {userPosition.position.amount >= 0.01 && numeralFormatter(userPosition.position.amount) + " " + bank.meta.tokenSymbol} {userPosition.position.amount < 0.01 && ( From 9683ecddaf9dae2d245684656853eba6557e00f2 Mon Sep 17 00:00:00 2001 From: Kobe Leenders Date: Mon, 27 Nov 2023 17:13:07 +0100 Subject: [PATCH 14/14] feat: improved toasts throughout the app --- .../common/AssetList/AssetRowInputBox.tsx | 4 ++-- .../Staking/StakingCard/StakingCard.tsx | 14 +++++++---- .../desktop/DesktopNavbar/DesktopNavbar.tsx | 2 -- .../src/components/desktop/Earn/index.tsx | 21 ++++------------- .../src/components/mobile/EmissionsBanner.tsx | 2 -- apps/marginfi-v2-ui/src/pages/bridge.tsx | 23 ++++--------------- apps/marginfi-v2-ui/src/utils/mrgnActions.ts | 7 +++++- 7 files changed, 27 insertions(+), 46 deletions(-) diff --git a/apps/marginfi-v2-ui/src/components/common/AssetList/AssetRowInputBox.tsx b/apps/marginfi-v2-ui/src/components/common/AssetList/AssetRowInputBox.tsx index c25e5d1329..a2e38e3403 100644 --- a/apps/marginfi-v2-ui/src/components/common/AssetList/AssetRowInputBox.tsx +++ b/apps/marginfi-v2-ui/src/components/common/AssetList/AssetRowInputBox.tsx @@ -1,7 +1,7 @@ import { InputAdornment, TextField } from "@mui/material"; import { FC, MouseEventHandler, useCallback } from "react"; import { NumberFormatValues, NumericFormat } from "react-number-format"; -import { toast } from "react-toastify"; +import { showErrorToast } from "~/utils/toastUtils"; interface AssetRowInputBox { value: number; @@ -28,7 +28,7 @@ const AssetRowInputBox: FC = ({ if (maxValue !== undefined) { setValue(maxValue); } else { - toast.error("Not implemented"); + showErrorToast("Not implemented"); } }, [maxValue, setValue]); diff --git a/apps/marginfi-v2-ui/src/components/common/Staking/StakingCard/StakingCard.tsx b/apps/marginfi-v2-ui/src/components/common/Staking/StakingCard/StakingCard.tsx index 967056c36a..fe7335fc86 100644 --- a/apps/marginfi-v2-ui/src/components/common/Staking/StakingCard/StakingCard.tsx +++ b/apps/marginfi-v2-ui/src/components/common/Staking/StakingCard/StakingCard.tsx @@ -37,7 +37,6 @@ import { SwapMode, useJupiter } from "@jup-ag/react-hook"; import JSBI from "jsbi"; import { StakeData, usePrevious } from "~/utils"; import { createJupiterApiClient } from "@jup-ag/api"; -import { toast } from "react-toastify"; import { SettingsModal } from "./SettingsModal"; import { SettingsIcon } from "./SettingsIcon"; import { LST_MINT, TokenData, TokenDataMap } from "~/store/lstStore"; @@ -46,6 +45,7 @@ import { IconLoader } from "~/components/ui/icons"; import BN from "bn.js"; import debounce from "lodash.debounce"; import { Desktop, Mobile } from "~/mediaQueries"; +import { MultiStepToastHandle, showErrorToast } from "~/utils/toastUtils"; const QUOTE_EXPIRY_MS = 30_000; const DEFAULT_DEPOSIT_OPTION: DepositOption = { type: "native", amount: new BN(0), maxAmount: new BN(0) }; @@ -182,7 +182,7 @@ export const StakingCard: FC = () => { [depositOption, refresh, lastRefreshTimestamp] ); - const showErrotToast = useRef(debounce(() => toast.error("Failed to find route"), 250)); + const showErrotToast = useRef(debounce(() => showErrorToast("Failed to find route"), 250)); const prevError = usePrevious(error); useEffect(() => { @@ -267,6 +267,9 @@ export const StakingCard: FC = () => { setOngoingAction("minting"); + const multiStepToast = new MultiStepToastHandle("Stake", [{ label: "Minting LST" }]); + multiStepToast.start(); + try { if (depositOption.type === "stake") { const { instructions, signers } = await makeDepositStakeToStakePoolIx( @@ -293,6 +296,7 @@ export const StakingCard: FC = () => { } else if (depositOption.type === "token") { const quote = quoteResponseMeta?.original; if (!quote) { + multiStepToast.setFailed("Route not calculated yet"); console.error("Route not calculated yet"); return; } @@ -346,10 +350,10 @@ export const StakingCard: FC = () => { "confirmed" ); } else { - throw new Error("Invalid deposit option"); + multiStepToast.setFailed("Invalid deposit option"); } - toast.success("Minting complete"); + multiStepToast.setSuccessAndNext(); } catch (error: any) { if (error.logs) { console.log("------ Logs 👇 ------"); @@ -360,7 +364,7 @@ export const StakingCard: FC = () => { if (errorMsg) { errorMsg = errorMsg ? errorMsg : "Transaction failed!"; } - toast.error(errorMsg); + multiStepToast.setFailed(errorMsg); } finally { await Promise.all([refresh(), fetchLstState()]); setDepositOption((currentDepositOption) => diff --git a/apps/marginfi-v2-ui/src/components/desktop/DesktopNavbar/DesktopNavbar.tsx b/apps/marginfi-v2-ui/src/components/desktop/DesktopNavbar/DesktopNavbar.tsx index 338b4ba711..c0f566896d 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/DesktopNavbar/DesktopNavbar.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/DesktopNavbar/DesktopNavbar.tsx @@ -11,7 +11,6 @@ import { useWalletContext } from "~/hooks/useWalletContext"; import { Features, isActive } from "~/utils/featureGates"; import { PublicKey } from "@solana/web3.js"; import { useConnection } from "~/hooks/useConnection"; -import { toast } from "react-toastify"; import { EMISSION_MINT_INFO_MAP } from "../AssetsList/AssetRow"; import { collectRewardsBatch } from "~/utils"; import { IconMrgn } from "~/components/ui/icons"; @@ -301,7 +300,6 @@ const DesktopNavbar: FC = () => { onClick={async () => { if (!wallet || !selectedAccount || bankAddressesWithEmissions.length === 0) return; await collectRewardsBatch(connection, wallet, selectedAccount, bankAddressesWithEmissions); - toast.success("Withdrawal successful"); }} > collect rewards diff --git a/apps/marginfi-v2-ui/src/components/desktop/Earn/index.tsx b/apps/marginfi-v2-ui/src/components/desktop/Earn/index.tsx index cfc62dedb2..0a0351e601 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/Earn/index.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/Earn/index.tsx @@ -28,11 +28,11 @@ import { } from "@mrgnlabs/mrgn-common"; import { Bank, PriceBias } from "@mrgnlabs/marginfi-client-v2"; import { Countdown } from "~/components/desktop/Countdown"; -import { toast } from "react-toastify"; import BigNumber from "bignumber.js"; import { useWalletContext } from "~/hooks/useWalletContext"; import { useMrgnlendStore } from "~/store"; import { Desktop } from "~/mediaQueries"; +import { MultiStepToastHandle } from "~/utils/toastUtils"; const Earn = () => { const { wallet, isOverride, connected, walletAddress } = useWalletContext(); @@ -181,25 +181,14 @@ const Earn = () => { const closeDeposit = useCallback( async (deposit: Deposit) => { if (!lipAccount) return; - toast.loading(`Closing deposit`, { - toastId: "close-deposit", - }); + const multiStepToast = new MultiStepToastHandle("Close deposit", [{ label: "Closing deposit" }]); + multiStepToast.start(); try { await lipAccount.closePosition(deposit); - toast.update("close-deposit", { - render: `Closing deposit 👍`, - type: toast.TYPE.SUCCESS, - autoClose: 2000, - isLoading: false, - }); + multiStepToast.setSuccessAndNext(); } catch (e) { console.error(e); - toast.update("close-deposit", { - render: `Error closing deposit: ${e}`, - type: toast.TYPE.ERROR, - autoClose: 2000, - isLoading: false, - }); + multiStepToast.setFailed(`Error closing deposit: ${e}`); } setReloading(true); setLipAccount(await lipAccount.reloadAndClone()); diff --git a/apps/marginfi-v2-ui/src/components/mobile/EmissionsBanner.tsx b/apps/marginfi-v2-ui/src/components/mobile/EmissionsBanner.tsx index eb1e1da095..5adae414f8 100644 --- a/apps/marginfi-v2-ui/src/components/mobile/EmissionsBanner.tsx +++ b/apps/marginfi-v2-ui/src/components/mobile/EmissionsBanner.tsx @@ -5,7 +5,6 @@ import { useWalletContext } from "~/hooks/useWalletContext"; import { useMrgnlendStore } from "~/store"; import { collectRewardsBatch } from "~/utils"; import { EMISSION_MINT_INFO_MAP } from "../desktop/AssetsList/AssetRow"; -import { toast } from "react-toastify"; const EmissionsBanner: FC = () => { const { connection } = useConnection(); @@ -33,7 +32,6 @@ const EmissionsBanner: FC = () => { onClick={async () => { if (!wallet || !selectedAccount || bankAddressesWithEmissions.length === 0) return; await collectRewardsBatch(connection, wallet, selectedAccount, bankAddressesWithEmissions); - toast.success("Withdrawal successful"); }} >
Collect LM rewards
diff --git a/apps/marginfi-v2-ui/src/pages/bridge.tsx b/apps/marginfi-v2-ui/src/pages/bridge.tsx index 794095fa4e..92184bcd3c 100644 --- a/apps/marginfi-v2-ui/src/pages/bridge.tsx +++ b/apps/marginfi-v2-ui/src/pages/bridge.tsx @@ -2,7 +2,6 @@ import React from "react"; import Script from "next/script"; -import { toast } from "react-toastify"; import { useHotkeys } from "react-hotkeys-hook"; import config from "~/config"; @@ -11,6 +10,7 @@ import { useUserProfileStore, useUiStore } from "~/store"; import { Desktop } from "~/mediaQueries"; import { useWalletContext } from "~/hooks/useWalletContext"; import { PageHeader } from "~/components/common/PageHeader"; +import { MultiStepToastHandle } from "~/utils/toastUtils"; const tokens = [ "0x0000000000000000000000000000000000000000", // SOL @@ -111,6 +111,7 @@ const BridgePage = () => { }, [walletContextState, handleConnect, walletAddress]); const handleLoadMayanWidget = React.useCallback(() => { + const multiStepToast = new MultiStepToastHandle("Bridge", [{ label: `Cross-chain swap/bridge in progress` }]); const configIndex = isBridgeIn ? 0 : 1; const config = { ...configs[configIndex], @@ -123,27 +124,13 @@ const BridgePage = () => { }; window.MayanSwap.init("swap_widget", config); window.MayanSwap.setSwapInitiateListener((data) => { - toast.loading("Cross-chain swap/bridge in progress", { - toastId: data.hash, - }); + multiStepToast.start(); }); window.MayanSwap.setSwapCompleteListener((data) => { - toast.update(data.hash, { - render: "Cross-chain swap/bridge done", - toastId: data.hash, - type: toast.TYPE.SUCCESS, - autoClose: 5000, - isLoading: false, - }); + multiStepToast.setSuccessAndNext(); }); window.MayanSwap.setSwapRefundListener((data) => { - toast.update(data.hash, { - render: "Cross-chain swap/bridge refunded", - toastId: data.hash, - type: toast.TYPE.WARNING, - autoClose: 5000, - isLoading: false, - }); + multiStepToast.setFailed("Cross-chain swap/bridge refunded"); }); }, [handleConnect, isBridgeIn, walletAddress, walletContextState.disconnect, walletContextState.signTransaction]); diff --git a/apps/marginfi-v2-ui/src/utils/mrgnActions.ts b/apps/marginfi-v2-ui/src/utils/mrgnActions.ts index e885095a19..b74a8e463c 100644 --- a/apps/marginfi-v2-ui/src/utils/mrgnActions.ts +++ b/apps/marginfi-v2-ui/src/utils/mrgnActions.ts @@ -219,10 +219,14 @@ export async function collectRewardsBatch( marginfiAccount: MarginfiAccountWrapper, bankAddresses: PublicKey[] ) { + const multiStepToast = new MultiStepToastHandle("Collect rewards", [{ label: "Collecting rewards" }]); + multiStepToast.start(); + try { const tx = new Transaction(); const ixs = []; const signers = []; + for (const bankAddress of bankAddresses) { const ix = await marginfiAccount.makeWithdrawEmissionsIx(bankAddress); ixs.push(...ix.instructions); @@ -230,9 +234,10 @@ export async function collectRewardsBatch( } tx.add(...ixs); await processTransaction(connection, wallet, tx); + multiStepToast.setSuccessAndNext(); } catch (error: any) { const msg = extractErrorString(error); - showErrorToast(msg); + multiStepToast.setFailed(msg); console.log(`Error while collecting rewards: ${msg}`); console.log(error); return;