From 1ecd80dac22b12980d0765700647733ee5d31a03 Mon Sep 17 00:00:00 2001 From: Bob van der Helm <34470358+bobthebuidlr@users.noreply.github.com> Date: Fri, 10 Nov 2023 17:20:48 +0100 Subject: [PATCH] Hls leverage (#628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨Add basic modal for HLS staking * ✨UI components for Manage * ✨All Manage actions (except change lev) * 🐛hls intro icons + checkbox, hide repay when no debt, clickable dropdown * fix build * ✨finish all actiosn for HLS staking * 🐛clean up tooltip props --- src/api/hls/getHLSStakingAssets.ts | 1 + .../HLS/Staking/Table/Columns/Manage.tsx | 4 +- src/components/Header/DesktopHeader.tsx | 2 +- .../Modals/HLS/Deposit/Leverage.tsx | 14 +- src/components/Modals/HLS/Deposit/index.tsx | 41 ++-- .../Modals/HLS/Deposit/useAccordionItems.tsx | 3 + .../HLS/Deposit/useStakingController.tsx | 3 +- .../Modals/HLS/Manage/ChangeLeverage.tsx | 116 ++++++++- src/components/Modals/HLS/Manage/Deposit.tsx | 2 +- src/components/Modals/HLS/Manage/index.tsx | 4 +- src/components/Modals/HLS/index.tsx | 9 +- .../Modals/Vault/VaultBorrowings.tsx | 4 +- src/components/Routes.tsx | 7 +- src/components/Slider.tsx | 205 ---------------- src/components/Slider/LeverageLabel.tsx | 26 ++ src/components/Slider/Mark.tsx | 23 ++ src/components/Slider/Track.tsx | 32 +++ src/components/Slider/index.tsx | 222 ++++++++++++++++++ .../TokenInput/TokenInputWithSlider.tsx | 5 + src/components/Tooltip/index.tsx | 3 +- src/hooks/useUpdatedAccount/index.ts | 18 +- src/pages/HLSStakingPage.tsx | 4 +- src/store/slices/broadcast.ts | 27 ++- src/types/interfaces/store/broadcast.d.ts | 6 +- src/utils/actions.ts | 68 ++++++ tailwind.config.js | 16 ++ 26 files changed, 610 insertions(+), 255 deletions(-) delete mode 100644 src/components/Slider.tsx create mode 100644 src/components/Slider/LeverageLabel.tsx create mode 100644 src/components/Slider/Mark.tsx create mode 100644 src/components/Slider/Track.tsx create mode 100644 src/components/Slider/index.tsx create mode 100644 src/utils/actions.ts diff --git a/src/api/hls/getHLSStakingAssets.ts b/src/api/hls/getHLSStakingAssets.ts index 8f2ad39a..41aa22c8 100644 --- a/src/api/hls/getHLSStakingAssets.ts +++ b/src/api/hls/getHLSStakingAssets.ts @@ -26,6 +26,7 @@ export default async function getHLSStakingAssets() { used: BN(depositCap.amount), max: BN(depositCap.cap), }, + apy: 18, // TODO: Actually implement the APY here! } as HLSStrategy }) }) diff --git a/src/components/HLS/Staking/Table/Columns/Manage.tsx b/src/components/HLS/Staking/Table/Columns/Manage.tsx index 640b230a..94141d19 100644 --- a/src/components/HLS/Staking/Table/Columns/Manage.tsx +++ b/src/components/HLS/Staking/Table/Columns/Manage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import DropDownButton from 'components/Button/DropDownButton' import { ArrowDownLine, Cross, HandCoins, Plus, Scale } from 'components/Icons' @@ -61,7 +61,7 @@ export default function Manage(props: Props) { onClick: () => closeHlsStakingPosition({ accountId: props.account.id, actions }), }, ], - [actions, closeHlsStakingPosition, openModal, props.account.id, hasNoDebt], + [actions, closeHlsStakingPosition, hasNoDebt, openModal, props.account.id], ) return diff --git a/src/components/Header/DesktopHeader.tsx b/src/components/Header/DesktopHeader.tsx index ba6be5a5..367c9b0a 100644 --- a/src/components/Header/DesktopHeader.tsx +++ b/src/components/Header/DesktopHeader.tsx @@ -17,7 +17,7 @@ export const menuTree: { pages: Page[]; label: string }[] = [ { pages: ['lend', 'farm'], label: 'Earn' }, { pages: ['borrow'], label: 'Borrow' }, { pages: ['portfolio'], label: 'Portfolio' }, - ...(ENABLE_HLS ? [{ pages: ['hls-farm', 'hls-staking'] as Page[], label: 'High Leverage' }] : []), + ...(ENABLE_HLS ? [{ pages: ['hls-staking'] as Page[], label: 'High Leverage' }] : []), ] export default function DesktopHeader() { diff --git a/src/components/Modals/HLS/Deposit/Leverage.tsx b/src/components/Modals/HLS/Deposit/Leverage.tsx index 3c1f4f02..105fdd01 100644 --- a/src/components/Modals/HLS/Deposit/Leverage.tsx +++ b/src/components/Modals/HLS/Deposit/Leverage.tsx @@ -12,20 +12,28 @@ interface Props { onChangeAmount: (amount: BigNumber) => void onClickBtn: () => void positionValue: BigNumber + leverage: number + maxLeverage: number } export default function Leverage(props: Props) { return ( -
+
- -
) } diff --git a/src/components/Modals/HLS/Deposit/index.tsx b/src/components/Modals/HLS/Deposit/index.tsx index 1a619753..176f802a 100644 --- a/src/components/Modals/HLS/Deposit/index.tsx +++ b/src/components/Modals/HLS/Deposit/index.tsx @@ -16,6 +16,7 @@ interface Props { borrowAsset: Asset collateralAsset: Asset vaultAddress: string | null + strategy?: HLSStrategy } export default function Controller(props: Props) { @@ -46,19 +47,24 @@ export default function Controller(props: Props) { /> ) - return ( - - ) + if (props.strategy) { + return ( + + ) + } + + return null } interface ContentProps { @@ -120,7 +126,11 @@ function Vault(props: VaultContentProps) { return } -function StakingContent(props: ContentProps) { +interface StakingContentProps extends ContentProps { + strategy: HLSStrategy +} + +function StakingContent(props: StakingContentProps) { const { depositAmount, onChangeCollateral, @@ -152,10 +162,11 @@ function StakingContent(props: ContentProps) { positionValue, selectedAccount: props.selectedAccount, setSelectedAccount: props.setSelectedAccount, + strategy: props.strategy, toggleIsOpen: props.toggleIsOpen, updatedAccount, maxBorrowAmount, - apy: 0, // TODO: Implement APY + apy: props.strategy.apy || 0, // TODO: Implement APY walletCollateralAsset: props.walletCollateralAsset, }) diff --git a/src/components/Modals/HLS/Deposit/useAccordionItems.tsx b/src/components/Modals/HLS/Deposit/useAccordionItems.tsx index c93ccf77..f36db15b 100644 --- a/src/components/Modals/HLS/Deposit/useAccordionItems.tsx +++ b/src/components/Modals/HLS/Deposit/useAccordionItems.tsx @@ -29,6 +29,7 @@ interface Props { positionValue: BigNumber selectedAccount: Account | null setSelectedAccount: (account: Account) => void + strategy?: HLSStrategy toggleIsOpen: (index: number) => void updatedAccount: Account | undefined walletCollateralAsset: Coin | undefined @@ -64,12 +65,14 @@ export default function useAccordionItems(props: Props) { title: 'Leverage', renderContent: () => ( props.toggleIsOpen(2)} max={props.maxBorrowAmount} positionValue={props.positionValue} + maxLeverage={props.strategy?.maxLeverage || 1} /> ), renderSubTitle: () => ( diff --git a/src/components/Modals/HLS/Deposit/useStakingController.tsx b/src/components/Modals/HLS/Deposit/useStakingController.tsx index bcc0e0de..7a54ba32 100644 --- a/src/components/Modals/HLS/Deposit/useStakingController.tsx +++ b/src/components/Modals/HLS/Deposit/useStakingController.tsx @@ -23,8 +23,6 @@ export default function useStakingController(props: Props) { setBorrowAmount, borrowAmount, positionValue, - borrowCoin, - depositCoin, actions, } = useDepositHlsVault({ collateralDenom: collateralAsset.denom, @@ -40,6 +38,7 @@ export default function useStakingController(props: Props) { }, [computeMaxBorrowAmount, props.borrowAsset.denom]) const execute = useCallback(() => { + useStore.setState({ hlsModal: null }) addToStakingStrategy({ actions, accountId: selectedAccount.id, diff --git a/src/components/Modals/HLS/Manage/ChangeLeverage.tsx b/src/components/Modals/HLS/Manage/ChangeLeverage.tsx index 6cb40d3f..e9734118 100644 --- a/src/components/Modals/HLS/Manage/ChangeLeverage.tsx +++ b/src/components/Modals/HLS/Manage/ChangeLeverage.tsx @@ -1,10 +1,120 @@ +import React, { useCallback, useMemo, useState } from 'react' + +import Button from 'components/Button' +import LeverageSummary from 'components/Modals/HLS/Deposit/LeverageSummary' +import TokenInputWithSlider from 'components/TokenInput/TokenInputWithSlider' +import { DEFAULT_SETTINGS } from 'constants/defaultSettings' +import { LocalStorageKeys } from 'constants/localStorageKeys' +import { BN_ZERO } from 'constants/math' +import useHealthComputer from 'hooks/useHealthComputer' +import useLocalStorage from 'hooks/useLocalStorage' +import usePrices from 'hooks/usePrices' +import { useUpdatedAccount } from 'hooks/useUpdatedAccount' +import useStore from 'store' +import { BNCoin } from 'types/classes/BNCoin' +import { getAccountPositionValues } from 'utils/accounts' +import { getHlsStakingChangeLevActions } from 'utils/actions' +import { byDenom } from 'utils/array' + interface Props { - account: Account + account: HLSAccountWithStrategy action: HlsStakingManageAction borrowAsset: Asset collateralAsset: Asset } -export default function Repay(props: Props) { - return <> +export default function ChangeLeverage(props: Props) { + const { data: prices } = usePrices() + const [slippage] = useLocalStorage(LocalStorageKeys.SLIPPAGE, DEFAULT_SETTINGS.slippage) + const { updatedAccount, simulateHlsStakingDeposit, simulateHlsStakingWithdraw, leverage } = + useUpdatedAccount(props.account) + + const changeHlsStakingLeverage = useStore((s) => s.changeHlsStakingLeverage) + const { computeMaxBorrowAmount } = useHealthComputer(props.account) + const previousDebt: BigNumber = useMemo( + () => props.account.debts.find(byDenom(props.borrowAsset.denom))?.amount || BN_ZERO, + [props.account.debts, props.borrowAsset.denom], + ) + + const [currentDebt, setAmount] = useState(previousDebt) + const maxBorrowAmount = useMemo(() => { + return computeMaxBorrowAmount(props.borrowAsset.denom, 'deposit').plus(previousDebt) + }, [computeMaxBorrowAmount, previousDebt, props.borrowAsset.denom]) + + const onChangeAmount = useCallback( + (currentDebt: BigNumber) => { + setAmount(currentDebt) + if (currentDebt.isLessThan(previousDebt)) { + simulateHlsStakingWithdraw( + props.collateralAsset.denom, + props.borrowAsset.denom, + previousDebt.minus(currentDebt), + ) + } else { + simulateHlsStakingDeposit( + BNCoin.fromDenomAndBigNumber(props.collateralAsset.denom, BN_ZERO), + BNCoin.fromDenomAndBigNumber(props.borrowAsset.denom, currentDebt.minus(previousDebt)), + ) + } + }, + [ + previousDebt, + props.borrowAsset.denom, + props.collateralAsset.denom, + simulateHlsStakingDeposit, + simulateHlsStakingWithdraw, + ], + ) + + const positionValue = useMemo(() => { + const [deposits, lends, debts, vaults] = getAccountPositionValues( + updatedAccount || props.account, + prices, + ) + + return deposits.plus(lends).plus(debts).plus(vaults) + }, [prices, props.account, updatedAccount]) + + const handleOnClick = useCallback(() => { + useStore.setState({ hlsManageModal: null }) + if (currentDebt.isEqualTo(previousDebt)) return + const actions = getHlsStakingChangeLevActions( + previousDebt, + currentDebt, + props.collateralAsset.denom, + props.borrowAsset.denom, + slippage, + prices, + ) + changeHlsStakingLeverage({ accountId: props.account.id, actions }) + }, [ + currentDebt, + changeHlsStakingLeverage, + previousDebt, + prices, + props.account.id, + props.borrowAsset.denom, + props.collateralAsset.denom, + slippage, + ]) + + return ( + <> + +
+ +
+ + ) } diff --git a/src/components/Modals/HLS/Manage/Deposit.tsx b/src/components/Modals/HLS/Manage/Deposit.tsx index 33c8c445..e076caa0 100644 --- a/src/components/Modals/HLS/Manage/Deposit.tsx +++ b/src/components/Modals/HLS/Manage/Deposit.tsx @@ -88,7 +88,7 @@ export default function Deposit(props: Props) { const actions = useDepositActions({ depositCoin, borrowCoin }) const currentDebt: BigNumber = useMemo( - () => props.account.debts.find(byDenom(props.borrowAsset.denom)).amount || BN_ZERO, + () => props.account.debts.find(byDenom(props.borrowAsset.denom))?.amount || BN_ZERO, [props.account.debts, props.borrowAsset.denom], ) diff --git a/src/components/Modals/HLS/Manage/index.tsx b/src/components/Modals/HLS/Manage/index.tsx index d7a5be43..329fd5e3 100644 --- a/src/components/Modals/HLS/Manage/index.tsx +++ b/src/components/Modals/HLS/Manage/index.tsx @@ -20,7 +20,7 @@ export default function HlsManageModalController() { return ( + ) return null @@ -37,6 +42,7 @@ export default function HlsModalController() { interface Props { primaryAsset: Asset secondaryAsset: Asset + strategy?: HLSStrategy vaultAddress: string | null } @@ -57,6 +63,7 @@ function HlsModal(props: Props) { collateralAsset={props.primaryAsset} borrowAsset={props.secondaryAsset} vaultAddress={props.vaultAddress} + strategy={props.strategy} /> ) diff --git a/src/components/Modals/Vault/VaultBorrowings.tsx b/src/components/Modals/Vault/VaultBorrowings.tsx index 923f14e4..8faac873 100644 --- a/src/components/Modals/Vault/VaultBorrowings.tsx +++ b/src/components/Modals/Vault/VaultBorrowings.tsx @@ -6,7 +6,7 @@ import DepositCapMessage from 'components/DepositCapMessage' import DisplayCurrency from 'components/DisplayCurrency' import Divider from 'components/Divider' import { ArrowRight, ExclamationMarkCircled } from 'components/Icons' -import Slider from 'components/Slider' +import Index from 'components/Slider' import Text from 'components/Text' import TokenInput from 'components/TokenInput' import { BN_ZERO } from 'constants/math' @@ -192,7 +192,7 @@ export default function VaultBorrowings(props: VaultBorrowingsProps) { /> ) })} - {props.borrowings.length === 1 && } + {props.borrowings.length === 1 && } {props.borrowings.length === 0 && (
diff --git a/src/components/Routes.tsx b/src/components/Routes.tsx index df26dd94..74b64b7c 100644 --- a/src/components/Routes.tsx +++ b/src/components/Routes.tsx @@ -3,7 +3,6 @@ import { Navigate, Outlet, Route, Routes as RoutesWrapper } from 'react-router-d import Layout from 'pages/_layout' import BorrowPage from 'pages/BorrowPage' import FarmPage from 'pages/FarmPage' -import HLSFarmPage from 'pages/HLSFarmPage' import HLSStakingPage from 'pages/HLSStakingPage' import LendPage from 'pages/LendPage' import MobilePage from 'pages/MobilePage' @@ -28,7 +27,7 @@ export default function Routes() { } /> } /> } /> - } /> + {/*} />*/} } /> } /> @@ -36,11 +35,11 @@ export default function Routes() { } /> } /> } /> + } /> } /> - } /> - } /> + {/*} />*/} } /> } /> diff --git a/src/components/Slider.tsx b/src/components/Slider.tsx deleted file mode 100644 index 8b2208d3..00000000 --- a/src/components/Slider.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import classNames from 'classnames' -import { ChangeEvent, useRef, useState } from 'react' -import Draggable from 'react-draggable' - -import { OverlayMark } from 'components/Icons/index' -import useToggle from 'hooks/useToggle' - -type Props = { - value: number - onChange: (value: number) => void - className?: string - disabled?: boolean -} - -export default function Slider(props: Props) { - const [showTooltip, setShowTooltip] = useToggle() - const [sliderRect, setSliderRect] = useState({ width: 0, left: 0, right: 0 }) - const ref = useRef(null) - const nodeRef = useRef(null) - const [isDragging, setIsDragging] = useToggle() - - function handleSliderRect() { - const leftCap = ref.current?.getBoundingClientRect().left ?? 0 - const rightCap = ref.current?.getBoundingClientRect().right ?? 0 - const newSliderWidth = ref.current?.getBoundingClientRect().width ?? 0 - - if ( - sliderRect.width !== newSliderWidth || - leftCap !== sliderRect.left || - rightCap !== sliderRect.right - ) { - setSliderRect({ - left: leftCap, - right: rightCap, - width: newSliderWidth, - }) - } - } - - function handleDrag(e: any) { - if (!isDragging) { - setIsDragging(true) - } - - const current: number = e.clientX - - if (current < sliderRect.left) { - props.onChange(0) - return - } - - if (current > sliderRect.right) { - props.onChange(100) - return - } - - const value = Math.round(((current - sliderRect.left) / sliderRect.width) * 100) - - if (value !== props.value) { - props.onChange(value) - } - } - - function handleSliderClick(e: ChangeEvent) { - props.onChange(Number(e.target.value)) - } - - function handleShowTooltip() { - setShowTooltip(true) - } - - function handleHideTooltip() { - setShowTooltip(false) - } - - const DraggableElement: any = Draggable - - return ( -
- -
- - - - - - - - - -
- {!props.disabled && ( -
- setIsDragging(false)} - position={{ x: (sliderRect.width / 100) * props.value, y: 0 }} - > -
-
- {(showTooltip || isDragging) && ( -
- - {props.value.toFixed(0)}% -
- )} -
- -
- )} -
- ) -} - -interface MarkProps { - value: number - sliderValue: number - onClick: (value: number) => void - disabled?: boolean -} - -function Mark(props: MarkProps) { - return ( - - ) -} - -interface TrackProps { - maxValue: number - sliderValue: number -} - -function Track(props: TrackProps) { - const minValue = props.maxValue - 21 - let percentage = 0 - - if (props.sliderValue >= props.maxValue) percentage = 100 - - if (props.sliderValue > minValue && props.sliderValue < props.maxValue) { - percentage = ((props.sliderValue - minValue) / (props.maxValue - minValue)) * 100 - } - - return ( -
-
-
-
- ) -} diff --git a/src/components/Slider/LeverageLabel.tsx b/src/components/Slider/LeverageLabel.tsx new file mode 100644 index 00000000..3611429f --- /dev/null +++ b/src/components/Slider/LeverageLabel.tsx @@ -0,0 +1,26 @@ +import classNames from 'classnames' + +import Text from 'components/Text' + +interface Props { + className?: string + decimals: number + leverage: number + style?: {} +} + +export default function LeverageLabel(props: Props) { + return ( +
+
+ {props.leverage.toFixed(props.decimals)}x +
+ ) +} diff --git a/src/components/Slider/Mark.tsx b/src/components/Slider/Mark.tsx new file mode 100644 index 00000000..28b10d00 --- /dev/null +++ b/src/components/Slider/Mark.tsx @@ -0,0 +1,23 @@ +import classNames from 'classnames' + +interface Props { + disabled?: boolean + onClick: (value: number) => void + sliderValue: number + style?: {} + value: number +} + +export default function Mark(props: Props) { + return ( +