Hls leverage (#628)

* 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
This commit is contained in:
Bob van der Helm 2023-11-10 17:20:48 +01:00 committed by GitHub
parent cc3b0eee20
commit 1ecd80dac2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 610 additions and 255 deletions

View File

@ -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
})
})

View File

@ -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 <DropDownButton items={ITEMS} text='Manage' color='tertiary' />

View File

@ -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() {

View File

@ -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 (
<div className='p-4 flex-col gap-6 flex'>
<div className='flex-col gap-6 flex justify-between h-full p-4'>
<TokenInputWithSlider
amount={props.amount}
asset={props.asset}
max={props.max}
onChange={props.onChangeAmount}
maxText='Max borrow'
leverage={{
current: props.leverage,
max: props.maxLeverage,
}}
/>
<LeverageSummary asset={props.asset} positionValue={props.positionValue} />
<Button onClick={props.onClickBtn} text='Continue' rightIcon={<ArrowRight />} />
<div className='flex flex-col gap-6'>
<LeverageSummary asset={props.asset} positionValue={props.positionValue} />
<Button onClick={props.onClickBtn} text='Continue' rightIcon={<ArrowRight />} />
</div>
</div>
)
}

View File

@ -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 (
<StakingContent
walletCollateralAsset={walletCollateralAsset}
collateralAsset={props.collateralAsset}
borrowAsset={props.borrowAsset}
emptyHlsAccounts={emptyHlsAccounts}
hlsAccounts={hlsAccounts}
isOpen={isOpen}
selectedAccount={selectedAccount}
setSelectedAccount={setSelectedAccount}
toggleIsOpen={toggleIsOpen}
/>
)
if (props.strategy) {
return (
<StakingContent
walletCollateralAsset={walletCollateralAsset}
collateralAsset={props.collateralAsset}
borrowAsset={props.borrowAsset}
emptyHlsAccounts={emptyHlsAccounts}
hlsAccounts={hlsAccounts}
isOpen={isOpen}
selectedAccount={selectedAccount}
setSelectedAccount={setSelectedAccount}
toggleIsOpen={toggleIsOpen}
strategy={props.strategy}
/>
)
}
return null
}
interface ContentProps {
@ -120,7 +126,11 @@ function Vault(props: VaultContentProps) {
return <Accordion className='h-[546px] overflow-y-scroll scrollbar-hide' items={items} />
}
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,
})

View File

@ -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: () => (
<Leverage
leverage={props.leverage}
amount={props.borrowAmount}
asset={props.borrowAsset}
onChangeAmount={props.onChangeDebt}
onClickBtn={() => props.toggleIsOpen(2)}
max={props.maxBorrowAmount}
positionValue={props.positionValue}
maxLeverage={props.strategy?.maxLeverage || 1}
/>
),
renderSubTitle: () => (

View File

@ -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,

View File

@ -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<number>(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 (
<>
<TokenInputWithSlider
amount={currentDebt}
asset={props.borrowAsset}
max={maxBorrowAmount}
onChange={onChangeAmount}
maxText='Max borrow'
leverage={{
current: leverage,
max: props.account.strategy.maxLeverage,
}}
/>
<div className='flex flex-col gap-6'>
<LeverageSummary asset={props.borrowAsset} positionValue={positionValue} />
<Button onClick={handleOnClick} text='Confirm' />
</div>
</>
)
}

View File

@ -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],
)

View File

