Hls staking manage actions (#622)

* 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
This commit is contained in:
Bob van der Helm 2023-11-08 13:05:39 +01:00 committed by GitHub
parent 4ec95c885c
commit 7439bea0d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 805 additions and 122 deletions

View File

@ -13,12 +13,10 @@ export default async function getHLSStakingAccounts(
const hlsAccountsWithStrategy: HLSAccountWithStrategy[] = []
activeAccounts.forEach((account) => {
if (account.deposits.length === 0 || account.debts.length === 0) return
if (account.deposits.length === 0) return
const strategy = hlsStrategies.find(
(strategy) =>
strategy.denoms.deposit === account.deposits.at(0).denom &&
strategy.denoms.borrow === account.debts.at(0).denom,
(strategy) => strategy.denoms.deposit === account.deposits.at(0).denom,
)
if (!strategy) return

View File

@ -1,4 +1,4 @@
import React, { useMemo } from 'react'
import React, { ReactElement, useMemo } from 'react'
import { CircularProgress } from 'components/CircularProgress'
import Text from 'components/Text'
@ -8,7 +8,7 @@ import { BN } from 'utils/helpers'
interface Props {
health: number
healthFactor: number
children: React.ReactNode
children: ReactElement
}
function HealthTooltipContent({ health, healthFactor }: { health: number; healthFactor: number }) {

View File

@ -2,6 +2,7 @@ import Button from 'components/Button'
import { ChevronDown } from 'components/Icons'
import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip'
import useToggle from 'hooks/useToggle'
interface Props extends ButtonProps {
items: DropDownItem[]
@ -9,6 +10,7 @@ interface Props extends ButtonProps {
}
export default function DropDownButton(props: Props) {
const [isOpen, toggleIsOpen] = useToggle(false)
return (
<Tooltip
content={<DropDown {...props} />}
@ -17,8 +19,15 @@ export default function DropDownButton(props: Props) {
contentClassName='!bg-white/10 border border-white/20 backdrop-blur-xl !p-0'
interactive
hideArrow
visible={isOpen}
onClickOutside={() => toggleIsOpen(false)}
>
<Button rightIcon={<ChevronDown />} iconClassName='w-3 h-3' {...props} />
<Button
onClick={() => toggleIsOpen()}
rightIcon={<ChevronDown />}
iconClassName='w-3 h-3'
{...props}
/>
</Tooltip>
)
}

View File

@ -13,13 +13,9 @@ interface Props {
export default function Checkbox(props: Props) {
return (
<>
<label
className='flex items-center gap-2 border-white cursor-pointer'
htmlFor={`${props.name}-id}`}
>
<label className='flex items-center gap-2 border-white cursor-pointer'>
<input
onChange={() => props.onChange(props.checked)}
id={`${props.name}-id`}
name={props.name}
checked={props.checked}
type='checkbox'

View File

@ -2,7 +2,7 @@ import classNames from 'classnames'
import { useCallback } from 'react'
import ActionButton from 'components/Button/ActionButton'
import { Enter } from 'components/Icons'
import { Circle, Enter, TrashBin, Wallet } from 'components/Icons'
import Loading from 'components/Loading'
import Text from 'components/Text'
import { LocalStorageKeys } from 'constants/localStorageKeys'
@ -91,18 +91,18 @@ export default function Deposit(props: Props) {
const INFO_ITEMS = [
{
icon: <Enter width={16} height={16} />,
icon: <Circle />,
title: 'One account, one position',
description:
'A minted HLS account can only have a single position tied to it, in order to limit risk.',
},
{
icon: <Enter />,
icon: <Wallet />,
title: 'Funded from your wallet',
description: 'To fund your HLS position, funds will have to come directly from your wallet.',
},
{
icon: <Enter />,
icon: <TrashBin />,
title: 'Accounts are reusable',
description:
'If you exited a position from a minted account, this account can be reused for a new position.',

View File

@ -1,7 +1,8 @@
import React, { useCallback, useMemo } from 'react'
import DropDownButton from 'components/Button/DropDownButton'
import { ArrowDownLine, HandCoins, Plus } from 'components/Icons'
import { ArrowDownLine, Cross, HandCoins, Plus, Scale } from 'components/Icons'
import useCloseHlsStakingPosition from 'hooks/HLS/useClosePositionActions'
import useStore from 'store'
export const MANAGE_META = { id: 'manage' }
@ -12,32 +13,55 @@ interface Props {
export default function Manage(props: Props) {
const openModal = useCallback(
(action: 'deposit' | 'withdraw' | 'repay') =>
(action: HlsStakingManageAction) =>
useStore.setState({
hlsManageModal: { staking: { strategy: props.account.strategy, action } },
hlsManageModal: {
accountId: props.account.id,
staking: { strategy: props.account.strategy, action },
},
}),
[props.account.strategy],
[props.account.id, props.account.strategy],
)
const actions = useCloseHlsStakingPosition({ account: props.account })
const closeHlsStakingPosition = useStore((s) => s.closeHlsStakingPosition)
const hasNoDebt = useMemo(() => props.account.debts.length === 0, [props.account.debts.length])
const ITEMS: DropDownItem[] = useMemo(
() => [
{
icon: <Scale width={16} />,
text: 'Change leverage',
onClick: () => openModal('leverage'),
},
{
icon: <Plus width={16} />,
text: 'Deposit more',
onClick: () => openModal('deposit'),
},
...(hasNoDebt
? []
: [
{
icon: <HandCoins width={16} />,
text: 'Repay',
onClick: () => openModal('repay'),
},
]),
{
icon: <HandCoins />,
text: 'Repay',
onClick: () => openModal('repay'),
},
{
icon: <ArrowDownLine />,
icon: <ArrowDownLine width={16} />,
text: 'Withdraw',
onClick: () => openModal('withdraw'),
},
{
icon: <Cross width={14} />,
text: 'Close Position',
onClick: () => closeHlsStakingPosition({ accountId: props.account.id, actions }),
},
],
[openModal],
[actions, closeHlsStakingPosition, openModal, props.account.id],
)
return <DropDownButton items={ITEMS} text='Manage' color='tertiary' />

View File

@ -0,0 +1,10 @@
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5007_284)">
<path d="M8.00016 14.6654C11.6821 14.6654 14.6668 11.6806 14.6668 7.9987C14.6668 4.3168 11.6821 1.33203 8.00016 1.33203C4.31826 1.33203 1.3335 4.3168 1.3335 7.9987C1.3335 11.6806 4.31826 14.6654 8.00016 14.6654Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_5007_284">
<rect width="16" height="16" fill="currentColor"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 567 B

View File

@ -0,0 +1,5 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="scales-02">
<path id="Icon" d="M2.50047 13H8.50047M15.5005 13H21.5005M12.0005 7V21M12.0005 7C13.3812 7 14.5005 5.88071 14.5005 4.5M12.0005 7C10.6198 7 9.50047 5.88071 9.50047 4.5M4.00047 21L20.0005 21M4.00047 4.50001L9.50047 4.5M9.50047 4.5C9.50047 3.11929 10.6198 2 12.0005 2C13.3812 2 14.5005 3.11929 14.5005 4.5M14.5005 4.5L20.0005 4.5M8.88091 14.3364C8.48022 15.8706 7.11858 17 5.50047 17C3.88237 17 2.52073 15.8706 2.12004 14.3364C2.0873 14.211 2.07093 14.1483 2.06935 13.8979C2.06838 13.7443 2.12544 13.3904 2.17459 13.2449C2.25478 13.0076 2.34158 12.8737 2.51519 12.6059L5.50047 8L8.48576 12.6059C8.65937 12.8737 8.74617 13.0076 8.82636 13.2449C8.87551 13.3904 8.93257 13.7443 8.9316 13.8979C8.93002 14.1483 8.91365 14.211 8.88091 14.3364ZM21.8809 14.3364C21.4802 15.8706 20.1186 17 18.5005 17C16.8824 17 15.5207 15.8706 15.12 14.3364C15.0873 14.211 15.0709 14.1483 15.0693 13.8979C15.0684 13.7443 15.1254 13.3904 15.1746 13.2449C15.2548 13.0076 15.3416 12.8737 15.5152 12.6059L18.5005 8L21.4858 12.6059C21.6594 12.8737 21.7462 13.0076 21.8264 13.2449C21.8755 13.3904 21.9326 13.7443 21.9316 13.8979C21.93 14.1483 21.9137 14.211 21.8809 14.3364Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -14,6 +14,7 @@ export { default as ChevronDown } from 'components/Icons/ChevronDown.svg'
export { default as ChevronLeft } from 'components/Icons/ChevronLeft.svg'
export { default as ChevronRight } from 'components/Icons/ChevronRight.svg'
export { default as ChevronUp } from 'components/Icons/ChevronUp.svg'
export { default as Circle } from 'components/Icons/Circle.svg'
export { default as Compass } from 'components/Icons/Compass.svg'
export { default as Copy } from 'components/Icons/Copy.svg'
export { default as Cross } from 'components/Icons/Cross.svg'
@ -43,6 +44,7 @@ export { default as PlusCircled } from 'components/Icons/PlusCircled.svg'
export { default as PlusSquared } from 'components/Icons/PlusSquared.svg'
export { default as Questionmark } from 'components/Icons/Questionmark.svg'
export { default as ReceiptCheck } from 'components/Icons/ReceiptCheck.svg'
export { default as Scale } from 'components/Icons/Scale.svg'
export { default as Search } from 'components/Icons/Search.svg'
export { default as Shield } from 'components/Icons/Shield.svg'
export { default as SortAsc } from 'components/Icons/SortAsc.svg'

View File

@ -4,7 +4,7 @@ import { ReactNode, useEffect, useRef } from 'react'
import EscButton from 'components/Button/EscButton'
import Card from 'components/Card'
interface Props {
export interface ModalProps {
header: string | ReactNode
headerClassName?: string
hideCloseBtn?: boolean
@ -18,7 +18,7 @@ interface Props {
dialogId?: string
}
export default function Modal(props: Props) {
export default function Modal(props: ModalProps) {
const ref: React.RefObject<HTMLDialogElement> = useRef(null)
const modalClassName = props.modalClassName ?? 'max-w-modal'

View File

@ -75,7 +75,7 @@ function AlertDialog(props: Props) {
)}
{checkbox && (
<Checkbox
name='aleart-toggle'
name='hls-info-toggle'
checked={toggle}
onChange={handleCheckboxClick}
text={checkbox.text}

View File

@ -2,7 +2,7 @@ import React from 'react'
import Button from 'components/Button'
import { ArrowRight } from 'components/Icons'
import LeverageSummary from 'components/Modals/HLS/LeverageSummary'
import LeverageSummary from 'components/Modals/HLS/Deposit/LeverageSummary'
import TokenInputWithSlider from 'components/TokenInput/TokenInputWithSlider'
interface Props {

View File

@ -1,7 +1,6 @@
import React, { useMemo } from 'react'
import { FormattedNumber } from 'components/FormattedNumber'
import Text from 'components/Text'
import SummaryItems from 'components/SummaryItems'
import useBorrowAsset from 'hooks/useBorrowAsset'
interface Props {
@ -12,7 +11,7 @@ interface Props {
export default function LeverageSummary(props: Props) {
const borrowAsset = useBorrowAsset(props.asset.denom)
const items: { title: string; amount: number; options: FormatOptions }[] = useMemo(() => {
const items: SummaryItem[] = useMemo(() => {
return [
// TODO: Get APY numbers
{
@ -33,18 +32,5 @@ export default function LeverageSummary(props: Props) {
]
}, [borrowAsset?.borrowRate, props.asset.symbol, props.positionValue])
return (
<div className='grid grid-cols-2 gap-2'>
{items.map((item) => (
<React.Fragment key={item.title}>
<Text className='text-white/60 text-xs'>{item.title}</Text>
<FormattedNumber
className='place-self-end text-xs'
amount={item.amount}
options={item.options}
/>
</React.Fragment>
))}
</div>
)
return <SummaryItems items={items} />
}

View File

@ -3,7 +3,7 @@ import React from 'react'
import AmountAndValue from 'components/AmountAndValue'
import AssetImage from 'components/Asset/AssetImage'
import { FormattedNumber } from 'components/FormattedNumber'
import Container from 'components/Modals/HLS/Summary/Container'
import Container from 'components/Modals/HLS/Deposit/Summary/Container'
import Text from 'components/Text'
interface Props {

View File

@ -3,8 +3,8 @@ import React, { useMemo } from 'react'
import DisplayCurrency from 'components/DisplayCurrency'
import { FormattedNumber } from 'components/FormattedNumber'
import { InfoCircle } from 'components/Icons'
import AprBreakdown from 'components/Modals/HLS/Summary/ApyBreakdown'
import Container from 'components/Modals/HLS/Summary/Container'
import AprBreakdown from 'components/Modals/HLS/Deposit/Summary/ApyBreakdown'
import Container from 'components/Modals/HLS/Deposit/Summary/Container'
import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip'
import { BNCoin } from 'types/classes/BNCoin'
@ -63,8 +63,10 @@ export default function YourPosition(props: Props) {
type='info'
className='items-center flex gap-2 group-hover/apytooltip:text-white text-white/60 cursor-pointer'
>
<span className='mt-0.5'>Net APY</span>{' '}
<InfoCircle className='w-4 h-4 text-white/40 inline group-hover/apytooltip:text-white transition-all' />
<>
<span className='mt-0.5'>Net APY</span>{' '}
<InfoCircle className='w-4 h-4 text-white/40 inline group-hover/apytooltip:text-white transition-all' />
</>
</Tooltip>
</Text>
<FormattedNumber

View File

@ -2,8 +2,8 @@ import React from 'react'
import Button from 'components/Button'
import { ArrowRight } from 'components/Icons'
import AssetSummary from 'components/Modals/HLS/Summary/AssetSummary'
import YourPosition from 'components/Modals/HLS/Summary/YourPosition'
import AssetSummary from 'components/Modals/HLS/Deposit/Summary/AssetSummary'
import YourPosition from 'components/Modals/HLS/Deposit/Summary/YourPosition'
import useBorrowAsset from 'hooks/useBorrowAsset'
import { BNCoin } from 'types/classes/BNCoin'

View File

@ -1,9 +1,9 @@
import React, { useMemo, useState } from 'react'
import Accordion from 'components/Accordion'
import useStakingController from 'components/Modals/HLS/Content//useStakingController'
import useVaultController from 'components/Modals/HLS/Content//useVaultController'
import useAccordionItems from 'components/Modals/HLS/Content/useAccordionItems'
import useStakingController from 'components/Modals/HLS/Deposit//useStakingController'
import useVaultController from 'components/Modals/HLS/Deposit//useVaultController'
import useAccordionItems from 'components/Modals/HLS/Deposit/useAccordionItems'
import { EMPTY_ACCOUNT_HLS } from 'constants/accounts'
import useAccounts from 'hooks/useAccounts'
import useCurrentWalletBalance from 'hooks/useCurrentWalletBalance'

View File

@ -1,11 +1,15 @@
import React, { useMemo } from 'react'
import CreateAccount from 'components/Modals/HLS/CreateAccount'
import Leverage from 'components/Modals/HLS/Leverage'
import ProvideCollateral from 'components/Modals/HLS/ProvideCollateral'
import SelectAccount from 'components/Modals/HLS/SelectAccount'
import { CollateralSubTitle, LeverageSubTitle, SubTitle } from 'components/Modals/HLS/SubTitles'
import Summary from 'components/Modals/HLS/Summary'
import CreateAccount from 'components/Modals/HLS/Deposit/CreateAccount'
import Leverage from 'components/Modals/HLS/Deposit/Leverage'
import ProvideCollateral from 'components/Modals/HLS/Deposit/ProvideCollateral'
import SelectAccount from 'components/Modals/HLS/Deposit/SelectAccount'
import {
CollateralSubTitle,
LeverageSubTitle,
SubTitle,
} from 'components/Modals/HLS/Deposit/SubTitles'
import Summary from 'components/Modals/HLS/Deposit/Summary'
import { BN } from 'utils/helpers'
interface Props {

View File

@ -1,14 +1,10 @@
import { useCallback, useMemo } from 'react'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import useDepositHlsVault from 'hooks/useDepositHlsVault'
import useHealthComputer from 'hooks/useHealthComputer'
import useLocalStorage from 'hooks/useLocalStorage'
import { useUpdatedAccount } from 'hooks/useUpdatedAccount'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { Action } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
interface Props {
borrowAsset: Asset
@ -16,9 +12,8 @@ interface Props {
selectedAccount: Account
}
export default function useVaultController(props: Props) {
export default function useStakingController(props: Props) {
const { collateralAsset, borrowAsset, selectedAccount } = props
const [slippage] = useLocalStorage<number>(LocalStorageKeys.SLIPPAGE, DEFAULT_SETTINGS.slippage)
const addToStakingStrategy = useStore((s) => s.addToStakingStrategy)
const {
@ -28,40 +23,14 @@ export default function useVaultController(props: Props) {
setBorrowAmount,
borrowAmount,
positionValue,
borrowCoin,
depositCoin,
actions,
} = useDepositHlsVault({
collateralDenom: collateralAsset.denom,
borrowDenom: borrowAsset.denom,
})
const depositCoin = useMemo(
() => BNCoin.fromDenomAndBigNumber(collateralAsset.denom, depositAmount),
[collateralAsset.denom, depositAmount],
)
const borrowCoin = useMemo(
() => BNCoin.fromDenomAndBigNumber(borrowAsset.denom, borrowAmount),
[borrowAsset.denom, borrowAmount],
)
const actions: Action[] = useMemo(
() => [
{
deposit: depositCoin.toCoin(),
},
{
borrow: borrowCoin.toCoin(),
},
{
swap_exact_in: {
denom_out: collateralAsset.denom,
slippage: slippage.toString(),
coin_in: BNCoin.fromDenomAndBigNumber(borrowAsset.denom, borrowAmount).toActionCoin(),
},
},
],
[borrowAmount, borrowAsset.denom, borrowCoin, collateralAsset.denom, depositCoin, slippage],
)
const { updatedAccount, addDeposits } = useUpdatedAccount(selectedAccount)
const { computeMaxBorrowAmount } = useHealthComputer(updatedAccount)

View File

@ -7,16 +7,18 @@ import Text from 'components/Text'
interface Props {
primaryAsset: Asset
secondaryAsset: Asset
action?: HlsStakingManageAction
}
export default function Header(props: Props) {
return (
<div className='flex items-center gap-2'>
<DoubleLogo
primaryDenom={props.secondaryAsset.denom}
secondaryDenom={props.primaryAsset.denom}
primaryDenom={props.primaryAsset.denom}
secondaryDenom={props.secondaryAsset.denom}
/>
<Text>{`${props.secondaryAsset.symbol} - ${props.primaryAsset.symbol}`}</Text>
<Text>{`${props.primaryAsset.symbol}/${props.secondaryAsset.symbol}`}</Text>
{props.action && <Text className='capitalize'> - {props.action}</Text>}
<HLSTag />
</div>
)

View File

@ -0,0 +1,10 @@
interface Props {
account: Account
action: HlsStakingManageAction
borrowAsset: Asset
collateralAsset: Asset
}
export default function Repay(props: Props) {
return <></>
}

View File

@ -0,0 +1,204 @@
import BigNumber from 'bignumber.js'
import React, { useCallback, useEffect, useMemo } from 'react'
import Button from 'components/Button'
import Divider from 'components/Divider'
import SummaryItems from 'components/SummaryItems'
import Switch from 'components/Switch'
import Text from 'components/Text'
import TokenInputWithSlider from 'components/TokenInput/TokenInputWithSlider'
import { BN_ZERO } from 'constants/math'
import useDepositActions from 'hooks/HLS/useDepositActions'
import useBorrowAsset from 'hooks/useBorrowAsset'
import useCurrentWalletBalance from 'hooks/useCurrentWalletBalance'
import useHealthComputer from 'hooks/useHealthComputer'
import usePrices from 'hooks/usePrices'
import useToggle from 'hooks/useToggle'
import { useUpdatedAccount } from 'hooks/useUpdatedAccount'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { calculateAccountLeverage } from 'utils/accounts'
import { byDenom } from 'utils/array'
import { getCoinAmount, getCoinValue } from 'utils/formatters'
import { BN } from 'utils/helpers'
interface Props {
account: Account
action: HlsStakingManageAction
borrowAsset: Asset
collateralAsset: Asset
}
export default function Deposit(props: Props) {
const { addedDeposits, addedDebts, updatedAccount, simulateHlsStakingDeposit } =
useUpdatedAccount(props.account)
const { computeMaxBorrowAmount } = useHealthComputer(updatedAccount)
const { data: prices } = usePrices()
const [keepLeverage, toggleKeepLeverage] = useToggle(true)
const collateralAssetAmountInWallet = BN(
useCurrentWalletBalance(props.collateralAsset.denom)?.amount || '0',
)
const addToStakingStrategy = useStore((s) => s.addToStakingStrategy)
const borrowRate = useBorrowAsset(props.borrowAsset.denom)?.borrowRate || 0
const currentLeverage = useMemo(
() => calculateAccountLeverage(props.account, prices).toNumber(),
[prices, props.account],
)
const depositCoin = useMemo(
() =>
BNCoin.fromDenomAndBigNumber(
props.collateralAsset.denom,
addedDeposits.find(byDenom(props.collateralAsset.denom))?.amount || BN_ZERO,
),
[addedDeposits, props.collateralAsset.denom],
)
const borrowCoin = useMemo(
() =>
BNCoin.fromDenomAndBigNumber(
props.borrowAsset.denom,
addedDebts.find(byDenom(props.borrowAsset.denom))?.amount || BN_ZERO,
),
[addedDebts, props.borrowAsset.denom],
)
const maxBorrowAmount = useMemo(
() => computeMaxBorrowAmount(props.collateralAsset.denom, 'deposit'),
[computeMaxBorrowAmount, props.collateralAsset.denom],
)
useEffect(() => {
if (borrowCoin.amount.isGreaterThan(maxBorrowAmount)) {
simulateHlsStakingDeposit(
BNCoin.fromDenomAndBigNumber(props.collateralAsset.denom, depositCoin.amount),
BNCoin.fromDenomAndBigNumber(props.borrowAsset.denom, maxBorrowAmount),
)
}
}, [
borrowCoin.amount,
depositCoin.amount,
maxBorrowAmount,
props.borrowAsset.denom,
props.collateralAsset.denom,
simulateHlsStakingDeposit,
])
const actions = useDepositActions({ depositCoin, borrowCoin })
const currentDebt: BigNumber = useMemo(
() => props.account.debts.find(byDenom(props.borrowAsset.denom)).amount || BN_ZERO,
[props.account.debts, props.borrowAsset.denom],
)
const handleDeposit = useCallback(() => {
useStore.setState({ hlsManageModal: null })
addToStakingStrategy({
accountId: props.account.id,
actions,
depositCoin,
borrowCoin,
})
}, [actions, addToStakingStrategy, borrowCoin, depositCoin, props.account.id])
const handleOnChange = useCallback(
(amount: BigNumber) => {
let additionalDebt = BN_ZERO
if (currentLeverage > 1 && keepLeverage) {
const depositValue = getCoinValue(
BNCoin.fromDenomAndBigNumber(props.collateralAsset.denom, amount),
prices,
)
const borrowValue = BN(currentLeverage - 1).times(depositValue)
additionalDebt = getCoinAmount(props.borrowAsset.denom, borrowValue, prices)
}
simulateHlsStakingDeposit(
BNCoin.fromDenomAndBigNumber(props.collateralAsset.denom, amount),
BNCoin.fromDenomAndBigNumber(props.borrowAsset.denom, additionalDebt),
)
},
[
currentLeverage,
keepLeverage,
prices,
props.borrowAsset.denom,
props.collateralAsset.denom,
simulateHlsStakingDeposit,
],
)
const items: SummaryItem[] = useMemo(
() => [
...(keepLeverage
? [
{
title: 'Borrow rate',
amount: borrowRate,
options: {
suffix: `%`,
minDecimals: 2,
maxDecimals: 2,
},
},
{
title: 'Additional Borrow Amount',
amount: borrowCoin.amount.toNumber(),
options: {
suffix: ` ${props.borrowAsset.symbol}`,
abbreviated: true,
decimals: props.borrowAsset.decimals,
},
},
{
title: 'New Debt Amount',
amount: currentDebt.plus(borrowCoin.amount).toNumber(),
options: {
suffix: ` ${props.borrowAsset.symbol}`,
abbreviated: true,
decimals: props.borrowAsset.decimals,
},
},
]
: []),
],
[
borrowCoin.amount,
borrowRate,
currentDebt,
keepLeverage,
props.borrowAsset.decimals,
props.borrowAsset.symbol,
],
)
return (
<>
<div>
<TokenInputWithSlider
amount={depositCoin.amount}
asset={props.collateralAsset}
max={collateralAssetAmountInWallet}
onChange={handleOnChange}
maxText='In Wallet'
/>
<Divider className='my-6' />
<div className='flex flex-wrap flex-1 items-center justify-between'>
<div>
<Text className='w-full mb-1'>Keep leverage</Text>
<Text size='xs' className='text-white/50'>
Automatically borrow more funds to keep leverage
</Text>
</div>
<Switch name='keep-leverage' checked={keepLeverage} onChange={toggleKeepLeverage} />
</div>
</div>
<div className='flex flex-col gap-4'>
<SummaryItems items={items} />
<Button onClick={handleDeposit} text='Deposit' disabled={depositCoin.amount.isZero()} />
</div>
</>
)
}

View File

@ -0,0 +1,98 @@
import BigNumber from 'bignumber.js'
import { useCallback, useMemo } from 'react'
import Button from 'components/Button'
import SummaryItems from 'components/SummaryItems'
import TokenInputWithSlider from 'components/TokenInput/TokenInputWithSlider'
import { BN_ZERO } from 'constants/math'
import useCurrentWalletBalance from 'hooks/useCurrentWalletBalance'
import { useUpdatedAccount } from 'hooks/useUpdatedAccount'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { byDenom } from 'utils/array'
import { BN } from 'utils/helpers'
interface Props {
account: Account
action: HlsStakingManageAction
borrowAsset: Asset
collateralAsset: Asset
}
export default function Repay(props: Props) {
const { removeDebts, removedDebts } = useUpdatedAccount(props.account)
const borrowAssetAmountInWallet = BN(
useCurrentWalletBalance(props.borrowAsset.denom)?.amount || '0',
)
const repay = useStore((s) => s.repay)
const currentDebt: BigNumber = useMemo(
() => props.account.debts.find(byDenom(props.borrowAsset.denom)).amount || BN_ZERO,
[props.account.debts, props.borrowAsset.denom],
)
const repayAmount: BigNumber = useMemo(
() => removedDebts.find(byDenom(props.borrowAsset.denom))?.amount || BN_ZERO,
[removedDebts, props.borrowAsset.denom],
)
const maxRepayAmount = useMemo(
() => BigNumber.min(borrowAssetAmountInWallet.toNumber(), currentDebt),
[borrowAssetAmountInWallet, currentDebt],
)
const items: SummaryItem[] = useMemo(
() => [
{
title: 'Total Debt Repayable',
amount: currentDebt.toNumber(),
options: {
suffix: ` ${props.borrowAsset.symbol}`,
abbreviated: true,
decimals: props.borrowAsset.decimals,
},
},
{
title: 'New Debt Amount',
amount: currentDebt.minus(repayAmount).toNumber(),
options: {
suffix: ` ${props.borrowAsset.symbol}`,
abbreviated: true,
decimals: props.borrowAsset.decimals,
},
},
],
[currentDebt, props.borrowAsset.decimals, props.borrowAsset.symbol, repayAmount],
)
const handleRepay = useCallback(() => {
useStore.setState({ hlsManageModal: null })
repay({
accountId: props.account.id,
coin: BNCoin.fromDenomAndBigNumber(props.borrowAsset.denom, repayAmount),
fromWallet: true,
})
}, [props.account.id, props.borrowAsset.denom, repay, repayAmount])
const handleOnChange = useCallback(
(amount: BigNumber) =>
removeDebts([BNCoin.fromDenomAndBigNumber(props.borrowAsset.denom, amount)]),
[props.borrowAsset.denom, removeDebts],
)
return (
<>
<TokenInputWithSlider
amount={removedDebts.find(byDenom(props.borrowAsset.denom))?.amount || BN_ZERO}
asset={props.borrowAsset}
max={maxRepayAmount}
onChange={handleOnChange}
maxText='In Wallet'
/>
<div className='flex flex-col gap-4'>
<SummaryItems items={items} />
<Button onClick={handleRepay} text='Repay' />
</div>
</>
)
}

View File

@ -0,0 +1,62 @@
import React, { useCallback, useMemo } from 'react'
import Button from 'components/Button'
import TokenInputWithSlider from 'components/TokenInput/TokenInputWithSlider'
import { BN_ZERO } from 'constants/math'
import useHealthComputer from 'hooks/useHealthComputer'
import { useUpdatedAccount } from 'hooks/useUpdatedAccount'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { byDenom } from 'utils/array'
interface Props {
account: Account
action: HlsStakingManageAction
borrowAsset: Asset
collateralAsset: Asset
}
export default function Withdraw(props: Props) {
const { removedDeposits, removeDeposits, updatedAccount } = useUpdatedAccount(props.account)
const { computeMaxWithdrawAmount } = useHealthComputer(updatedAccount)
const withdraw = useStore((s) => s.withdraw)
const handleChange = useCallback(
(amount: BigNumber) =>
removeDeposits([BNCoin.fromDenomAndBigNumber(props.collateralAsset.denom, amount)]),
[removeDeposits, props.collateralAsset.denom],
)
const removedDeposit = useMemo(
() => removedDeposits.find(byDenom(props.collateralAsset.denom)),
[props.collateralAsset.denom, removedDeposits],
)
const maxWithdrawAmount = useMemo(() => {
const currentWithdrawAmount = removedDeposit?.amount || BN_ZERO
const extraWithdrawAmount = computeMaxWithdrawAmount(props.collateralAsset.denom)
return currentWithdrawAmount.plus(extraWithdrawAmount)
}, [computeMaxWithdrawAmount, props.collateralAsset.denom, removedDeposit?.amount])
const onClick = useCallback(() => {
useStore.setState({ hlsManageModal: null })
withdraw({
accountId: props.account.id,
coins: [{ coin: removedDeposit }],
borrow: [],
reclaims: [],
})
}, [props.account.id, removedDeposit, withdraw])
return (
<>
<TokenInputWithSlider
amount={removedDeposit?.amount || BN_ZERO}
asset={props.collateralAsset}
max={maxWithdrawAmount}
onChange={handleChange}
maxText='Available'
/>
<Button onClick={onClick} text='Withdraw' disabled={removedDeposit?.amount?.isZero()} />
</>
)
}

View File

@ -1,40 +1,74 @@
import React from 'react'
import React, { useCallback } from 'react'
import Modal from 'components/Modal'
import Header from 'components/Modals/HLS/Header'
import ChangeLeverage from 'components/Modals/HLS/Manage/ChangeLeverage'
import Deposit from 'components/Modals/HLS/Manage/Deposit'
import Repay from 'components/Modals/HLS/Manage/Repay'
import Withdraw from 'components/Modals/HLS/Manage/Withdraw'
import ModalContentWithSummary from 'components/Modals/ModalContentWithSummary'
import useAccount from 'hooks/useAccount'
import useStore from 'store'
import { getAssetByDenom } from 'utils/assets'
export default function HlsManageModalController() {
const modal = useStore((s) => s.hlsManageModal)
const { data: account } = useAccount(modal?.accountId)
const collateralAsset = getAssetByDenom(modal?.staking.strategy.denoms.deposit || '')
const borrowAsset = getAssetByDenom(modal?.staking.strategy.denoms.borrow || '')
if (!modal || !collateralAsset || !borrowAsset) return null
if (!modal || !collateralAsset || !borrowAsset || !account) return null
return <HlsModal collateralAsset={collateralAsset} borrowAsset={borrowAsset} />
return (
<HlsModal
account={account}
action={modal.staking.action}
collateralAsset={collateralAsset}
borrowAsset={borrowAsset}
/>
)
}
interface Props {
account: Account
action: HlsStakingManageAction
borrowAsset: Asset
collateralAsset: Asset
}
function HlsModal(props: Props) {
const updatedAccount = useStore((s) => s.updatedAccount)
function handleClose() {
useStore.setState({ hlsManageModal: null })
}
const ContentComponent = useCallback(() => {
switch (props.action) {
case 'deposit':
return <Deposit {...props} />
case 'withdraw':
return <Withdraw {...props} />
case 'repay':
return <Repay {...props} />
case 'leverage':
return <ChangeLeverage {...props} />
default:
return null
}
}, [props])
return (
<Modal
header={<Header primaryAsset={props.collateralAsset} secondaryAsset={props.borrowAsset} />}
headerClassName='gradient-header pl-2 pr-2.5 py-3 border-b-white/5 border-b'
contentClassName='flex flex-col p-6'
modalClassName='max-w-modal-md'
<ModalContentWithSummary
account={props.account}
header={
<Header
action={props.action}
primaryAsset={props.collateralAsset}
secondaryAsset={props.borrowAsset}
/>
}
onClose={handleClose}
>
Some kind of text here
</Modal>
content={<ContentComponent />}
isContentCard
/>
)
}

View File

@ -1,7 +1,7 @@
import React from 'react'
import Modal from 'components/Modal'
import Content from 'components/Modals/HLS/Content'
import Content from 'components/Modals/HLS/Deposit'
import Header from 'components/Modals/HLS/Header'
import useStore from 'store'
import { getAssetByDenom } from 'utils/assets'

View File

@ -0,0 +1,38 @@
import classNames from 'classnames'
import React from 'react'
import AccountSummary from 'components/Account/AccountSummary'
import Card from 'components/Card'
import Modal, { ModalProps } from 'components/Modal'
import useStore from 'store'
interface Props extends ModalProps {
account: Account
isContentCard?: boolean
}
export default function ModalContentWithSummary(props: Props) {
const updatedAccount = useStore((s) => s.updatedAccount)
return (
<Modal
{...props}
headerClassName={classNames(
'gradient-header pl-2 pr-2.5 py-3 border-b-white/5 border-b',
props.headerClassName,
)}
contentClassName={classNames('flex items-start flex-1 gap-6 p-6', props.contentClassName)}
>
{props.isContentCard ? (
<Card
className='flex flex-1 p-4 bg-white/5'
contentClassName='gap-6 flex flex-col justify-between h-full min-h-[380px]'
>
{props.content}
</Card>
) : (
props.content
)}
<AccountSummary account={updatedAccount || props.account} />
</Modal>
)
}

View File

@ -0,0 +1,25 @@
import React from 'react'
import { FormattedNumber } from 'components/FormattedNumber'
import Text from 'components/Text'
interface Props {
items: SummaryItem[]
}
export default function SummaryItems(props: Props) {
return (
<div className='grid grid-cols-2 gap-2'>
{props.items.map((item) => (
<React.Fragment key={item.title}>
<Text className='text-white/60 text-sm'>{item.title}</Text>
<FormattedNumber
className='place-self-end text-sm'
amount={item.amount}
options={item.options}
/>
</React.Fragment>
))}
</div>
)
}

View File

@ -1,4 +1,4 @@
import Tippy from '@tippyjs/react'
import Tippy, { TippyProps } from '@tippyjs/react'
import classNames from 'classnames'
import { ReactNode } from 'react'
@ -8,10 +8,9 @@ import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import useLocalStorage from 'hooks/useLocalStorage'
interface Props {
interface Props extends TippyProps {
content: ReactNode | string
type: TooltipType
children?: ReactNode | string
className?: string
delay?: number
interactive?: boolean
@ -45,6 +44,7 @@ export const Tooltip = (props: Props) => {
className={props.contentClassName}
/>
)}
{...props}
>
{props.children ? (
<span

View File

@ -0,0 +1,59 @@
import { useMemo } from 'react'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import { BN_ZERO } from 'constants/math'
import useLocalStorage from 'hooks/useLocalStorage'
import usePrices from 'hooks/usePrices'
import { BNCoin } from 'types/classes/BNCoin'
import { Action } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import { getCoinAmount, getCoinValue } from 'utils/formatters'
interface Props {
account: HLSAccountWithStrategy
}
export default function UseClosePositionActions(props: Props): Action[] {
const [slippage] = useLocalStorage<number>(LocalStorageKeys.SLIPPAGE, DEFAULT_SETTINGS.slippage)
const { data: prices } = usePrices()
const collateralDenom = props.account.strategy.denoms.deposit
const borrowDenom = props.account.strategy.denoms.borrow
const debtAmount: BigNumber = useMemo(
() =>
props.account.debts.find((debt) => debt.denom === props.account.strategy.denoms.borrow)
?.amount || BN_ZERO,
[props.account.debts, props.account.strategy.denoms.borrow],
)
const swapInAmount = useMemo(() => {
const targetValue = getCoinValue(BNCoin.fromDenomAndBigNumber(borrowDenom, debtAmount), prices)
return getCoinAmount(collateralDenom, targetValue, prices)
.times(1 + slippage)
.integerValue()
}, [slippage, borrowDenom, debtAmount, prices, collateralDenom])
return useMemo<Action[]>(
() => [
...(debtAmount.isZero()
? []
: [
{
swap_exact_in: {
coin_in: BNCoin.fromDenomAndBigNumber(collateralDenom, swapInAmount).toActionCoin(),
denom_out: borrowDenom,
slippage: slippage.toString(),
},
},
{
repay: {
coin: BNCoin.fromDenomAndBigNumber(borrowDenom, debtAmount).toActionCoin(),
},
},
]),
{ refund_all_coin_balances: {} },
],
[borrowDenom, collateralDenom, debtAmount, slippage, swapInAmount],
)
}

View File

@ -0,0 +1,38 @@
import { useMemo } from 'react'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import useLocalStorage from 'hooks/useLocalStorage'
import { BNCoin } from 'types/classes/BNCoin'
interface Props {
borrowCoin: BNCoin
depositCoin: BNCoin
}
export default function useDepositActions(props: Props) {
const [slippage] = useLocalStorage<number>(LocalStorageKeys.SLIPPAGE, DEFAULT_SETTINGS.slippage)
return useMemo(
() => [
{
deposit: props.depositCoin.toCoin(),
},
...(props.borrowCoin.amount.isZero()
? []
: [
{
borrow: props.borrowCoin.toCoin(),
},
{
swap_exact_in: {
denom_out: props.depositCoin.denom,
slippage: slippage.toString(),
coin_in: props.borrowCoin.toActionCoin(),
},
},
]),
],
[props.borrowCoin, props.depositCoin, slippage],
)
}

View File

@ -1,8 +1,12 @@
import { useMemo, useState } from 'react'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import { BN_ZERO } from 'constants/math'
import useLocalStorage from 'hooks/useLocalStorage'
import usePrices from 'hooks/usePrices'
import { BNCoin } from 'types/classes/BNCoin'
import { Action } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import { getCoinValue } from 'utils/formatters'
interface Props {
@ -11,10 +15,21 @@ interface Props {
}
export default function useDepositHlsVault(props: Props) {
const { data: prices } = usePrices()
const [slippage] = useLocalStorage<number>(LocalStorageKeys.SLIPPAGE, DEFAULT_SETTINGS.slippage)
const [depositAmount, setDepositAmount] = useState<BigNumber>(BN_ZERO)
const [borrowAmount, setBorrowAmount] = useState<BigNumber>(BN_ZERO)
const depositCoin = useMemo(
() => BNCoin.fromDenomAndBigNumber(props.collateralDenom, depositAmount),
[depositAmount, props.collateralDenom],
)
const borrowCoin = useMemo(
() => BNCoin.fromDenomAndBigNumber(props.borrowDenom, borrowAmount),
[borrowAmount, props.borrowDenom],
)
const { positionValue, leverage } = useMemo(() => {
const collateralValue = getCoinValue(
BNCoin.fromDenomAndBigNumber(props.collateralDenom, depositAmount),
@ -31,6 +46,32 @@ export default function useDepositHlsVault(props: Props) {
}
}, [borrowAmount, depositAmount, prices, props.collateralDenom, props.borrowDenom])
const actions: Action[] = useMemo(
() => [
{
deposit: depositCoin.toCoin(),
},
...(borrowAmount.isZero()
? []
: [
{
borrow: borrowCoin.toCoin(),
},
{
swap_exact_in: {
denom_out: props.collateralDenom,
slippage: slippage.toString(),
coin_in: BNCoin.fromDenomAndBigNumber(
props.borrowDenom,
borrowAmount,
).toActionCoin(),
},
},
]),
],
[borrowAmount, borrowCoin, depositCoin, props.borrowDenom, props.collateralDenom, slippage],
)
return {
setDepositAmount,
depositAmount,
@ -38,5 +79,8 @@ export default function useDepositHlsVault(props: Props) {
borrowAmount,
positionValue,
leverage,
depositCoin,
borrowCoin,
actions,
}
}

View File

@ -16,6 +16,7 @@ import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { cloneAccount } from 'utils/accounts'
import { byDenom } from 'utils/array'
import { getCoinAmount, getCoinValue } from 'utils/formatters'
import { getValueFromBNCoins } from 'utils/helpers'
export interface VaultValue {
@ -37,6 +38,7 @@ export function useUpdatedAccount(account?: Account) {
const [addedVaultValues, addVaultValues] = useState<VaultValue[]>([])
const [addedLends, addLends] = useState<BNCoin[]>([])
const [removedLends, removeLends] = useState<BNCoin[]>([])
const [addedTrades, addTrades] = useState<BNCoin[]>([])
const removeDepositAndLendsByDenom = useCallback(
(denom: string) => {
@ -151,6 +153,18 @@ export function useUpdatedAccount(account?: Account) {
[account, addDebts, addDeposits, addLends, removeDeposits, removeLends],
)
const simulateHlsStakingDeposit = useCallback(
(depositCoin: BNCoin, borrowCoin: BNCoin) => {
addDeposits([depositCoin])
addDebts([borrowCoin])
const additionalDebtValue = getCoinValue(borrowCoin, prices)
const tradeOutputAmount = getCoinAmount(depositCoin.denom, additionalDebtValue, prices)
addTrades([BNCoin.fromDenomAndBigNumber(depositCoin.denom, tradeOutputAmount)])
},
[prices],
)
const simulateVaultDeposit = useCallback(
(address: string, coins: BNCoin[], borrowCoins: BNCoin[]) => {
if (!account) return
@ -179,7 +193,7 @@ export function useUpdatedAccount(account?: Account) {
if (!account) return
const accountCopy = cloneAccount(account)
accountCopy.deposits = addCoins(addedDeposits, [...accountCopy.deposits])
accountCopy.deposits = addCoins([...addedDeposits, ...addedTrades], [...accountCopy.deposits])
accountCopy.debts = addCoins(addedDebts, [...accountCopy.debts])
accountCopy.vaults = addValueToVaults(
addedVaultValues,
@ -205,6 +219,7 @@ export function useUpdatedAccount(account?: Account) {
removedLends,
availableVaults,
prices,
addedTrades,
])
return {
@ -225,6 +240,7 @@ export function useUpdatedAccount(account?: Account) {
removedLends,
simulateBorrow,
simulateDeposits,
simulateHlsStakingDeposit,
simulateLending,
simulateRepay,
simulateTrade,

View File

@ -232,6 +232,32 @@ export default function createBroadcastSlice(
return response.then((response) => !!response.result)
},
closeHlsStakingPosition: 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: `Exited HLS strategy`,
},
})
const response_1 = await response
return response_1.result
? getSingleValueFromBroadcastResult(response_1.result, 'wasm', 'token_id')
: null
},
createAccount: async (accountKind: AccountKind) => {
const msg: CreditManagerExecuteMsg = {
create_credit_account: accountKind,
@ -538,8 +564,10 @@ export default function createBroadcastSlice(
coin: BNCoin
accountBalance?: boolean
lend?: BNCoin
fromWallet?: boolean
}) => {
const actions: Action[] = [
...(options.fromWallet ? [{ deposit: options.coin.toCoin() }] : []),
{
repay: {
coin: options.coin.toActionCoin(options.accountBalance),
@ -558,7 +586,14 @@ export default function createBroadcastSlice(
}
const response = get().executeMsg({
messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
messages: [
generateExecutionMessage(
get().address,
ENV.ADDRESS_CREDIT_MANAGER,
msg,
options.fromWallet ? [options.coin.toCoin()] : [],
),
],
})
get().setToast({

View File

@ -0,0 +1,5 @@
interface SummaryItem {
amount: number
options: FormatOptions
title: string
}

View File

@ -92,6 +92,10 @@ interface BroadcastSlice {
borrowToWallet: boolean
}) => Promise<boolean>
claimRewards: (options: { accountId: string }) => ExecutableTx
closeHlsStakingPosition: (options: {
accountId: string
actions: Action[]
}) => Promise<string | null>
createAccount: (
accountKind: import('types/generated/mars-rover-health-types/MarsRoverHealthTypes.types').AccountKind,
) => Promise<string | null>
@ -113,6 +117,7 @@ interface BroadcastSlice {
coin: BNCoin
accountBalance?: boolean
lend?: BNCoin
fromWallet?: boolean
}) => Promise<boolean>
setToast: (toast: ToastObject) => void
swap: (options: {

View File

@ -76,8 +76,11 @@ interface HlsModal {
}
interface HlsManageModal {
accountId: string
staking: {
strategy: HLSStrategy
action: 'deposit' | 'withdraw' | 'repay'
action: HlsStakingManageAction
}
}
type HlsStakingManageAction = 'deposit' | 'withdraw' | 'repay' | 'leverage'