Mp 3367 staking interactions (#613)

* ♻️ Refactor borrowRate to be in full numbers

* Enter into HLS Staking strategy

* HLS Staking deposited table + Portfolio pages

* tidy: refactored the masks for HealthBar

---------

Co-authored-by: Linkie Link <linkielink.dev@gmail.com>
This commit is contained in:
Bob van der Helm 2023-11-03 15:01:15 +01:00 committed by GitHub
parent dd29f17a42
commit d2afe06b16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 743 additions and 194 deletions

View File

@ -4,16 +4,20 @@ import getDepositedVaults from 'api/vaults/getDepositedVaults'
import { BNCoin } from 'types/classes/BNCoin' import { BNCoin } from 'types/classes/BNCoin'
import { Positions } from 'types/generated/mars-credit-manager/MarsCreditManager.types' import { Positions } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
export default async function getAccount(accountIdAndKind: AccountIdAndKind): Promise<Account> { export default async function getAccount(accountId?: string): Promise<Account> {
if (!accountId) return new Promise((_, reject) => reject('No account ID found'))
const creditManagerQueryClient = await getCreditManagerQueryClient() const creditManagerQueryClient = await getCreditManagerQueryClient()
const accountPosition: Positions = await cacheFn( const accountPosition: Positions = await cacheFn(
() => creditManagerQueryClient.positions({ accountId: accountIdAndKind.id }), () => creditManagerQueryClient.positions({ accountId: accountId }),
positionsCache, positionsCache,
`account/${accountIdAndKind.id}`, `account/${accountId}`,
) )
const depositedVaults = await getDepositedVaults(accountIdAndKind.id, accountPosition) const accountKind = await creditManagerQueryClient.accountKind({ accountId: accountId })
const depositedVaults = await getDepositedVaults(accountId, accountPosition)
if (accountPosition) { if (accountPosition) {
return { return {
@ -22,7 +26,7 @@ export default async function getAccount(accountIdAndKind: AccountIdAndKind): Pr
lends: accountPosition.lends.map((lend) => new BNCoin(lend)), lends: accountPosition.lends.map((lend) => new BNCoin(lend)),
deposits: accountPosition.deposits.map((deposit) => new BNCoin(deposit)), deposits: accountPosition.deposits.map((deposit) => new BNCoin(deposit)),
vaults: depositedVaults, vaults: depositedVaults,
kind: accountIdAndKind.kind, kind: accountKind,
} }
} }

View File

@ -0,0 +1,41 @@
import getHLSStakingAssets from 'api/hls/getHLSStakingAssets'
import getPrices from 'api/prices/getPrices'
import getAccounts from 'api/wallets/getAccounts'
import { calculateAccountLeverage, getAccountPositionValues, isAccountEmpty } from 'utils/accounts'
export default async function getHLSStakingAccounts(
address?: string,
): Promise<HLSAccountWithStrategy[]> {
const accounts = await getAccounts('high_levered_strategy', address)
const activeAccounts = accounts.filter((account) => !isAccountEmpty(account))
const hlsStrategies = await getHLSStakingAssets()
const prices = await getPrices()
const hlsAccountsWithStrategy: HLSAccountWithStrategy[] = []
activeAccounts.forEach((account) => {
if (account.deposits.length === 0 || account.debts.length === 0) return
const strategy = hlsStrategies.find(
(strategy) =>
strategy.denoms.deposit === account.deposits.at(0).denom &&
strategy.denoms.borrow === account.debts.at(0).denom,
)
if (!strategy) return
const [deposits, lends, debts, vaults] = getAccountPositionValues(account, prices)
hlsAccountsWithStrategy.push({
...account,
strategy,
values: {
net: deposits.minus(debts),
debt: debts,
total: deposits,
},
leverage: calculateAccountLeverage(account, prices).toNumber(),
})
})
return hlsAccountsWithStrategy
}

View File

@ -1,14 +1,18 @@
import { getParamsQueryClient } from 'api/cosmwasm-client' import { getParamsQueryClient } from 'api/cosmwasm-client'
import getAssetParams from 'api/params/getAssetParams' import getAssetParams from 'api/params/getAssetParams'
import { getStakingAssets } from 'utils/assets'
import { BN } from 'utils/helpers' import { BN } from 'utils/helpers'
import { resolveHLSStrategies } from 'utils/resolvers' import { resolveHLSStrategies } from 'utils/resolvers'
export default async function getHLSStakingAssets() { export default async function getHLSStakingAssets() {
const stakingAssetDenoms = getStakingAssets().map((asset) => asset.denom)
const assetParams = await getAssetParams() const assetParams = await getAssetParams()
const client = await getParamsQueryClient() const HLSAssets = assetParams
const HLSAssets = assetParams.filter((asset) => asset.credit_manager.hls) .filter((asset) => stakingAssetDenoms.includes(asset.denom))
.filter((asset) => asset.credit_manager.hls)
const strategies = resolveHLSStrategies('coin', HLSAssets) const strategies = resolveHLSStrategies('coin', HLSAssets)
const client = await getParamsQueryClient()
const depositCaps$ = strategies.map((strategy) => const depositCaps$ = strategies.map((strategy) =>
client.totalDeposit({ denom: strategy.denoms.deposit }), client.totalDeposit({ denom: strategy.denoms.deposit }),
) )

View File

@ -41,7 +41,7 @@ export default async function getVaults(): Promise<Vault[]> {
), ),
}, },
apy: apr ? convertAprToApy(apr.apr, 365) : null, apy: apr ? convertAprToApy(apr.apr, 365) : null,
apr: apr ? apr.apr / 100 : null, apr: apr ? apr.apr : null,
ltv: { ltv: {
max: Number(vaultConfig.max_loan_to_value), max: Number(vaultConfig.max_loan_to_value),
liq: Number(vaultConfig.liquidation_threshold), liq: Number(vaultConfig.liquidation_threshold),

View File

@ -8,7 +8,7 @@ export default async function getAccounts(kind: AccountKind, address?: string):
const $accounts = accountIdsAndKinds const $accounts = accountIdsAndKinds
.filter((a) => a.kind === kind) .filter((a) => a.kind === kind)
.map((account) => getAccount(account)) .map((account) => getAccount(account.id))
const accounts = await Promise.all($accounts).then((accounts) => accounts) const accounts = await Promise.all($accounts).then((accounts) => accounts)

View File

@ -65,7 +65,7 @@ export default function useAccountBalanceData(props: Props) {
const prevDebt = updatedAccount const prevDebt = updatedAccount
? account?.debts.find((position) => position.denom === debt.denom) ? account?.debts.find((position) => position.denom === debt.denom)
: debt : debt
return getAssetAccountBalanceRow('borrowing', asset, prices, debt, apy * -100, prevDebt) return getAssetAccountBalanceRow('borrowing', asset, prices, debt, apy, prevDebt)
}) })
return [...deposits, ...lends, ...vaults, ...debts] return [...deposits, ...lends, ...vaults, ...debts]
}, [prices, account, updatedAccount, borrowingData, lendingData]) }, [prices, account, updatedAccount, borrowingData, lendingData])

View File