@ -20,7 +20,7 @@ export default function HlsManageModalController() {
return (
<HlsModal
account={account}
account={{ ...account, strategy: modal.staking.strategy } as HLSAccountWithStrategy}
action={modal.staking.action}
collateralAsset={collateralAsset}
borrowAsset={borrowAsset}
@ -29,7 +29,7 @@ export default function HlsManageModalController() {
}
interface Props {
account: Account
account: HLSAccountWithStrategy
action: HlsStakingManageAction
borrowAsset: Asset
collateralAsset: Asset

View File

@ -28,7 +28,12 @@ export default function HlsModalController() {
)
if (modal?.strategy)
return (
<HlsModal primaryAsset={primaryAsset} secondaryAsset={secondaryAsset} vaultAddress={null} />
<HlsModal
primaryAsset={primaryAsset}
secondaryAsset={secondaryAsset}
strategy={modal.strategy}
vaultAddress={null}
/>
)
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}
/>
</Modal>
)

View File

@ -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 && <Slider onChange={onChangeSlider} value={percentage} />}
{props.borrowings.length === 1 && <Index onChange={onChangeSlider} value={percentage} />}
{props.borrowings.length === 0 && (
<div className='flex items-center gap-4 py-2'>
<div className='w-4'>

View File

@ -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() {
<Route path='/portfolio' element={<PortfolioPage />} />
<Route path='/mobile' element={<MobilePage />} />
<Route path='/hls-staking' element={<HLSStakingPage />} />
<Route path='/hls-farm' element={<HLSFarmPage />} />
{/*<Route path='/hls-farm' element={<HLSFarmPage />} />*/}
<Route path='/' element={<TradePage />} />
<Route path='/wallets/:address'>
<Route path='trade' element={<TradePage />} />
@ -36,11 +35,11 @@ export default function Routes() {
<Route path='lend' element={<LendPage />} />
<Route path='borrow' element={<BorrowPage />} />
<Route path='portfolio' element={<PortfolioPage />} />
<Route path='hls-staking' element={<HLSStakingPage />} />
<Route path='portfolio/:accountId'>
<Route path='' element={<PortfolioAccountPage />} />
</Route>
<Route path='hls-staking' element={<HLSStakingPage />} />
<Route path='hls-farm' element={<HLSFarmPage />} />
{/*<Route path='hls-farm' element={<HLSFarmPage />} />*/}
<Route path='' element={<TradePage />} />
</Route>
<Route path='*' element={<Navigate to='/' />} />

View File

@ -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<HTMLDivElement>(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<HTMLInputElement>) {
props.onChange(Number(e.target.value))
}
function handleShowTooltip() {
setShowTooltip(true)
}
function handleHideTooltip() {
setShowTooltip(false)
}
const DraggableElement: any = Draggable
return (
<div
ref={ref}
className={classNames(
'relative min-h-3 w-full transition-opacity',
props.className,
props.disabled && 'pointer-events-none',
)}
onMouseEnter={handleSliderRect}
>
<input
type='range'
value={props.value}
onChange={handleSliderClick}
onMouseDown={handleShowTooltip}
className='absolute z-2 w-full hover:cursor-pointer appearance-none bg-transparent [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:appearance-none'
/>
<div className='absolute flex items-center w-full gap-1'>
<Mark
onClick={props.onChange}
value={0}
sliderValue={props.value}
disabled={props.disabled}
/>
<Track maxValue={23} sliderValue={props.value} />
<Mark
onClick={props.onChange}
value={25}
sliderValue={props.value}
disabled={props.disabled}
/>
<Track maxValue={48} sliderValue={props.value} />
<Mark
onClick={props.onChange}
value={50}
sliderValue={props.value}
disabled={props.disabled}
/>
<Track maxValue={73} sliderValue={props.value} />
<Mark
onClick={props.onChange}
value={75}
sliderValue={props.value}
disabled={props.disabled}
/>
<Track maxValue={98} sliderValue={props.value} />
<Mark
onClick={props.onChange}
value={100}
sliderValue={props.value}
disabled={props.disabled}
/>
</div>
{!props.disabled && (
<div onMouseEnter={handleShowTooltip} onMouseLeave={handleHideTooltip}>
<DraggableElement
nodeRef={nodeRef}
axis='x'
grid={[sliderRect.width / 100, 0]}
bounds={{ left: 0, right: sliderRect.width }}
positionOffset={{ x: (props.value / 100) * -12, y: 0 }}
onDrag={handleDrag}
onStop={() => setIsDragging(false)}
position={{ x: (sliderRect.width / 100) * props.value, y: 0 }}
>
<div ref={nodeRef} className='absolute z-20 leading-3'>
<div
className={
'z-20 h-3 w-3 rotate-45 hover:cursor-pointer rounded-xs border-[2px] border-white bg-martian-red'
}
/>
{(showTooltip || isDragging) && (
<div className='absolute -top-8 left-1/2 -translate-x-1/2 rounded-xs bg-martian-red px-2 py-[2px] text-xs'>
<OverlayMark className='absolute h-2 -translate-x-1/2 -bottom-2 left-1/2 -z-1 text-martian-red' />
{props.value.toFixed(0)}%
</div>
)}
</div>
</DraggableElement>
</div>
)}
</div>
)
}
interface MarkProps {
value: number
sliderValue: number
onClick: (value: number) => void
disabled?: boolean
}
function Mark(props: MarkProps) {
return (
<button
onClick={() => props.onClick(props.value)}
className={`z-20 h-3 w-3 rotate-45 rounded-xs border-[1px] border-white/20 hover:border-[2px] hover:border-white ${
props.sliderValue >= props.value && !props.disabled
? 'bg-martian-red hover:border-white'
: 'bg-grey-medium'
}`}
disabled={props.disabled}
></button>
)
}
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 (
<div className='relative flex-1 h-1 overflow-hidden bg-transparent rounded-sm'>
<div className='absolute h-3 z-1 bg-martian-red ' style={{ width: `${percentage}%` }} />
<div className='absolute w-full h-3 bg-white/20' />
</div>
)
}

View File

@ -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 (
<div
className={classNames(
'flex flex-col gap-1 items-center',
'transition-opacity duration-100',
props.className,
)}
style={props.style}
>
<div className={classNames('h-2.5 w-[1px] border-[0.5px] border-white/20')} />
<Text className='text-xs text-white/50'>{props.leverage.toFixed(props.decimals)}x</Text>
</div>
)
}

View File

@ -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 (
<button
onClick={() => props.onClick(props.value)}
className={classNames(
'z-20 h-2 w-2 rotate-45 rounded-xs border-[1px] border-white/20 hover:border-[1px] hover:border-white',
(props.sliderValue < props.value || props.disabled) && '!bg-grey-medium',
)}
style={props.style}
disabled={props.disabled}
/>
)
}

View File

@ -0,0 +1,32 @@
import classNames from 'classnames'
interface Props {
bg: string
maxValue: number
sliderValue: number
}
export default function Track(props: Props) {
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 (
<div className='relative flex-1 h-1 bg-white/20 rounded-sm w-1'>
<div
className={classNames(
'h-1 z-1 rounded-sm w-1',
'before:absolute',
'before:top-0 before:bottom-0 before:right-0 before:left-0',
percentage > 0 && props.bg,
percentage > 0 && 'slider-mask',
)}
style={{ width: `${percentage}%` }}
/>
</div>
)
}

View File

@ -0,0 +1,222 @@
import classNames from 'classnames'
import { ChangeEvent, useRef, useState } from 'react'
import Draggable from 'react-draggable'
import { OverlayMark } from 'components/Icons'
import LeverageLabel from 'components/Slider/LeverageLabel'
import Mark from 'components/Slider/Mark'
import Track from 'components/Slider/Track'
import useToggle from 'hooks/useToggle'
const colors = {
'1': '#897E83',
'2': '#BD8898',
'3': '#DB83A5',
'4': '#B5469B',
'5': '#920D92',
}
type Props = {
value: number
onChange: (value: number) => void
leverage?: {
current: number
max: number
}
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<HTMLDivElement>(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<HTMLInputElement>) {
props.onChange(Number(e.target.value))
}
function handleShowTooltip() {
setShowTooltip(true)
}
function handleHideTooltip() {
setShowTooltip(false)
}
function getActiveIndex() {
if (props.value >= 100) return '5'
if (props.value >= 75) return '4'
if (props.value >= 50) return '3'
if (props.value >= 25) return '2'
return '1'
}
const DraggableElement: any = Draggable
return (
<div>
<div
ref={ref}
className={classNames(
'relative min-h-3 w-full transition-opacity',
props.className,
props.disabled && 'pointer-events-none',
)}
onMouseEnter={handleSliderRect}
>
<input
type='range'
value={props.value}
onChange={handleSliderClick}
onMouseDown={handleShowTooltip}
className='absolute z-2 w-full hover:cursor-pointer appearance-none bg-transparent [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:appearance-none'
/>
<div className='absolute flex items-center w-full gap-1.5'>
<Mark
onClick={props.onChange}
value={0}
sliderValue={props.value}
disabled={props.disabled}
style={{ backgroundColor: colors['1'] }}
/>
<Track maxValue={23} sliderValue={props.value} bg='before:gradient-slider-1' />
<Mark
onClick={props.onChange}
value={25}
sliderValue={props.value}
disabled={props.disabled}
style={{ backgroundColor: colors['2'] }}
/>
<Track maxValue={48} sliderValue={props.value} bg='before:gradient-slider-2' />
<Mark
onClick={props.onChange}
value={50}
sliderValue={props.value}
disabled={props.disabled}
style={{ backgroundColor: colors['3'] }}
/>
<Track maxValue={73} sliderValue={props.value} bg='before:gradient-slider-3' />
<Mark
onClick={props.onChange}
value={75}
sliderValue={props.value}
disabled={props.disabled}
style={{ backgroundColor: colors['4'] }}
/>
<Track maxValue={98} sliderValue={props.value} bg='before:gradient-slider-4' />
<Mark
onClick={props.onChange}
value={100}
sliderValue={props.value}
disabled={props.disabled}
style={{ backgroundColor: colors['5'] }}
/>
</div>
{!props.disabled && (
<div onMouseEnter={handleShowTooltip} onMouseLeave={handleHideTooltip}>
<DraggableElement
nodeRef={nodeRef}
axis='x'
grid={[sliderRect.width / 100, 0]}
bounds={{ left: 0, right: sliderRect.width }}
positionOffset={{ x: (props.value / 100) * -12, y: 0 }}
onDrag={handleDrag}
onStop={() => setIsDragging(false)}
position={{ x: (sliderRect.width / 100) * props.value, y: -2 }}
>
<div ref={nodeRef} className='absolute z-20 leading-3'>
<div
className={classNames(
'z-20 h-3 w-3 rotate-45 hover:cursor-pointer rounded-xs border-[2px] border-white',
)}
style={{ background: colors[getActiveIndex()] }}
/>
{props.leverage ? (
<div className='pt-2.5'>
<LeverageLabel
leverage={props.leverage.current}
decimals={1}
className={props.leverage.current >= 10 ? '-translate-x-2' : '-translate-x-1'}
/>
</div>
) : (
(showTooltip || isDragging) && (
<div className='absolute -top-8 left-1/2 -translate-x-1/2 rounded-xs bg-fuchsia px-2 py-[2px] text-xs'>
<OverlayMark
className={classNames(
'absolute h-2 -translate-x-1/2 -bottom-2 left-1/2 -z-1 text-fuchsia',
)}
/>
{props.value.toFixed(0)}%
</div>
)
)}
</div>
</DraggableElement>
</div>
)}
</div>
{props.leverage && (
<div className='pt-2 flex justify-between'>
<LeverageLabel
leverage={1}
decimals={0}
className='-translate-x-0.5'
style={{ opacity: props.value < 5 ? 0 : 1 }}
/>
<LeverageLabel
leverage={props.leverage.max || 1}
decimals={0}
className='translate-x-1.5'
style={{ opacity: props.value > 95 ? 0 : 1 }}
/>
</div>
)}
</div>
)
}