@ -21,7 +21,7 @@ interface Props {
export default function AccountStats(props: Props) { export default function AccountStats(props: Props) {
const { accountId, isActive, setShowMenu } = props const { accountId, isActive, setShowMenu } = props
const { data: account } = useAccount('default', accountId) const { data: account } = useAccount(accountId)
const { data: prices } = usePrices() const { data: prices } = usePrices()
const positionBalance = useMemo( const positionBalance = useMemo(
() => (!account ? null : calculateAccountBalanceValue(account, prices)), () => (!account ? null : calculateAccountBalanceValue(account, prices)),

View File

@ -1,6 +1,7 @@
import classNames from 'classnames' import classNames from 'classnames'
import { useMemo } from 'react' import { useMemo } from 'react'
import HealthIcon from 'components/Account/Health/HealthIcon'
import HealthTooltip from 'components/Account/Health/HealthTooltip' import HealthTooltip from 'components/Account/Health/HealthTooltip'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings' import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys' import { LocalStorageKeys } from 'constants/localStorageKeys'
@ -9,12 +10,15 @@ import useLocalStorage from 'hooks/useLocalStorage'
import { getHealthIndicatorColors } from 'utils/healthIndicator' import { getHealthIndicatorColors } from 'utils/healthIndicator'
interface Props { interface Props {
className?: string
hasLabel?: boolean
health: number health: number
healthFactor: number healthFactor: number
height?: string
iconClassName?: string
updatedHealth?: number updatedHealth?: number
updatedHealthFactor?: number updatedHealthFactor?: number
hasLabel?: boolean showIcon?: boolean
className?: string
} }
function calculateHealth(health: number): number { function calculateHealth(health: number): number {
@ -36,6 +40,9 @@ export default function HealthBar({
healthFactor = 0, healthFactor = 0,
updatedHealthFactor = 0, updatedHealthFactor = 0,
className, className,
height = '4',
iconClassName = 'w-5',
showIcon = false,
}: Props) { }: Props) {
const [reduceMotion] = useLocalStorage<boolean>( const [reduceMotion] = useLocalStorage<boolean>(
LocalStorageKeys.REDUCE_MOTION, LocalStorageKeys.REDUCE_MOTION,
@ -57,59 +64,81 @@ export default function HealthBar({
health={isUpdated ? updatedHealth : health} health={isUpdated ? updatedHealth : health}
healthFactor={isUpdated ? updatedHealthFactor : healthFactor} healthFactor={isUpdated ? updatedHealthFactor : healthFactor}
> >
<div className={classNames('flex w-full', className)}> <>
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 184 4'> {showIcon && (
<HealthIcon
health={health}
isLoading={healthFactor === 0}
className={classNames('mr-2', iconClassName)}
colorClass='text-white'
/>
)}
<div className={classNames('flex w-full', 'rounded-full overflow-hidden', className)}>
<svg
version='1.1'
xmlns='http://www.w3.org/2000/svg'
x='0px'
y='0px'
viewBox={`0 0 184 ${height}`}
>
<mask id='healthBarMask'> <mask id='healthBarMask'>
<path fill='#FFFFFF' d='M0,2c0-1.1,0.9-2,2-2h41.6v4H2C0.9,4,0,3.1,0,2z' /> <rect fill='#FFFFFF' x='46' width='47' height={height} />
<rect x='46' fill='#FFFFFF' width='47' height='4' /> <rect fill='#FFFFFF' width='43.6' height={height} />
<path fill='#FFFFFF' d='M95.5,0H182c1.1,0,2,0.9,2,2s-0.9,2-2,2H95.5V0z' /> <rect fill='#FFFFFF' x='95.5' width='88.5' height={height} />
</mask> </mask>
<mask id='backgroundHealthBarMask'> <mask id='backgroundHealthBarMask'>
<rect x='62.1' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='0' y='0' width='6.4' height='{height}' />
<rect x='48' fill='white' width='2' height='4' /> <rect fill='#FFFFFF' x='8.9' y='0' width='2.4' height='{height}' />
<rect x='57.3' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='13.7' y='0' width='2.4' height='{height}' />
<rect x='52.5' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='18.5' y='0' width='2.4' height='{height}' />
<rect x='66.9' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='23.3' y='0' width='2.4' height='{height}' />
<rect x='86.1' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='28.1' y='0' width='2.4' height='{height}' />
<rect x='81.3' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='32.9' y='0' width='2.4' height='{height}' />
<rect x='71.7' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='37.7' y='0' width='2.4' height='{height}' />
<rect x='90.9' fill='white' width='2.1' height='4' /> <rect fill='#FFFFFF' x='42.5' y='0' width='2.4' height='{height}' />
<rect x='76.5' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='47.3' y='0' width='2.4' height='{height}' />
<rect x='119.2' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='52.1' y='0' width='2.4' height='{height}' />
<rect x='143.2' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='56.9' y='0' width='2.4' height='{height}' />
<rect x='138.4' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='61.7' y='0' width='2.4' height='{height}' />
<rect x='133.6' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='66.5' y='0' width='2.4' height='{height}' />
<rect x='124' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='71.3' y='0' width='2.4' height='{height}' />
<rect x='100' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='76.1' y='0' width='2.4' height='{height}' />
<rect x='104.8' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='80.9' y='0' width='2.4' height='{height}' />
<rect x='109.6' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='85.7' y='0' width='2.4' height='{height}' />
<rect x='114.4' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='90.5' y='0' width='2.4' height='{height}' />
<rect x='128.8' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='95.3' y='0' width='2.4' height='{height}' />
<rect x='172' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='100.1' y='0' width='2.4' height='{height}' />
<rect x='176.8' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='104.9' y='0' width='2.4' height='{height}' />
<rect x='95.5' fill='white' width='2.1' height='4' /> <rect fill='#FFFFFF' x='109.7' y='0' width='2.4' height='{height}' />
<path fill='white' d='M182,0h-0.4v4h0.4c1.1,0,2-0.9,2-2S183.1,0,182,0z' /> <rect fill='#FFFFFF' x='114.5' y='0' width='2.4' height='{height}' />
<rect x='162.4' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='119.2' y='0' width='2.4' height='{height}' />
<rect x='152.8' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='124' y='0' width='2.4' height='{height}' />
<rect x='157.6' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='128.8' y='0' width='2.4' height='{height}' />
<rect x='167.2' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='133.6' y='0' width='2.4' height='{height}' />
<rect x='148' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='138.4' y='0' width='2.4' height='{height}' />
<rect x='17.2' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='143.2' y='0' width='2.4' height='{height}' />
<rect x='12.4' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='148' y='0' width='2.4' height='{height}' />
<rect x='3.1' fill='white' width='2.1' height='4' /> <rect fill='#FFFFFF' x='152.8' y='0' width='2.4' height='{height}' />
<rect x='7.6' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='157.6' y='0' width='2.4' height='{height}' />
<rect x='22' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='162.4' y='0' width='2.4' height='{height}' />
<rect x='41.2' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='167.2' y='0' width='2.4' height='{height}' />
<rect x='36.4' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='172' y='0' width='2.4' height='{height}' />
<rect x='26.8' fill='white' width='2.4' height='4' /> <rect fill='#FFFFFF' x='176.8' y='0' width='2.4' height='{height}' />
<path fill='white' d='M0.7,0.5C0.3,0.9,0,1.4,0,2s0.3,1.1,0.7,1.5V0.5z' /> <rect fill='#FFFFFF' x='181.6' y='0' width='2.4' height='{height}' />
<rect x='31.6' fill='white' width='2.4' height='4' />
</mask> </mask>
<rect className='fill-white/10' width='184' height='4' mask='url(#healthBarMask)' />
<rect <rect
className={classNames(backgroundColor, !reduceMotion && 'transition-all duration-500')} className='fill-white/10'
width='184'
height={height}
mask='url(#healthBarMask)'
/>
<rect
className={classNames(
backgroundColor,
!reduceMotion && 'transition-all duration-500',
)}
width={isUpdated && isIncrease ? updatedWidth : width} width={isUpdated && isIncrease ? updatedWidth : width}
height='4' height={height}
mask={isUpdated ? 'url(#backgroundHealthBarMask)' : 'url(#healthBarMask)'} mask={isUpdated ? 'url(#backgroundHealthBarMask)' : 'url(#healthBarMask)'}
/> />
{isUpdated && ( {isUpdated && (
@ -119,12 +148,13 @@ export default function HealthBar({
!reduceMotion && 'transition-all duration-500', !reduceMotion && 'transition-all duration-500',
)} )}
width={isUpdated && !isIncrease ? updatedWidth : width} width={isUpdated && !isIncrease ? updatedWidth : width}
height='4' height={height}
mask='url(#healthBarMask)' mask='url(#healthBarMask)'
/> />
)} )}
</svg> </svg>
</div> </div>
</>
</HealthTooltip> </HealthTooltip>
) )
} }

View File

@ -16,7 +16,7 @@ export default function HealthIcon(props: Props) {
return ( return (
<> <>
{!isLoading && health === 0 ? ( {!isLoading && health === 0 ? (
<ExclamationMarkCircled className='w-5 text-loss animate-pulse' /> <ExclamationMarkCircled className={classNames('w-5 text-loss animate-pulse', className)} />
) : ( ) : (
<Heart <Heart
className={classNames( className={classNames(

View File

@ -17,7 +17,7 @@ export default function BorrowRate(props: Props) {
return ( return (
<FormattedNumber <FormattedNumber
className='justify-end text-xs' className='justify-end text-xs'
amount={props.borrowRate * 100} amount={props.borrowRate}
options={{ minDecimals: 2, maxDecimals: 2, suffix: '%' }} options={{ minDecimals: 2, maxDecimals: 2, suffix: '%' }}
animate animate
/> />

View File

@ -0,0 +1,38 @@
import { FormattedNumber } from 'components/FormattedNumber'
import TitleAndSubCell from 'components/TitleAndSubCell'
import { VAULT_DEPOSIT_BUFFER } from 'constants/vaults'
import { getAssetByDenom } from 'utils/assets'
interface Props {
depositCap: DepositCap
}
export default function DepositCapCell(props: Props) {
const percent = props.depositCap.used
.dividedBy(props.depositCap.max.multipliedBy(VAULT_DEPOSIT_BUFFER))
.multipliedBy(100)
.integerValue()
const decimals = getAssetByDenom(props.depositCap.denom)?.decimals ?? 6
return (
<TitleAndSubCell
title={
<FormattedNumber
amount={props.depositCap.max.toNumber()}
options={{ minDecimals: 2, abbreviated: true, decimals }}
className='text-xs'
animate
/>
}
sub={
<FormattedNumber
amount={percent.toNumber()}
options={{ minDecimals: 2, maxDecimals: 2, suffix: '% Filled' }}
className='text-xs'
animate
/>
}
/>
)
}

View File

@ -1,10 +1,7 @@
import { Row } from '@tanstack/react-table' import { Row } from '@tanstack/react-table'
import { FormattedNumber } from 'components/FormattedNumber' import DepositCapCell from 'components/DepositCapCell'
import Loading from 'components/Loading' import Loading from 'components/Loading'
import TitleAndSubCell from 'components/TitleAndSubCell'
import { VAULT_DEPOSIT_BUFFER } from 'constants/vaults'
import { getAssetByDenom } from 'utils/assets'
export const DEPOSIT_CAP_META = { accessorKey: 'cap', header: 'Deposit Cap' } export const DEPOSIT_CAP_META = { accessorKey: 'cap', header: 'Deposit Cap' }
@ -27,31 +24,5 @@ export default function DepositCap(props: Props) {
if (props.isLoading) return <Loading /> if (props.isLoading) return <Loading />
const percent = vault.cap.used return <DepositCapCell depositCap={vault.cap} />
.dividedBy(vault.cap.max.multipliedBy(VAULT_DEPOSIT_BUFFER))
.multipliedBy(100)
.integerValue()
const decimals = getAssetByDenom(vault.cap.denom)?.decimals ?? 6
return (
<TitleAndSubCell
title={
<FormattedNumber
amount={vault.cap.max.toNumber()}
options={{ minDecimals: 2, abbreviated: true, decimals }}
className='text-xs'
animate
/>
}
sub={
<FormattedNumber
amount={percent.toNumber()}
options={{ minDecimals: 2, maxDecimals: 2, suffix: '% Filled' }}
className='text-xs'
animate
/>
}
/>
)
} }

View File

@ -74,7 +74,7 @@ export default function VaultCard(props: Props) {
abbreviated: true, abbreviated: true,
decimals: getAssetByDenom(props.vault.cap.denom)?.decimals, decimals: getAssetByDenom(props.vault.cap.denom)?.decimals,
})} })}
sub={'Depo. Cap'} sub={'Deposit Cap'}
/> />
</div> </div>
<ActionButton onClick={openVaultModal} color='secondary' text='Deposit' className='w-full' /> <ActionButton onClick={openVaultModal} color='secondary' text='Deposit' className='w-full' />

View File

@ -14,7 +14,7 @@ export default function Apr(props: Props) {
return ( return (
<AssetRate <AssetRate
rate={convertAprToApy((props.marketLiquidityRate ?? 0) * 100, 365)} rate={convertAprToApy(props.marketLiquidityRate ?? 0, 365)}
isEnabled={props.borrowEnabled} isEnabled={props.borrowEnabled}
className='justify-end text-xs' className='justify-end text-xs'
type='apy' type='apy'

View File

@ -7,7 +7,7 @@ import { demagnify } from 'utils/formatters'
export const DEPOSIT_CAP_META = { export const DEPOSIT_CAP_META = {
accessorKey: 'marketDepositCap', accessorKey: 'marketDepositCap',
header: 'Depo. Cap', header: 'Deposit Cap',
id: 'marketDepositCap', id: 'marketDepositCap',
} }

View File

@ -0,0 +1,11 @@
import React from 'react'
import Text from 'components/Text'
export default function HLSTag() {
return (
<Text tag='span' className='rounded-sm gradient-hls px-2 font-bold py-0.5 ml-2' size='xs'>
HLS
</Text>
)
}

View File

@ -0,0 +1,24 @@
import { NAME_META } from 'components/HLS/Farm/Table/Columns/Name'
import useDepositedColumns from 'components/HLS/Staking/Table/Columns/useDepositedColumns'
import Table from 'components/Table'
import useHLSStakingAccounts from 'hooks/useHLSStakingAccounts'
import useStore from 'store'
const title = 'Active Strategies'
export default function ActiveStakingAccounts() {
const address = useStore((s) => s.address)
const columns = useDepositedColumns({ isLoading: false })
const { data: hlsStakingAccounts } = useHLSStakingAccounts(address)
if (!hlsStakingAccounts.length) return null
return (
<Table
title={title}
columns={columns}
data={hlsStakingAccounts}
initialSorting={[{ id: NAME_META.id, desc: true }]}
/>
)
}

View File

@ -6,7 +6,7 @@ import Table from 'components/Table'
import useHLSStakingAssets from 'hooks/useHLSStakingAssets' import useHLSStakingAssets from 'hooks/useHLSStakingAssets'
import { getEnabledMarketAssets } from 'utils/assets' import { getEnabledMarketAssets } from 'utils/assets'
const title = 'Available HLS Staking' const title = 'Available Strategies'
function Content() { function Content() {
const assets = getEnabledMarketAssets() const assets = getEnabledMarketAssets()

View File

@ -0,0 +1,30 @@
import React from 'react'
import HealthBar from 'components/Account/Health/HealthBar'
import TitleAndSubCell from 'components/TitleAndSubCell'
import useHealthComputer from 'hooks/useHealthComputer'
export const ACCOUNT_META = { id: 'account', header: 'Account', accessorKey: 'id' }
interface Props {
account: HLSAccountWithStrategy
}
export default function Name(props: Props) {
const { health, healthFactor } = useHealthComputer(props.account)
return (
<TitleAndSubCell
className=''
title={`Account ${props.account.id}`}
sub={
<HealthBar
health={health}
healthFactor={healthFactor}
className=''
showIcon
height='10'
iconClassName='mr-0.5 w-3'
/>
}
/>
)
}

View File

@ -0,0 +1,22 @@
import { Row } from '@tanstack/react-table'
import React from 'react'
import TitleAndSubCell from 'components/TitleAndSubCell'
export const ACTIVE_APY_META = { header: 'APY', accessorKey: 'strategy' }
export const activeApySortingFn = (
a: Row<HLSAccountWithStrategy>,
b: Row<HLSAccountWithStrategy>,
): number => {
// TODO: Properly implement this
return 0
}
interface Props {
account: HLSAccountWithStrategy
}
export default function ActiveAPY(props: Props) {
return <TitleAndSubCell title={'-'} sub={'-%/day'} />
}

View File

@ -0,0 +1,25 @@
import { Row } from '@tanstack/react-table'
import DisplayCurrency from 'components/DisplayCurrency'
import { BNCoin } from 'types/classes/BNCoin'
export const DEBT_VAL_META = { header: 'Debt Value', accessorKey: 'values.debt' }
interface Props {
account: HLSAccountWithStrategy
}
export function debtValueSorting(
a: Row<HLSAccountWithStrategy>,
b: Row<HLSAccountWithStrategy>,
): number {
return a.original.values.debt.minus(b.original.values.debt).toNumber()
}
export default function DebtValue(props: Props) {
return (
<DisplayCurrency
coin={BNCoin.fromDenomAndBigNumber('usd', props.account.values.debt)}
className='text-xs'
/>
)
}

View File

@ -0,0 +1,23 @@
import { Row } from '@tanstack/react-table'
import React from 'react'
import DepositCapCell from 'components/DepositCapCell'
export const CAP_META = { header: 'Cap', accessorKey: 'strategy.depositCap' }
export const depositCapSortingFn = (
a: Row<HLSAccountWithStrategy>,
b: Row<HLSAccountWithStrategy>,
): number => {
const depositCapA = a.original.strategy.depositCap.max
const depositCapB = b.original.strategy.depositCap.max
return depositCapA.minus(depositCapB).toNumber()
}
interface Props {
account: HLSAccountWithStrategy
}
export default function Name(props: Props) {
return <DepositCapCell depositCap={props.account.strategy.depositCap} />
}

View File

@ -0,0 +1,25 @@
import { Row } from '@tanstack/react-table'
import React from 'react'
import { FormattedNumber } from 'components/FormattedNumber'
export const LEV_META = { accessorKey: 'leverage ', header: 'Leverage' }
interface Props {
account: HLSAccountWithStrategy
}
export function leverageSortingFn(a: Row<HLSAccountWithStrategy>, b: Row<HLSAccountWithStrategy>) {
return a.original.leverage - b.original.leverage
}
export default function MaxLeverage(props: Props) {
return (
<FormattedNumber
amount={props.account.leverage}
options={{ minDecimals: 2, maxDecimals: 2, suffix: 'x' }}
className='text-xs'
animate
/>
)
}

View File

@ -0,0 +1,14 @@
import React from 'react'
import Button from 'components/Button'
export const MANAGE_META = { id: 'manage' }
interface Props {
account: HLSAccountWithStrategy
}
export default function Manage(props: Props) {
// TODO: Impelement dropdown
return <Button text='Manage' color='tertiary' />
}

View File

@ -5,7 +5,7 @@ import Loading from 'components/Loading'
import TitleAndSubCell from 'components/TitleAndSubCell' import TitleAndSubCell from 'components/TitleAndSubCell'
import { getAssetByDenom } from 'utils/assets' import { getAssetByDenom } from 'utils/assets'
export const NAME_META = { id: 'name', header: 'Vault', accessorKey: 'denoms.deposit' } export const NAME_META = { id: 'name', header: 'Strategy', accessorKey: 'strategy.denoms.deposit' }
interface Props { interface Props {
strategy: HLSStrategy strategy: HLSStrategy
} }
@ -21,8 +21,8 @@ export default function Name(props: Props) {
{depositAsset && borrowAsset ? ( {depositAsset && borrowAsset ? (
<TitleAndSubCell <TitleAndSubCell
className='ml-2 mr-2 text-left' className='ml-2 mr-2 text-left'
title={`${depositAsset.symbol}/${borrowAsset.symbol}`} title={`${depositAsset.symbol} - ${borrowAsset.symbol}`}
sub='Staking' sub='Via MARS'
/> />
) : ( ) : (
<Loading /> <Loading />

View File

@ -0,0 +1,25 @@
import { Row } from '@tanstack/react-table'
import DisplayCurrency from 'components/DisplayCurrency'
import { BNCoin } from 'types/classes/BNCoin'
export const NET_VAL_META = { header: 'Net Value', accessorKey: 'values.net' }
interface Props {
account: HLSAccountWithStrategy
}
export function netValueSorting(
a: Row<HLSAccountWithStrategy>,
b: Row<HLSAccountWithStrategy>,
): number {
return a.original.values.net.minus(b.original.values.net).toNumber()
}
export default function NetValue(props: Props) {
return (
<DisplayCurrency
coin={BNCoin.fromDenomAndBigNumber('usd', props.account.values.net)}
className='text-xs'
/>
)
}

View File

@ -0,0 +1,25 @@
import { Row } from '@tanstack/react-table'
import DisplayCurrency from 'components/DisplayCurrency'
import { BNCoin } from 'types/classes/BNCoin'
export const POS_VAL_META = { header: 'Pos. Value', accessorKey: 'values.total' }
interface Props {
account: HLSAccountWithStrategy
}
export function positionValueSorting(
a: Row<HLSAccountWithStrategy>,
b: Row<HLSAccountWithStrategy>,
): number {
return a.original.values.total.minus(b.original.values.total).toNumber()
}
export default function PositionValue(props: Props) {
return (
<DisplayCurrency
coin={BNCoin.fromDenomAndBigNumber('usd', props.account.values.total)}
className='text-xs'
/>
)
}

View File

@ -0,0 +1,84 @@
import { ColumnDef } from '@tanstack/react-table'
import React, { useMemo } from 'react'
import Account, { ACCOUNT_META } from 'components/HLS/Staking/Table/Columns/Account'
import ActiveApy, {
ACTIVE_APY_META,
activeApySortingFn,
} from 'components/HLS/Staking/Table/Columns/ActiveApy'
import DebtValue, {
DEBT_VAL_META,
debtValueSorting,
} from 'components/HLS/Staking/Table/Columns/DebtValue'
import DepositCap, {
CAP_META,
depositCapSortingFn,
} from 'components/HLS/Staking/Table/Columns/DepositCap'
import Leverage, {
LEV_META,
leverageSortingFn,
} from 'components/HLS/Staking/Table/Columns/Leverage'
import Manage, { MANAGE_META } from 'components/HLS/Staking/Table/Columns/Manage'
import Name, { NAME_META } from 'components/HLS/Staking/Table/Columns/Name'
import NetValue, {
NET_VAL_META,
netValueSorting,
} from 'components/HLS/Staking/Table/Columns/NetValue'
import PositionValue, {
POS_VAL_META,
positionValueSorting,
} from 'components/HLS/Staking/Table/Columns/PositionValue'
interface Props {
isLoading: boolean
}
export default function useDepositedColumns(props: Props) {
return useMemo<ColumnDef<HLSAccountWithStrategy>[]>(
() => [
{
...NAME_META,
cell: ({ row }) => <Name strategy={row.original.strategy} />,
},
{
...ACCOUNT_META,
cell: ({ row }) => <Account account={row.original} />,
},
{
...LEV_META,
cell: ({ row }) => <Leverage account={row.original} />,
sortingFn: leverageSortingFn,
},
{
...POS_VAL_META,
cell: ({ row }) => <PositionValue account={row.original} />,
sortingFn: positionValueSorting,
},
{
...NET_VAL_META,
cell: ({ row }) => <NetValue account={row.original} />,
sortingFn: netValueSorting,
},
{
...DEBT_VAL_META,
cell: ({ row }) => <DebtValue account={row.original} />,
sortingFn: debtValueSorting,
},
{
...CAP_META,
cell: ({ row }) => <DepositCap account={row.original} />,
sortingFn: depositCapSortingFn,
},
{
...ACTIVE_APY_META,
cell: ({ row }) => <ActiveApy account={row.original} />,
sortingFn: activeApySortingFn,
},
{
...MANAGE_META,
cell: ({ row }) => <Manage account={row.original} />,
},
],
[],
)
}

View File

@ -30,7 +30,7 @@ export default function useAssetTableColumns(isBorrow: boolean) {
const borrowAsset = row.original.asset as BorrowAsset const borrowAsset = row.original.asset as BorrowAsset
const showRate = !borrowAsset?.borrowRate const showRate = !borrowAsset?.borrowRate
const rate = isBorrow ? market?.borrowRate : market?.liquidityRate const rate = isBorrow ? market?.borrowRate : market?.liquidityRate
const apy = convertAprToApy((rate ?? 0) * 100, 365) const apy = convertAprToApy(rate ?? 0, 365)
return ( return (
<div className='flex items-center'> <div className='flex items-center'>

View File

@ -1,17 +0,0 @@
import React from 'react'
import Button from 'components/Button'
import { ArrowRight } from 'components/Icons'
interface Props {
hlsAccounts: AccountIdAndKind[]
onClickBtn: () => void
}
export default function ChooseAccount(props: Props) {
return (
<div className='p-4'>
<Button onClick={props.onClickBtn} text='Continue' rightIcon={<ArrowRight />} />
</div>
)
}

View File

@ -65,6 +65,7 @@ export default function useAccordionItems(props: Props) {
onChangeAmount={props.onChangeDebt} onChangeAmount={props.onChangeDebt}
onClickBtn={() => props.toggleIsOpen(2)} onClickBtn={() => props.toggleIsOpen(2)}
max={props.maxBorrowAmount} max={props.maxBorrowAmount}
positionValue={props.positionValue}
/> />
), ),
renderSubTitle: () => ( renderSubTitle: () => (
@ -78,7 +79,7 @@ export default function useAccordionItems(props: Props) {
toggleOpen: props.toggleIsOpen, toggleOpen: props.toggleIsOpen,
}, },
...[ ...[
props.hlsAccounts.length > 2 props.emptyHlsAccounts.length > 0
? { ? {
title: 'Select HLS Account', title: 'Select HLS Account',
renderContent: () => ( renderContent: () => (

View File

@ -1,8 +1,14 @@
import { useCallback } from 'react' import { useCallback, useMemo } from 'react'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import useDepositHlsVault from 'hooks/useDepositHlsVault' import useDepositHlsVault from 'hooks/useDepositHlsVault'
import useHealthComputer from 'hooks/useHealthComputer'
import useLocalStorage from 'hooks/useLocalStorage'
import { useUpdatedAccount } from 'hooks/useUpdatedAccount' import { useUpdatedAccount } from 'hooks/useUpdatedAccount'
import { BN } from 'utils/helpers' import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { Action } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
interface Props { interface Props {
borrowAsset: Asset borrowAsset: Asset
@ -12,6 +18,8 @@ interface Props {
export default function useVaultController(props: Props) { export default function useVaultController(props: Props) {
const { collateralAsset, borrowAsset, selectedAccount } = props const { collateralAsset, borrowAsset, selectedAccount } = props
const [slippage] = useLocalStorage<number>(LocalStorageKeys.SLIPPAGE, DEFAULT_SETTINGS.slippage)
const addToStakingStrategy = useStore((s) => s.addToStakingStrategy)
const { const {
leverage, leverage,
@ -25,17 +33,66 @@ export default function useVaultController(props: Props) {
borrowDenom: borrowAsset.denom, borrowDenom: borrowAsset.denom,
}) })
const actions = [] const depositCoin = useMemo(
() => BNCoin.fromDenomAndBigNumber(collateralAsset.denom, depositAmount),
[collateralAsset.denom, depositAmount],
)
const { updatedAccount, simulateVaultDeposit } = useUpdatedAccount(selectedAccount) const borrowCoin = useMemo(
() => BNCoin.fromDenomAndBigNumber(borrowAsset.denom, borrowAmount),
[borrowAsset.denom, borrowAmount],
)
const execute = () => null 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)
const maxBorrowAmount = useMemo(() => {
// TODO: Perhaps we need a specific target for this -> target = swap
return computeMaxBorrowAmount(props.borrowAsset.denom, 'deposit')
}, [computeMaxBorrowAmount, props.borrowAsset.denom])
const execute = useCallback(() => {
addToStakingStrategy({
actions,
accountId: selectedAccount.id,
borrowCoin: BNCoin.fromDenomAndBigNumber(borrowAsset.denom, borrowAmount),
depositCoin: BNCoin.fromDenomAndBigNumber(collateralAsset.denom, depositAmount),
})
}, [
actions,
addToStakingStrategy,
borrowAmount,
borrowAsset.denom,
collateralAsset.denom,
depositAmount,
selectedAccount.id,
])
const onChangeCollateral = useCallback( const onChangeCollateral = useCallback(
(amount: BigNumber) => { (amount: BigNumber) => {
setDepositAmount(amount) setDepositAmount(amount)
addDeposits([BNCoin.fromDenomAndBigNumber(collateralAsset.denom, amount)])
}, },
[setDepositAmount], [addDeposits, collateralAsset.denom, setDepositAmount],
) )
const onChangeDebt = useCallback( const onChangeDebt = useCallback(
@ -50,7 +107,7 @@ export default function useVaultController(props: Props) {
depositAmount, depositAmount,
execute, execute,
leverage, leverage,
maxBorrowAmount: BN(0), maxBorrowAmount,
onChangeCollateral, onChangeCollateral,
onChangeDebt, onChangeDebt,
positionValue, positionValue,

View File

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import DoubleLogo from 'components/DoubleLogo' import DoubleLogo from 'components/DoubleLogo'
import HLSTag from 'components/HLS/HLSTag'
import Text from 'components/Text' import Text from 'components/Text'
import { getAssetByDenom } from 'utils/assets' import { getAssetByDenom } from 'utils/assets'
@ -19,9 +20,7 @@ export default function Header(props: Props) {
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<DoubleLogo primaryDenom={props.primaryDenom} secondaryDenom={props.secondaryDenom} /> <DoubleLogo primaryDenom={props.primaryDenom} secondaryDenom={props.secondaryDenom} />
<Text>{`${primaryAsset.symbol} - ${secondaryAsset.symbol}`}</Text> <Text>{`${primaryAsset.symbol} - ${secondaryAsset.symbol}`}</Text>
<Text className='rounded-sm gradient-hls px-2 font-bold py-0.5' size='xs'> <HLSTag />
HLS
</Text>
</div> </div>
) )
} }

View File

@ -11,6 +11,7 @@ interface Props {
max: BigNumber max: BigNumber
onChangeAmount: (amount: BigNumber) => void onChangeAmount: (amount: BigNumber) => void
onClickBtn: () => void onClickBtn: () => void
positionValue: BigNumber
} }
export default function Leverage(props: Props) { export default function Leverage(props: Props) {
@ -23,7 +24,7 @@ export default function Leverage(props: Props) {
onChange={props.onChangeAmount} onChange={props.onChangeAmount}
maxText='Max borrow' maxText='Max borrow'
/> />
<LeverageSummary asset={props.asset} /> <LeverageSummary asset={props.asset} positionValue={props.positionValue} />
<Button onClick={props.onClickBtn} text='Continue' rightIcon={<ArrowRight />} /> <Button onClick={props.onClickBtn} text='Continue' rightIcon={<ArrowRight />} />
</div> </div>
) )

View File

@ -2,14 +2,19 @@ import React, { useMemo } from 'react'
import { FormattedNumber } from 'components/FormattedNumber' import { FormattedNumber } from 'components/FormattedNumber'
import Text from 'components/Text' import Text from 'components/Text'
import useBorrowAsset from 'hooks/useBorrowAsset'
interface Props { interface Props {
asset: Asset asset: Asset
positionValue: BigNumber
} }
export default function LeverageSummary(props: Props) { export default function LeverageSummary(props: Props) {
const borrowAsset = useBorrowAsset(props.asset.denom)
const items: { title: string; amount: number; options: FormatOptions }[] = useMemo(() => { const items: { title: string; amount: number; options: FormatOptions }[] = useMemo(() => {
return [ return [
// TODO: Get APY numbers
{ {
title: 'APY', title: 'APY',
amount: 0, amount: 0,
@ -17,16 +22,16 @@ export default function LeverageSummary(props: Props) {
}, },
{ {
title: `Borrow APR ${props.asset.symbol}`, title: `Borrow APR ${props.asset.symbol}`,
amount: 0, amount: borrowAsset?.borrowRate || 0,
options: { suffix: '%', minDecimals: 1, maxDecimals: 1 }, options: { suffix: '%', minDecimals: 2, maxDecimals: 2 },
}, },
{ {
title: 'Total Position Size', title: 'Total Position Size',
amount: 0, amount: props.positionValue.toNumber(),
options: { prefix: '$' }, options: { prefix: '$' },
}, },
] ]
}, [props.asset.symbol]) }, [borrowAsset?.borrowRate, props.asset.symbol, props.positionValue])
return ( return (
<div className='grid grid-cols-2 gap-2'> <div className='grid grid-cols-2 gap-2'>

View File

@ -48,6 +48,14 @@ export default function YourPosition(props: Props) {
className='text-white/60 place-self-end text-xs' className='text-white/60 place-self-end text-xs'
/> />
</div> </div>
<div className='flex justify-between mb-2'>
<Text className='text-white/60 text-xs'>Leverage</Text>
<FormattedNumber
amount={props.leverage}
options={{ suffix: 'x' }}
className='text-white/60 place-self-end text-xs'
/>
</div>
<div className='flex justify-between'> <div className='flex justify-between'>
<Text className='text-xs group/apytooltip' tag='span'> <Text className='text-xs group/apytooltip' tag='span'>
<Tooltip <Tooltip

View File

@ -13,7 +13,7 @@ interface Props {
} }
function Content(props: Props) { function Content(props: Props) {
const { data: account } = useAccount('high_levered_strategy', props.accountId, true) const { data: account } = useAccount(props.accountId, true)
const { data } = useBorrowMarketAssetsTableData(false) const { data } = useBorrowMarketAssetsTableData(false)
const borrowAssets = useMemo(() => data?.allAssets || [], [data]) const borrowAssets = useMemo(() => data?.allAssets || [], [data])

View File

@ -17,7 +17,7 @@ interface Props {
} }
function Content(props: Props) { function Content(props: Props) {
const { data: account } = useAccount('default', props.accountId, true) const { data: account } = useAccount(props.accountId, true)
const { data: prices } = usePrices() const { data: prices } = usePrices()
const { health, healthFactor } = useHealthComputer(account) const { health, healthFactor } = useHealthComputer(account)
const { data } = useBorrowMarketAssetsTableData(false) const { data } = useBorrowMarketAssetsTableData(false)
@ -80,6 +80,7 @@ function Content(props: Props) {
health={health} health={health}
healthFactor={healthFactor} healthFactor={healthFactor}
title={`Credit Account ${props.accountId}`} title={`Credit Account ${props.accountId}`}
accountId={props.accountId}
/> />
) )
} }
@ -88,7 +89,12 @@ export default function Summary(props: Props) {
return ( return (
<Suspense <Suspense
fallback={ fallback={
<Skeleton health={0} healthFactor={0} title={`Credit Account ${props.accountId}`} /> <Skeleton
health={0}
healthFactor={0}
title={`Credit Account ${props.accountId}`}
accountId={props.accountId}
/>
} }
> >
<Content {...props} /> <Content {...props} />

View File

@ -1,8 +1,8 @@
import React from 'react' import React from 'react'
import HealthBar from 'components/Account/Health/HealthBar' import HealthBar from 'components/Account/Health/HealthBar'
import HealthIcon from 'components/Account/Health/HealthIcon'
import Card from 'components/Card' import Card from 'components/Card'
import HLSTag from 'components/HLS/HLSTag'
import Text from 'components/Text' import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell' import TitleAndSubCell from 'components/TitleAndSubCell'
@ -12,6 +12,7 @@ interface Props {
healthFactor: number healthFactor: number
accountId: string accountId: string
isCurrent?: boolean isCurrent?: boolean
isHls?: boolean
} }
export default function Skeleton(props: Props) { export default function Skeleton(props: Props) {
@ -19,7 +20,10 @@ export default function Skeleton(props: Props) {
return ( return (
<Card className='p-4 bg-white/5'> <Card className='p-4 bg-white/5'>
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<Text>Credit Account {accountId}</Text> <Text>
Credit Account {accountId}
{props.isHls && <HLSTag />}
</Text>
<Text size='xs' className='text-white/60'> <Text size='xs' className='text-white/60'>
{isCurrent && '(current)'} {isCurrent && '(current)'}
</Text> </Text>
@ -30,13 +34,7 @@ export default function Skeleton(props: Props) {
))} ))}
</div> </div>
<div className='flex gap-1 mt-6'> <div className='flex gap-1 mt-6'>
<HealthIcon <HealthBar health={health} healthFactor={healthFactor} showIcon />
isLoading={healthFactor === 0}
health={health}
className='w-5'
colorClass='text-white/60'
/>
<HealthBar health={health} healthFactor={healthFactor} />
</div> </div>
</Card> </Card>
) )

View File

@ -27,7 +27,7 @@ interface Props {
} }
export default function PortfolioCard(props: Props) { export default function PortfolioCard(props: Props) {
const { data: account } = useAccount('default', props.accountId) const { data: account } = useAccount(props.accountId)
const { health, healthFactor } = useHealthComputer(account) const { health, healthFactor } = useHealthComputer(account)
const { address: urlAddress } = useParams() const { address: urlAddress } = useParams()
const { data: prices } = usePrices() const { data: prices } = usePrices()
@ -110,6 +110,7 @@ export default function PortfolioCard(props: Props) {
healthFactor={healthFactor} healthFactor={healthFactor}
accountId={props.accountId} accountId={props.accountId}
isCurrent={props.accountId === currentAccountId} isCurrent={props.accountId === currentAccountId}
isHls={account.kind === 'high_levered_strategy'}
/> />
</NavLink> </NavLink>
) )

View File

@ -91,5 +91,5 @@ export default function PortfolioSummary() {
if (!walletAddress && !urlAddress) return null if (!walletAddress && !urlAddress) return null
return <SummarySkeleton title='Portfolio Summary' stats={stats} /> return <SummarySkeleton title='Portfolio Summary' stats={stats} accountId='' />
} }

View File

@ -3,9 +3,11 @@ import React from 'react'
import HealthBar from 'components/Account/Health/HealthBar' import HealthBar from 'components/Account/Health/HealthBar'
import HealthIcon from 'components/Account/Health/HealthIcon' import HealthIcon from 'components/Account/Health/HealthIcon'
import Card from 'components/Card' import Card from 'components/Card'
import HLSTag from 'components/HLS/HLSTag'
import Loading from 'components/Loading' import Loading from 'components/Loading'
import Text from 'components/Text' import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell' import TitleAndSubCell from 'components/TitleAndSubCell'
import useAccount from 'hooks/useAccount'
import { DEFAULT_PORTFOLIO_STATS } from 'utils/constants' import { DEFAULT_PORTFOLIO_STATS } from 'utils/constants'
interface Props { interface Props {
@ -13,16 +15,21 @@ interface Props {
health?: number health?: number
healthFactor?: number healthFactor?: number
title: string title: string
accountId: string
} }
export default function SummarySkeleton(props: Props) { export default function SummarySkeleton(props: Props) {
const { health, healthFactor, title } = props const { health, healthFactor, title } = props
const stats = props.stats || DEFAULT_PORTFOLIO_STATS const stats = props.stats || DEFAULT_PORTFOLIO_STATS
const { data: account } = useAccount(props.accountId, false)
return ( return (
<div className='flex flex-col w-full gap-8'> <div className='flex flex-col w-full gap-8'>
<div className='flex justify-between'> <div className='flex justify-between'>
<div className='flex items-center'>
<Text size='2xl'>{title}</Text> <Text size='2xl'>{title}</Text>
{account?.kind === 'high_levered_strategy' && <HLSTag />}
</div>
{health !== undefined && healthFactor !== undefined && ( {health !== undefined && healthFactor !== undefined && (
<div className='flex gap-1 max-w-[300px] flex-grow'> <div className='flex gap-1 max-w-[300px] flex-grow'>
<HealthIcon isLoading={healthFactor === 0} health={health} className='w-5' /> <HealthIcon isLoading={healthFactor === 0} health={health} className='w-5' />

View File

@ -80,7 +80,7 @@ export default function Option(props: Props) {
})} })}
</Text> </Text>
<AssetRate <AssetRate
rate={convertAprToApy((marketAsset?.borrowRate ?? 0) * 100, 365)} rate={convertAprToApy(marketAsset?.borrowRate ?? 0, 365)}
isEnabled={marketAsset?.borrowEnabled ?? false} isEnabled={marketAsset?.borrowEnabled ?? false}
className='col-span-2 text-white/50' className='col-span-2 text-white/50'
type='apy' type='apy'

View File

@ -45,7 +45,7 @@ export default function Row<T>(props: Props<T>) {
key={cell.id} key={cell.id}
className={classNames( className={classNames(
isSymbolOrName ? 'text-left' : 'text-right', isSymbolOrName ? 'text-left' : 'text-right',
props.spacingClassName ?? 'p-4', props.spacingClassName ?? 'px-3 py-4',
borderClasses, borderClasses,
)} )}
> >

View File

@ -79,10 +79,10 @@ export default function Table<T>(props: Props<T>) {
'align-center', 'align-center',
)} )}
> >
<span className='w-6 h-6 text-white'> <span className='w-5 h-5 text-white'>
{header.column.getCanSort() {header.column.getCanSort()
? { ? {
asc: <SortAsc />, asc: <SortAsc size={16} />,
desc: <SortDesc />, desc: <SortDesc />,
false: <SortNone />, false: <SortNone />,
}[header.column.getIsSorted() as string] ?? null }[header.column.getIsSorted() as string] ?? null
@ -90,8 +90,8 @@ export default function Table<T>(props: Props<T>) {
</span> </span>
<Text <Text
tag='span' tag='span'
size='sm' size='xs'
className='flex items-center font-normal text-white/70' className='flex items-center font-normal text-white/60'
> >
{flexRender(header.column.columnDef.header, header.getContext())} {flexRender(header.column.columnDef.header, header.getContext())}
</Text> </Text>

View File

@ -58,6 +58,7 @@ export const ASSETS: Asset[] = [
isDisplayCurrency: ENV.NETWORK !== NETWORK.TESTNET, isDisplayCurrency: ENV.NETWORK !== NETWORK.TESTNET,
isAutoLendEnabled: false, isAutoLendEnabled: false,
poolId: 803, poolId: 803,
isStaking: true,
}, },
{ {
symbol: 'WBTC.axl', symbol: 'WBTC.axl',

View File

@ -1,10 +1,9 @@
import useSWR from 'swr' import useSWR from 'swr'
import getAccount from 'api/accounts/getAccount' import getAccount from 'api/accounts/getAccount'
import { AccountKind } from 'types/generated/mars-rover-health-computer/MarsRoverHealthComputer.types'
export default function useAccount(kind: AccountKind, accountId?: string, suspense?: boolean) { export default function useAccount(accountId?: string, suspense?: boolean) {
return useSWR(`account${accountId}`, () => getAccount({ id: accountId || '', kind }), { return useSWR(`account${accountId}`, () => getAccount(accountId), {
suspense: suspense, suspense: suspense,
revalidateOnFocus: false, revalidateOnFocus: false,
}) })

View File

@ -0,0 +1,11 @@
import useSWR from 'swr'
import getHLSStakingAccounts from 'api/hls/getHLSStakingAccounts'
export default function useHLSStakingAccounts(address?: string) {
return useSWR(`${address}/hlsStakingAccounts`, () => getHLSStakingAccounts(address), {
fallbackData: [],
suspense: true,
revalidateOnFocus: false,
})
}

View File

@ -1,4 +1,5 @@
import Tab from 'components/Earn/Tab' import Tab from 'components/Earn/Tab'
import ActiveStakingAccounts from 'components/HLS/Staking/ActiveStakingAccounts'
import AvailableHlsStakingAssets from 'components/HLS/Staking/AvailableHLSStakingAssets' import AvailableHlsStakingAssets from 'components/HLS/Staking/AvailableHLSStakingAssets'
import HLSStakingIntro from 'components/HLS/Staking/HLSStakingIntro' import HLSStakingIntro from 'components/HLS/Staking/HLSStakingIntro'
import MigrationBanner from 'components/MigrationBanner' import MigrationBanner from 'components/MigrationBanner'
@ -11,6 +12,7 @@ export default function HLSStakingPage() {
<Tab tabs={HLS_TABS} activeTabIdx={1} /> <Tab tabs={HLS_TABS} activeTabIdx={1} />
<HLSStakingIntro /> <HLSStakingIntro />
<AvailableHlsStakingAssets /> <AvailableHlsStakingAssets />
<ActiveStakingAccounts />
</div> </div>
) )
} }

View File

@ -1,9 +1,11 @@
import { MsgExecuteContract } from '@delphi-labs/shuttle-react' import { MsgExecuteContract } from '@delphi-labs/shuttle-react'
import BigNumber from 'bignumber.js'
import moment from 'moment' import moment from 'moment'
import { isMobile } from 'react-device-detect' import { isMobile } from 'react-device-detect'
import { GetState, SetState } from 'zustand' import { GetState, SetState } from 'zustand'
import { ENV } from 'constants/env' import { ENV } from 'constants/env'
import { BN_ZERO } from 'constants/math'
import { Store } from 'store' import { Store } from 'store'
import { BNCoin } from 'types/classes/BNCoin' import { BNCoin } from 'types/classes/BNCoin'
import { ExecuteMsg as AccountNftExecuteMsg } from 'types/generated/mars-account-nft/MarsAccountNft.types' import { ExecuteMsg as AccountNftExecuteMsg } from 'types/generated/mars-account-nft/MarsAccountNft.types'
@ -121,6 +123,7 @@ export default function createBroadcastSlice(
coins: changes.deposits?.map((debt) => debt.toCoin()) ?? [], coins: changes.deposits?.map((debt) => debt.toCoin()) ?? [],
text: action === 'vaultCreate' ? 'Created a Vault Position' : 'Added to Vault Position', text: action === 'vaultCreate' ? 'Created a Vault Position' : 'Added to Vault Position',
}) })
break
} }
set({ toast }) set({ toast })
@ -156,6 +159,41 @@ export default function createBroadcastSlice(
return { return {
toast: null, toast: null,
addToStakingStrategy: async (options: {
accountId: string
actions: Action[]
depositCoin: BNCoin
borrowCoin: BNCoin
}) => {
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, [
options.depositCoin.toCoin(),
]),
],
})
const swapOptions = { denomOut: options.depositCoin.denom, coinIn: options.borrowCoin }
get().setToast({
response,
options: {
action: 'hls-staking',
accountId: options.accountId,
changes: { deposits: [options.depositCoin], debts: [options.borrowCoin] },
},
swapOptions,
})
return response.then((response) => !!response.result)
},
borrow: async (options: { accountId: string; coin: BNCoin; borrowToWallet: boolean }) => { borrow: async (options: { accountId: string; coin: BNCoin; borrowToWallet: boolean }) => {
const borrowAction: Action = { borrow: options.coin.toCoin() } const borrowAction: Action = { borrow: options.coin.toCoin() }
const withdrawAction: Action = { withdraw: options.coin.toActionCoin() } const withdrawAction: Action = { withdraw: options.coin.toActionCoin() }
@ -687,12 +725,23 @@ export default function createBroadcastSlice(
getSingleValueFromBroadcastResult(response.result, 'wasm', 'token_id') ?? undefined getSingleValueFromBroadcastResult(response.result, 'wasm', 'token_id') ?? undefined
} }
if (toast.options.action === 'swap' && toast.swapOptions) { if (toast.swapOptions) {
const coinOut = getTokenOutFromSwapResponse(response, toast.swapOptions.denomOut) const coinOut = getTokenOutFromSwapResponse(response, toast.swapOptions.denomOut)
const successMessage = `Swapped ${formatAmountWithSymbol(
if (toast.options.action === 'swap') {
toast.options.message = `Swapped ${formatAmountWithSymbol(
toast.swapOptions.coinIn.toCoin(), toast.swapOptions.coinIn.toCoin(),
)} for ${formatAmountWithSymbol(coinOut)}` )} for ${formatAmountWithSymbol(coinOut)}`
toast.options.message = successMessage }
if (toast.options.action === 'hls-staking') {
const depositAmount: BigNumber = toast.options.changes?.deposits?.length
? toast.options.changes.deposits[0].amount
: BN_ZERO
coinOut.amount = depositAmount.plus(coinOut.amount).toFixed(0)
toast.options.message = `Added ${formatAmountWithSymbol(coinOut)}`
}
} }
handleResponseMessages({ handleResponseMessages({

View File

@ -29,3 +29,13 @@ interface AccountIdAndKind {
id: string id: string
kind: AccountKind kind: AccountKind
} }
interface HLSAccountWithStrategy extends Account {
leverage: number
strategy: HLSStrategy
values: {
net: BigNumber
debt: BigNumber
total: BigNumber
}
}

View File

@ -54,6 +54,7 @@ interface Asset {
pythPriceFeedId?: string pythPriceFeedId?: string
forceFetchPrice?: boolean forceFetchPrice?: boolean
testnetDenom?: string testnetDenom?: string
isStaking?: boolean
} }
interface PseudoAsset { interface PseudoAsset {

View File

@ -71,6 +71,7 @@ interface HandleResponseProps {
| 'unlock' | 'unlock'
| 'swap' | 'swap'
| 'oracle' | 'oracle'
| 'hls-staking'
lend?: boolean lend?: boolean
accountId?: string accountId?: string
changes?: { debts?: BNCoin[]; deposits?: BNCoin[]; lends?: BNCoin[] } changes?: { debts?: BNCoin[]; deposits?: BNCoin[]; lends?: BNCoin[] }
@ -79,6 +80,12 @@ interface HandleResponseProps {
} }
interface BroadcastSlice { interface BroadcastSlice {
addToStakingStrategy: (options: {
accountId: string
actions: Action[]
depositCoin: BNCoin
borrowCoin: BNCoin
}) => Promise<boolean>
borrow: (options: { borrow: (options: {
accountId: string accountId: string
coin: BNCoin coin: BNCoin

View File

@ -89,13 +89,13 @@ export const calculateAccountApr = (
const apr = const apr =
lendingAssetsData.find((lendingAsset) => lendingAsset.asset.denom === lend.denom) lendingAssetsData.find((lendingAsset) => lendingAsset.asset.denom === lend.denom)
?.marketLiquidityRate ?? 0 ?.marketLiquidityRate ?? 0
const positionInterest = amount.multipliedBy(price).multipliedBy(apr) const positionInterest = amount.multipliedBy(price).multipliedBy(apr).dividedBy(100)
totalLendsInterestValue = totalLendsInterestValue.plus(positionInterest) totalLendsInterestValue = totalLendsInterestValue.plus(positionInterest)
}) })
vaults?.forEach((vault) => { vaults?.forEach((vault) => {
const lockedValue = vault.values.primary.plus(vault.values.secondary) const lockedValue = vault.values.primary.plus(vault.values.secondary)
const positionInterest = lockedValue.multipliedBy(vault?.apr ?? 0) const positionInterest = lockedValue.multipliedBy(vault?.apr ?? 0).dividedBy(100)
totalVaultsInterestValue = totalVaultsInterestValue.plus(positionInterest) totalVaultsInterestValue = totalVaultsInterestValue.plus(positionInterest)
}) })
@ -107,15 +107,19 @@ export const calculateAccountApr = (
const apy = const apy =
borrowAssetsData.find((borrowAsset) => borrowAsset.asset.denom === debt.denom)?.borrowRate ?? borrowAssetsData.find((borrowAsset) => borrowAsset.asset.denom === debt.denom)?.borrowRate ??
0 0
const positionInterest = amount.multipliedBy(price).multipliedBy(convertApyToApr(apy, 365)) const positionInterest = amount
.multipliedBy(price)
.multipliedBy(convertApyToApr(apy, 365))
.dividedBy(100)
totalDebtInterestValue = totalDebtInterestValue.plus(positionInterest) totalDebtInterestValue = totalDebtInterestValue.plus(positionInterest)
}) })
const totalInterstValue = totalLendsInterestValue const totalInterestValue = totalLendsInterestValue
.plus(totalVaultsInterestValue) .plus(totalVaultsInterestValue)
.minus(totalDebtInterestValue) .minus(totalDebtInterestValue)
return totalInterstValue.dividedBy(totalNetValue).times(100) return totalInterestValue.dividedBy(totalNetValue).times(100)
} }
export function calculateAccountLeverage(account: Account, prices: BNCoin[]) { export function calculateAccountLeverage(account: Account, prices: BNCoin[]) {

View File

@ -43,3 +43,7 @@ export function getLendEnabledAssets() {
export function getBorrowEnabledAssets() { export function getBorrowEnabledAssets() {
return ASSETS.filter((asset) => asset.isBorrowEnabled) return ASSETS.filter((asset) => asset.isBorrowEnabled)
} }
export function getStakingAssets() {
return ASSETS.filter((asset) => asset.isStaking)
}

View File

@ -148,7 +148,7 @@ export function formatLeverage(leverage: number) {
} }
export function formatPercent(percent: number | string, minDecimals?: number) { export function formatPercent(percent: number | string, minDecimals?: number) {
return formatValue(+percent * 100, { return formatValue(+percent, {
minDecimals: minDecimals ?? 0, minDecimals: minDecimals ?? 0,
suffix: '%', suffix: '%',
}) })
@ -209,6 +209,5 @@ export function getCoinAmount(denom: string, value: BigNumber, prices: BNCoin[])
} }
export function convertLiquidityRateToAPR(rate: number) { export function convertLiquidityRateToAPR(rate: number) {
const rateMulHundred = rate * 100 return rate >= 0.01 ? rate : 0.0
return rateMulHundred >= 0.01 ? rateMulHundred : 0.0
} }

View File

@ -14,8 +14,8 @@ export const convertAprToApy = (apr: number, numberOfCompoundingPeriods: number)
} }
export const convertApyToApr = (apy: number, numberOfCompoundingPeriods: number): number => { export const convertApyToApr = (apy: number, numberOfCompoundingPeriods: number): number => {
const periodicRate = (1 + apy) ** (1 / numberOfCompoundingPeriods) - 1 const periodicRate = (1 + apy / 100) ** (1 / numberOfCompoundingPeriods) - 1
return periodicRate * numberOfCompoundingPeriods return periodicRate * numberOfCompoundingPeriods * 100
} }
export const combineBNCoins = (coins: BNCoin[]): BNCoin[] => { export const combineBNCoins = (coins: BNCoin[]): BNCoin[] => {

View File

@ -13,7 +13,7 @@ export function resolveMarketResponse(
): Market { ): Market {
return { return {
denom: marketResponse.denom, denom: marketResponse.denom,
borrowRate: Number(marketResponse.borrow_rate), borrowRate: Number(marketResponse.borrow_rate) * 100,
debtTotalScaled: marketResponse.debt_total_scaled, debtTotalScaled: marketResponse.debt_total_scaled,
collateralTotalScaled: marketResponse.collateral_total_scaled, collateralTotalScaled: marketResponse.collateral_total_scaled,
depositEnabled: assetParamsResponse.red_bank.deposit_enabled, depositEnabled: assetParamsResponse.red_bank.deposit_enabled,
@ -24,7 +24,7 @@ export function resolveMarketResponse(
max: BN(assetParamsResponse.deposit_cap), max: BN(assetParamsResponse.deposit_cap),
}, },
maxLtv: Number(assetParamsResponse.max_loan_to_value), maxLtv: Number(assetParamsResponse.max_loan_to_value),
liquidityRate: Number(marketResponse.liquidity_rate), liquidityRate: Number(marketResponse.liquidity_rate) * 100,
liquidationThreshold: Number(assetParamsResponse.liquidation_threshold), liquidationThreshold: Number(assetParamsResponse.liquidation_threshold),
} }
} }
@ -60,8 +60,8 @@ export function resolveHLSStrategies(
maxLeverage: getLeverageFromLTV(+asset.credit_manager.hls!.max_loan_to_value), maxLeverage: getLeverageFromLTV(+asset.credit_manager.hls!.max_loan_to_value),
maxLTV: +asset.credit_manager.hls!.max_loan_to_value, maxLTV: +asset.credit_manager.hls!.max_loan_to_value,
denoms: { denoms: {
deposit: correlatedDenom, deposit: asset.denom,
borrow: asset.denom, borrow: correlatedDenom,
}, },
}), }),
) )