View File

@ -19,6 +19,10 @@ interface Props {
hasSelect?: boolean
maxText?: string
onChangeAsset?: (asset: Asset) => void
leverage?: {
current: number
max: number
}
}
export default function TokenInputWithSlider(props: Props) {
@ -71,6 +75,7 @@ export default function TokenInputWithSlider(props: Props) {
value={percentage || 0}
onChange={(value) => onChangeSlider(value)}
disabled={props.disabled}
leverage={props.leverage}
/>
</div>
)

View File

@ -44,7 +44,8 @@ export const Tooltip = (props: Props) => {
className={props.contentClassName}
/>
)}
{...props}
onClickOutside={props.onClickOutside}
visible={props.visible}
>
{props.children ? (
<span

View File

@ -14,7 +14,7 @@ import {
import useVaults from 'hooks/useVaults'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { cloneAccount } from 'utils/accounts'
import { calculateAccountLeverage, cloneAccount } from 'utils/accounts'
import { byDenom } from 'utils/array'
import { getCoinAmount, getCoinValue } from 'utils/formatters'
import { getValueFromBNCoins } from 'utils/helpers'
@ -30,6 +30,7 @@ export function useUpdatedAccount(account?: Account) {
const [updatedAccount, setUpdatedAccount] = useState<Account | undefined>(
account ? cloneAccount(account) : undefined,
)
const [slippage] = useLocalStorage<number>(LocalStorageKeys.SLIPPAGE, DEFAULT_SETTINGS.slippage)
const [addedDeposits, addDeposits] = useState<BNCoin[]>([])
const [removedDeposits, removeDeposits] = useState<BNCoin[]>([])
@ -39,7 +40,7 @@ export function useUpdatedAccount(account?: Account) {
const [addedLends, addLends] = useState<BNCoin[]>([])
const [removedLends, removeLends] = useState<BNCoin[]>([])
const [addedTrades, addTrades] = useState<BNCoin[]>([])
const [leverage, setLeverage] = useState<number>(0)
const removeDepositAndLendsByDenom = useCallback(
(denom: string) => {
if (!account) return
@ -165,6 +166,16 @@ export function useUpdatedAccount(account?: Account) {
[prices],
)
const simulateHlsStakingWithdraw = useCallback(
(collateralDenom: string, debtDenom: string, repayAmount: BigNumber) => {
const repayValue = getCoinValue(BNCoin.fromDenomAndBigNumber(debtDenom, repayAmount), prices)
const removeDepositAmount = getCoinAmount(collateralDenom, repayValue, prices)
removeDeposits([BNCoin.fromDenomAndBigNumber(collateralDenom, removeDepositAmount)])
removeDebts([BNCoin.fromDenomAndBigNumber(debtDenom, repayAmount)])
},
[prices],
)
const simulateVaultDeposit = useCallback(
(address: string, coins: BNCoin[], borrowCoins: BNCoin[]) => {
if (!account) return
@ -205,6 +216,7 @@ export function useUpdatedAccount(account?: Account) {
accountCopy.lends = addCoins(addedLends, [...accountCopy.lends])
accountCopy.lends = removeCoins(removedLends, [...accountCopy.lends])
setUpdatedAccount(accountCopy)
setLeverage(calculateAccountLeverage(accountCopy, prices).toNumber())
useStore.setState({ updatedAccount: accountCopy })
return () => useStore.setState({ updatedAccount: undefined })
@ -235,12 +247,14 @@ export function useUpdatedAccount(account?: Account) {
addedDeposits,
addedDebts,
addedLends,
leverage,
removedDeposits,
removedDebts,
removedLends,
simulateBorrow,
simulateDeposits,
simulateHlsStakingDeposit,
simulateHlsStakingWithdraw,
simulateLending,
simulateRepay,
simulateTrade,

View File

@ -1,15 +1,13 @@
import Tab from 'components/Earn/Tab'
import ActiveStakingAccounts from 'components/HLS/Staking/ActiveStakingAccounts'
import AvailableHlsStakingAssets from 'components/HLS/Staking/AvailableHLSStakingAssets'
import HLSStakingIntro from 'components/HLS/Staking/HLSStakingIntro'
import MigrationBanner from 'components/MigrationBanner'
import { HLS_TABS } from 'constants/pages'
export default function HLSStakingPage() {
return (
<div className='flex flex-wrap w-full gap-6'>
<MigrationBanner />
<Tab tabs={HLS_TABS} activeTabIdx={1} />
{/*<Tab tabs={HLS_TABS} activeTabIdx={1} />*/}
<HLSStakingIntro />
<AvailableHlsStakingAssets />
<ActiveStakingAccounts />

View File

@ -232,6 +232,28 @@ export default function createBroadcastSlice(
return response.then((response) => !!response.result)
},
changeHlsStakingLeverage: async (options: { accountId: string; actions: Action[] }) => {
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
account_id: options.accountId,
actions: options.actions,
},
}
const response = get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
})
get().setToast({
response,
options: {
action: 'deposit',
message: `Changed Leverage`,
},
})
return response.then((response) => !!response.result)
},
closeHlsStakingPosition: async (options: { accountId: string; actions: Action[] }) => {
const msg: CreditManagerExecuteMsg = {
update_credit_account: {
@ -252,10 +274,7 @@ export default function createBroadcastSlice(
},
})
const response_1 = await response
return response_1.result
? getSingleValueFromBroadcastResult(response_1.result, 'wasm', 'token_id')
: null
return response.then((response) => !!response.result)
},
createAccount: async (accountKind: AccountKind) => {

View File

@ -91,11 +91,9 @@ interface BroadcastSlice {
coin: BNCoin
borrowToWallet: boolean
}) => Promise<boolean>
changeHlsStakingLeverage: (options: { accountId: string; actions: Action[] }) => Promise<boolean>
claimRewards: (options: { accountId: string }) => ExecutableTx
closeHlsStakingPosition: (options: {
accountId: string
actions: Action[]
}) => Promise<string | null>
closeHlsStakingPosition: (options: { accountId: string; actions: Action[] }) => Promise<boolean>
createAccount: (
accountKind: import('types/generated/mars-rover-health-types/MarsRoverHealthTypes.types').AccountKind,
) => Promise<string | null>

68
src/utils/actions.ts Normal file
View File

@ -0,0 +1,68 @@
import { BN_ZERO } from 'constants/math'
import { BNCoin } from 'types/classes/BNCoin'
import { Action } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import { getCoinAmount, getCoinValue } from 'utils/formatters'
export function getHlsStakingChangeLevActions(
previousAmount: BigNumber,
currentAmount: BigNumber,
collateralDenom: string,
borrowDenom: string,
slippage: number,
prices: BNCoin[],
): Action[] {
let actions: Action[] = []
if (currentAmount.isLessThan(previousAmount)) {
const debtValue = getCoinValue(
BNCoin.fromDenomAndBigNumber(borrowDenom, previousAmount.minus(currentAmount)),
prices,
)
const collateralAmount = getCoinAmount(collateralDenom, debtValue, prices)
actions = [
{
swap_exact_in: {
coin_in: BNCoin.fromDenomAndBigNumber(collateralDenom, collateralAmount).toActionCoin(),
denom_out: borrowDenom,
slippage: slippage.toString(),
},
},
{
repay: {
coin: BNCoin.fromDenomAndBigNumber(
borrowDenom,
previousAmount
.minus(currentAmount)
.times(1 - slippage)
.integerValue(),
).toActionCoin(),
},
},
{
withdraw: BNCoin.fromDenomAndBigNumber(borrowDenom, BN_ZERO).toActionCoin(true),
},
]
} else {
actions = [
{
borrow: BNCoin.fromDenomAndBigNumber(
borrowDenom,
currentAmount.minus(previousAmount),
).toCoin(),
},
{
swap_exact_in: {
denom_out: collateralDenom,
coin_in: BNCoin.fromDenomAndBigNumber(
borrowDenom,
currentAmount.minus(previousAmount),
).toActionCoin(true),
slippage: slippage.toString(),
},
},
]
}
return actions
}

View File

@ -88,6 +88,7 @@ module.exports = {
chart: '#220e1d',
error: '#F04438',
'error-bg': '#FDA29B',
fuchsia: '#B7439F',
green: '#039855',
grey: '#908e91',
'grey-dark': '#1a1c25',
@ -352,6 +353,18 @@ module.exports = {
'.gradient-secondary-to-primary': {
background: 'linear-gradient(180deg, #926AC8 100%, #7F78E8 0%)',
},
'.gradient-slider-1': {
background: 'linear-gradient(to right, #8D7F85, #B78796)',
},
'.gradient-slider-2': {
background: 'linear-gradient(to right, #C08899, #E08AA6)',
},
'.gradient-slider-3': {
background: 'linear-gradient(to right, #D97FA4, #B84A9C)',
},
'.gradient-slider-4': {
background: 'linear-gradient(to right, #961293, #B3419B)',
},
'.gradient-tooltip': {
background:
'linear-gradient(77.47deg, rgba(20, 24, 57, 0.9) 11.58%, rgba(34, 16, 57, 0.9) 93.89%)',
@ -364,6 +377,9 @@ module.exports = {
whiteSpace: 'nowrap',
fontFeatureSettings: '"tnum" on',
},
'.slider-mask': {
mask: 'linear-gradient(#fff 0 0)',
},
'.text-3xs': { fontSize: '9px', lineHeight: '12px' },
'.text-3xs-caps': {
fontSize: '9px',