Mp 3270 deposit cap messages (#416)

* Add Deposit cap for AssetSelector

* Add DepositCap component for Trade

* DepositCap to Vault and Fund

* DepositCap to Vault and Fund

* Small bugixes

* formatting fixes

* formatting fixes

* formatting fixes

* formatting fixes

* run format
This commit is contained in:
Bob van der Helm 2023-09-04 09:46:13 +02:00 committed by GitHub
parent 28ccf8ba84
commit d87fbd2a15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 357 additions and 143 deletions

View File

@ -1,12 +1,12 @@
import { render } from '@testing-library/react'
import { ASSETS } from 'constants/assets'
import { BN } from 'utils/helpers'
import useStore from 'store'
import DisplayCurrency from 'components/DisplayCurrency'
import VaultBorrowings, { VaultBorrowingsProps } from 'components/Modals/Vault/VaultBorrowings'
import { ASSETS } from 'constants/assets'
import { TESTNET_VAULTS_META_DATA } from 'constants/vaults'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { BN } from 'utils/helpers'
jest.mock('hooks/usePrices', () =>
jest.fn(() => ({
@ -58,6 +58,7 @@ describe('<VaultBorrowings />', () => {
deposits: [],
onChangeBorrowings: jest.fn(),
depositActions: [],
depositCapReachedCoins: [],
}
beforeAll(() => {

View File

@ -33,9 +33,7 @@ export default async function calculateAssetIncentivesApy(
const annualEmission = totalActiveEmissionValue.multipliedBy(SECONDS_IN_A_YEAR)
const totalAnnualReturnsValue = annualEmission.plus(marketReturns)
const incentivesApy = totalAnnualReturnsValue.dividedBy(marketLiquidityValue).multipliedBy(100)
return incentivesApy
return totalAnnualReturnsValue.dividedBy(marketLiquidityValue).multipliedBy(100)
} catch (ex) {
console.error(ex)
return null

View File

@ -3,15 +3,16 @@ import { getParamsQueryClient, getRedBankQueryClient } from 'api/cosmwasm-client
export default async function getMarket(denom: string): Promise<Market> {
try {
const redbankClient = await getRedBankQueryClient()
const redBankClient = await getRedBankQueryClient()
const paramsClient = await getParamsQueryClient()
const [market, assetParams] = await Promise.all([
redbankClient.market({ denom }),
const [market, assetParams, assetCap] = await Promise.all([
redBankClient.market({ denom }),
paramsClient.assetParams({ denom }),
paramsClient.totalDeposit({ denom }),
])
return resolveMarketResponse(market, assetParams)
return resolveMarketResponse(market, assetParams, assetCap)
} catch (ex) {
throw ex
}

View File

@ -3,12 +3,10 @@ import { getRedBankQueryClient } from 'api/cosmwasm-client'
export default async function getUnderlyingLiquidityAmount(market: Market): Promise<string> {
try {
const client = await getRedBankQueryClient()
const marketLiquidityAmount: string = await client.underlyingLiquidityAmount({
return await client.underlyingLiquidityAmount({
denom: market.denom,
amountScaled: market.collateralTotalScaled,
})
return marketLiquidityAmount
} catch (ex) {
throw ex
}

View File

@ -4,23 +4,31 @@ import iterateContractQuery from 'utils/iterateContractQuery'
import { byDenom } from 'utils/array'
import { resolveMarketResponse } from 'utils/resolvers'
import { Market as RedBankMarket } from 'types/generated/mars-red-bank/MarsRedBank.types'
import { AssetParamsBaseForAddr as AssetParams } from 'types/generated/mars-params/MarsParams.types'
import {
AssetParamsBaseForAddr as AssetParams,
TotalDepositResponse,
} from 'types/generated/mars-params/MarsParams.types'
export default async function getMarkets(): Promise<Market[]> {
try {
const redbankClient = await getRedBankQueryClient()
const redBankClient = await getRedBankQueryClient()
const paramsClient = await getParamsQueryClient()
const enabledAssets = getEnabledMarketAssets()
const [markets, assetParams] = await Promise.all([
iterateContractQuery(redbankClient.markets),
const capQueries = enabledAssets.map((asset) =>
paramsClient.totalDeposit({ denom: asset.denom }),
)
const [markets, assetParams, assetCaps] = await Promise.all([
iterateContractQuery(redBankClient.markets),
iterateContractQuery(paramsClient.allAssetParams),
Promise.all(capQueries),
])
return enabledAssets.map((asset) =>
resolveMarketResponse(
markets.find(byDenom(asset.denom)) as RedBankMarket,
assetParams.find(byDenom(asset.denom)) as AssetParams,
assetCaps.find(byDenom(asset.denom)) as TotalDepositResponse,
),
)
} catch (ex) {

View File

@ -1,8 +1,9 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useParams } from 'react-router-dom'
import Button from 'components/Button'
import Card from 'components/Card'
import DepositCapMessage from 'components/DepositCapMessage'
import FullOverlayContent from 'components/FullOverlayContent'
import { Plus } from 'components/Icons'
import SwitchAutoLend from 'components/Switch/SwitchAutoLend'
@ -12,6 +13,7 @@ import WalletBridges from 'components/Wallet/WalletBridges'
import { BN_ZERO } from 'constants/math'
import useAccounts from 'hooks/useAccounts'
import useCurrentAccount from 'hooks/useCurrentAccount'
import useMarketAssets from 'hooks/useMarketAssets'
import useToggle from 'hooks/useToggle'
import useWalletBalances from 'hooks/useWalletBalances'
import useStore from 'store'
@ -19,6 +21,7 @@ import { BNCoin } from 'types/classes/BNCoin'
import { byDenom } from 'utils/array'
import { getAssetByDenom, getBaseAsset } from 'utils/assets'
import { defaultFee } from 'utils/constants'
import { getCapLeftWithBuffer } from 'utils/generic'
import { BN } from 'utils/helpers'
export default function AccountFund() {
@ -36,6 +39,7 @@ export default function AccountFund() {
const hasAssetSelected = fundingAssets.length > 0
const hasFundingAssets =
fundingAssets.length > 0 && fundingAssets.every((a) => a.toCoin().amount !== '0')
const { data: marketAssets } = useMarketAssets()
const baseBalance = useMemo(
() => walletBalances.find(byDenom(baseAsset.denom))?.amount ?? '0',
@ -84,16 +88,15 @@ export default function AccountFund() {
setFundingAssets(newFundingAssets)
}, [selectedDenoms, fundingAssets])
const updateFundingAssets = useCallback(
(amount: BigNumber, denom: string) => {
const assetToUpdate = fundingAssets.find(byDenom(denom))
if (assetToUpdate) {
assetToUpdate.amount = amount
setFundingAssets([...fundingAssets.filter((a) => a.denom !== denom), assetToUpdate])
}
},
[fundingAssets],
)
const updateFundingAssets = useCallback((amount: BigNumber, denom: string) => {
setFundingAssets((fundingAssets) => {
const updateIdx = fundingAssets.findIndex(byDenom(denom))
if (updateIdx === -1) return fundingAssets
fundingAssets[updateIdx].amount = amount
return [...fundingAssets]
})
}, [])
useEffect(() => {
if (BN(baseBalance).isLessThan(defaultFee.amount[0].amount)) {
@ -106,6 +109,21 @@ export default function AccountFund() {
setSelectedAccountId(currentAccount?.id ?? accountId)
}, [accounts, selectedAccountId, accountId, currentAccount])
const depositCapReachedCoins = useMemo(() => {
const depositCapReachedCoins: BNCoin[] = []
fundingAssets.forEach((asset) => {
const marketAsset = marketAssets.find(byDenom(asset.denom))
if (!marketAsset) return
const capLeft = getCapLeftWithBuffer(marketAsset.cap)
if (asset.amount.isLessThanOrEqualTo(capLeft)) return
depositCapReachedCoins.push(BNCoin.fromDenomAndBigNumber(asset.denom, capLeft))
})
return depositCapReachedCoins
}, [fundingAssets, marketAssets])
if (!selectedAccountId) return null
return (
@ -147,6 +165,12 @@ export default function AccountFund() {
onClick={handleSelectAssetsClick}
disabled={isFunding}
/>
<DepositCapMessage
action='fund'
coins={depositCapReachedCoins}
className='pr-4 py-2 mt-4'
showIcon
/>
<SwitchAutoLend
className='pt-4 mt-4 border border-transparent border-t-white/10'
accountId={selectedAccountId}
@ -155,7 +179,7 @@ export default function AccountFund() {
className='w-full mt-4'
text='Fund account'
color='tertiary'
disabled={!hasFundingAssets}
disabled={!hasFundingAssets || depositCapReachedCoins.length > 0}
showProgressIndicator={isFunding}
onClick={handleClick}
size='lg'

View File

@ -0,0 +1,50 @@
import classNames from 'classnames'
import { HTMLAttributes } from 'react'
import { FormattedNumber } from 'components/FormattedNumber'
import { InfoCircle } from 'components/Icons'
import Text from 'components/Text'
import { BNCoin } from 'types/classes/BNCoin'
import { getAssetByDenom } from 'utils/assets'
interface Props extends HTMLAttributes<HTMLDivElement> {
action: 'buy' | 'deposit' | 'fund'
coins: BNCoin[]
showIcon?: boolean
}
export default function DepositCapMessage(props: Props) {
if (!props.coins.length) return null
return (
<div className={classNames('flex items-start', props.className)}>
{props.showIcon && <InfoCircle width={26} className='mr-5' />}
<div className='flex-col gap-2 flex'>
<Text size='sm'>Deposit Cap Reached</Text>
<Text size='xs' className='text-white/40'>{`Unfortunately you're not able to ${
props.action
} more than the following amount${props.coins.length > 1 ? 's' : ''}:`}</Text>
{props.coins.map((coin) => {
const asset = getAssetByDenom(coin.denom)
if (!asset) return null
return (
<div key={coin.denom} className='flex gap-1'>
<Text size='xs'>Cap Left:</Text>
<FormattedNumber
amount={coin.amount.toNumber()}
options={{
abbreviated: true,
decimals: asset.decimals,
suffix: ` ${asset.symbol}`,
}}
className='text-white/60 text-xs'
/>
</div>
)
})}
</div>
</div>
)
}

View File

@ -1,5 +1,5 @@
import { useMemo } from 'react'
import classNames from 'classnames'
import { useMemo } from 'react'
import { FormattedNumber } from 'components/FormattedNumber'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
@ -14,7 +14,7 @@ interface Props {
coin: BNCoin
className?: string
isApproximation?: boolean
parantheses?: boolean
parentheses?: boolean
}
export default function DisplayCurrency(props: Props) {
@ -43,7 +43,7 @@ export default function DisplayCurrency(props: Props) {
<FormattedNumber
className={classNames(
props.className,
props.parantheses && 'before:content-["("] after:content-[")"]',
props.parentheses && 'before:content-["("] after:content-[")"]',
)}
amount={convertToDisplayAmount(props.coin, displayCurrency, prices).toNumber()}
options={{

View File

@ -107,7 +107,7 @@ export default function VaultExpanded(props: Props) {
!isExpanded && props.row.toggleExpanded()
}}
>
<td colSpan={!status ? 5 : 6}>
<td colSpan={!status ? 7 : 8}>
<div className='align-center flex justify-end gap-3 p-4'>
{status && <DepositMoreButton />}
{status === VaultStatus.ACTIVE && <UnlockButton />}

View File

@ -182,6 +182,7 @@ export const VaultTable = (props: Props) => {
accessorKey: 'ltv.max',
header: 'Max LTV',
cell: ({ row }) => {
if (props.isLoading) return <Loading />
return <Text className='text-xs'>{formatPercent(row.original.ltv.max)}</Text>
},
},
@ -189,6 +190,7 @@ export const VaultTable = (props: Props) => {
accessorKey: 'ltv.liq',
header: 'Liq. LTV',
cell: ({ row }) => {
if (props.isLoading) return <Loading />
return <Text className='text-xs'>{formatPercent(row.original.ltv.liq)}</Text>
},
},

View File

@ -9,11 +9,11 @@ import Settings from 'components/Settings'
import Wallet from 'components/Wallet'
import useStore from 'store'
export const menuTree: { page: Page; label: string }[] = [
{ page: 'trade', label: 'Trade' },
{ page: 'lend', label: 'Earn' },
{ page: 'borrow', label: 'Borrow' },
{ page: 'portfolio', label: 'Portfolio' },
export const menuTree: { pages: Page[]; label: string }[] = [
{ pages: ['trade'], label: 'Trade' },
{ pages: ['lend', 'farm'], label: 'Earn' },
{ pages: ['borrow'], label: 'Borrow' },
{ pages: ['portfolio'], label: 'Portfolio' },
]
export default function DesktopHeader() {

View File

@ -207,7 +207,7 @@ function BorrowModal(props: Props) {
asset.denom,
modal.marketData?.liquidity?.amount ?? BN_ZERO,
)}
parantheses
parentheses
/>
</div>
<Text size='xs' className='text-white/50' tag='span'>

View File

@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import Button from 'components/Button'
import DepositCapMessage from 'components/DepositCapMessage'
import { ArrowRight, Plus } from 'components/Icons'
import SwitchAutoLend from 'components/Switch/SwitchAutoLend'
import Text from 'components/Text'
@ -8,6 +9,7 @@ import TokenInputWithSlider from 'components/TokenInput/TokenInputWithSlider'
import WalletBridges from 'components/Wallet/WalletBridges'
import { BN_ZERO } from 'constants/math'
import useAutoLendEnabledAccountIds from 'hooks/useAutoLendEnabledAccountIds'
import useMarketAssets from 'hooks/useMarketAssets'
import useToggle from 'hooks/useToggle'
import { useUpdatedAccount } from 'hooks/useUpdatedAccount'
import useWalletBalances from 'hooks/useWalletBalances'
@ -16,6 +18,7 @@ import { BNCoin } from 'types/classes/BNCoin'
import { byDenom } from 'utils/array'
import { getAssetByDenom, getBaseAsset } from 'utils/assets'
import { defaultFee } from 'utils/constants'
import { getCapLeftWithBuffer } from 'utils/generic'
import { BN } from 'utils/helpers'
interface Props {
@ -38,7 +41,7 @@ export default function FundAccount(props: Props) {
const { autoLendEnabledAccountIds } = useAutoLendEnabledAccountIds()
const isAutoLendEnabled = autoLendEnabledAccountIds.includes(accountId)
const { simulateDeposits } = useUpdatedAccount(account)
const { data: marketAssets } = useMarketAssets()
const baseBalance = useMemo(
() => walletBalances.find(byDenom(baseAsset.denom))?.amount ?? '0',
[walletBalances, baseAsset],
@ -87,16 +90,15 @@ export default function FundAccount(props: Props) {
setFundingAssets(newFundingAssets)
}, [selectedDenoms, fundingAssets])
const updateFundingAssets = useCallback(
(amount: BigNumber, denom: string) => {
const assetToUpdate = fundingAssets.find(byDenom(denom))
if (assetToUpdate) {
assetToUpdate.amount = amount
setFundingAssets([...fundingAssets.filter((a) => a.denom !== denom), assetToUpdate])
}
},
[fundingAssets],
)
const updateFundingAssets = useCallback((amount: BigNumber, denom: string) => {
setFundingAssets((fundingAssets) => {
const updateIdx = fundingAssets.findIndex(byDenom(denom))
if (updateIdx === -1) return fundingAssets
fundingAssets[updateIdx].amount = amount
return [...fundingAssets]
})
}, [])
useEffect(() => {
simulateDeposits(isAutoLendEnabled ? 'lend' : 'deposit', fundingAssets)
@ -108,6 +110,21 @@ export default function FundAccount(props: Props) {
}
}, [baseBalance])
const depositCapReachedCoins = useMemo(() => {
const depositCapReachedCoins: BNCoin[] = []
fundingAssets.forEach((asset) => {
const marketAsset = marketAssets.find(byDenom(asset.denom))
if (!marketAsset) return
const capLeft = getCapLeftWithBuffer(marketAsset.cap)
if (asset.amount.isLessThanOrEqualTo(capLeft)) return
depositCapReachedCoins.push(BNCoin.fromDenomAndBigNumber(asset.denom, capLeft))
})
return depositCapReachedCoins
}, [fundingAssets, marketAssets])
return (
<>
<div className='flex flex-wrap items-start'>
@ -139,6 +156,12 @@ export default function FundAccount(props: Props) {
onClick={handleSelectAssetsClick}
disabled={isFunding}
/>
<DepositCapMessage
action='fund'
coins={depositCapReachedCoins}
className='pr-4 py-2 mt-4'
showIcon
/>
<SwitchAutoLend
className='pt-4 mt-4 border border-transparent border-t-white/10'
accountId={accountId}
@ -148,7 +171,7 @@ export default function FundAccount(props: Props) {
className='w-full mt-4'
text='Fund account'
rightIcon={<ArrowRight />}
disabled={!hasFundingAssets}
disabled={!hasFundingAssets || depositCapReachedCoins.length > 0}
showProgressIndicator={isFunding}
onClick={handleClick}
/>

View File

@ -2,6 +2,7 @@ import BigNumber from 'bignumber.js'
import React, { useEffect, useMemo, useState } from 'react'
import Button from 'components/Button'
import DepositCapMessage from 'components/DepositCapMessage'
import DisplayCurrency from 'components/DisplayCurrency'
import Divider from 'components/Divider'
import { ArrowRight, ExclamationMarkCircled } from 'components/Icons'
@ -27,6 +28,7 @@ export interface VaultBorrowingsProps {
vault: Vault
depositActions: Action[]
onChangeBorrowings: (borrowings: BNCoin[]) => void
depositCapReachedCoins: BNCoin[]
}
export default function VaultBorrowings(props: VaultBorrowingsProps) {
@ -187,12 +189,21 @@ export default function VaultBorrowings(props: VaultBorrowingsProps) {
</Text>
</div>
)}
<Button
text='Select borrow assets +'
color='tertiary'
onClick={addAsset}
disabled={isConfirming}
/>
<DepositCapMessage
action='deposit'
coins={props.depositCapReachedCoins}
className='px-4 y-2'
showIcon
/>
<Divider />
<div className='flex flex-col gap-2'>
<div className='flex justify-between'>
@ -221,7 +232,7 @@ export default function VaultBorrowings(props: VaultBorrowingsProps) {
text='Deposit'
rightIcon={<ArrowRight />}
showProgressIndicator={isConfirming}
disabled={!props.depositActions.length}
disabled={!props.depositActions.length || props.depositCapReachedCoins.length > 0}
/>
</div>
)

View File

@ -1,7 +1,7 @@
import BigNumber from 'bignumber.js'
import { useMemo, useState } from 'react'
import Button from 'components/Button'
import DepositCapMessage from 'components/DepositCapMessage'
import DisplayCurrency from 'components/DisplayCurrency'
import Divider from 'components/Divider'
import { Gauge } from 'components/Gauge'
@ -12,6 +12,7 @@ import Text from 'components/Text'
import TokenInput from 'components/TokenInput'
import { BN_ZERO } from 'constants/math'
import usePrice from 'hooks/usePrice'
import { useMemo, useState } from 'react'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { accumulateAmounts } from 'utils/accounts'
@ -27,6 +28,7 @@ interface Props {
onChangeDeposits: (deposits: BNCoin[]) => void
onChangeIsCustomRatio: (isCustomRatio: boolean) => void
toggleOpen: (index: number) => void
depositCapReachedCoins: BNCoin[]
}
export default function VaultDeposit(props: Props) {
@ -158,7 +160,7 @@ export default function VaultDeposit(props: Props) {
return (
<div className='flex flex-col'>
<div className='flex gap-4 p-4'>
<div className='flex gap-4 pl-3 p-4'>
<div className='flex flex-col items-center justify-between gap-1 pb-[30px] pt-2'>
<Gauge
percentage={primaryValuePercentage}
@ -202,6 +204,14 @@ export default function VaultDeposit(props: Props) {
/>
</div>
</div>
<DepositCapMessage
action='deposit'
coins={props.depositCapReachedCoins}
className='px-4 "y-2'
showIcon
/>
<div className='flex flex-col gap-6 p-4 pt-2'>
{disableInput ? (
<div>
@ -240,4 +250,4 @@ export default function VaultDeposit(props: Props) {
</div>
</div>
)
}
}

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import Accordion from 'components/Accordion'
import AccountSummary from 'components/Account/AccountSummary'
@ -8,10 +8,14 @@ import VaultDeposit from 'components/Modals/Vault/VaultDeposits'
import VaultDepositSubTitle from 'components/Modals/Vault/VaultDepositsSubTitle'
import { BN_ZERO } from 'constants/math'
import useDepositVault from 'hooks/broadcast/useDepositVault'
import useDisplayAsset from 'hooks/useDisplayAsset'
import useIsOpenArray from 'hooks/useIsOpenArray'
import usePrices from 'hooks/usePrices'
import { useUpdatedAccount } from 'hooks/useUpdatedAccount'
import { BNCoin } from 'types/classes/BNCoin'
import { byDenom } from 'utils/array'
import { convertToDisplayAmount, magnify } from 'utils/formatters'
import { getCapLeftWithBuffer } from 'utils/generic'
interface Props {
vault: Vault | DepositedVault
@ -29,14 +33,14 @@ export default function VaultModalContent(props: Props) {
removedDeposits,
removedLends,
removeLends,
updatedAccount,
addVaultValues,
} = useUpdatedAccount(props.account)
const { data: prices } = usePrices()
const [isOpen, toggleOpen] = useIsOpenArray(2, false)
const [isCustomRatio, setIsCustomRatio] = useState(false)
const [selectedCoins, setSelectedCoins] = useState<BNCoin[]>([])
const displayAsset = useDisplayAsset()
const { actions: depositActions, totalValue } = useDepositVault({
vault: props.vault,
reclaims: removedLends,
@ -44,6 +48,24 @@ export default function VaultModalContent(props: Props) {
borrowings: addedDebts,
})
const depositCapReachedCoins = useMemo(() => {
const capLeft = getCapLeftWithBuffer(props.vault.cap)
if (totalValue.isGreaterThan(capLeft)) {
const amount = magnify(
convertToDisplayAmount(
BNCoin.fromDenomAndBigNumber(props.vault.cap.denom, capLeft),
displayAsset.denom,
prices,
).toString(),
displayAsset,
)
return [BNCoin.fromDenomAndBigNumber(displayAsset.denom, amount)]
}
return []
}, [displayAsset, prices, props.vault.cap, totalValue])
const handleDepositSelect = useCallback(
(selectedCoins: BNCoin[]) => {
const reclaims: BNCoin[] = []
@ -134,6 +156,7 @@ export default function VaultModalContent(props: Props) {
toggleOpen={toggleOpen}
isCustomRatio={isCustomRatio}
onChangeIsCustomRatio={onChangeIsCustomRatio}
depositCapReachedCoins={depositCapReachedCoins}
/>
),
title: 'Deposit',
@ -151,6 +174,7 @@ export default function VaultModalContent(props: Props) {
onChangeBorrowings={addDebts}
vault={props.vault}
depositActions={depositActions}
depositCapReachedCoins={depositCapReachedCoins}
/>
),
title: 'Borrow',

View File

@ -1,5 +1,5 @@
import { useParams } from 'react-router-dom'
import classNames from 'classnames'
import { useParams } from 'react-router-dom'
import { menuTree } from 'components/Header/DesktopHeader'
import { Logo } from 'components/Icons'
@ -11,8 +11,8 @@ export default function DesktopNavigation() {
const { address, accountId } = useParams()
const focusComponent = useStore((s) => s.focusComponent)
function getIsActive(href: string) {
return location.pathname.includes(href)
function getIsActive(pages: string[]) {
return pages.some((page) => location.pathname.includes(page))
}
return (
@ -31,8 +31,8 @@ export default function DesktopNavigation() {
{menuTree.map((item, index) => (
<NavLink
key={index}
href={getRoute(item.page, address, accountId)}
isActive={getIsActive(item.page)}
href={getRoute(item.pages[0], address, accountId)}
isActive={getIsActive(item.pages)}
>
{item.label}
</NavLink>

View File

@ -12,12 +12,10 @@ export const NavLink = (props: Props) => {
return (
<Link
to={props.href}
className={({ isActive }) =>
classNames(
'text-sm font-semibold hover:text-white active:text-white',
isActive ? 'pointer-events-none text-white' : 'text-white/60',
)
}
className={classNames(
'text-sm font-semibold hover:text-white active:text-white',
props.isActive ? 'pointer-events-none text-white' : 'text-white/60',
)}
>
{props.children}
</Link>

View File

@ -10,6 +10,7 @@ import { BNCoin } from 'types/classes/BNCoin'
interface Props {
asset: Asset
onSelectAsset: (asset: Asset) => void
depositCap?: DepositCap
}
export default function AssetItem(props: Props) {
@ -40,11 +41,24 @@ export default function AssetItem(props: Props) {
{asset.isFavorite ? <StarFilled width={16} /> : <StarOutlined width={16} />}
</div>
<AssetImage asset={asset} size={24} />
<Text size='sm' className='text-left'>
{asset.name}
</Text>
<div className='rounded-sm bg-white/20 px-[6px] py-[2px]'>
<Text size='xs'>{asset.symbol}</Text>
<div className='flex-col'>
<div className='flex gap-1'>
<Text size='sm' className='text-left'>
{asset.name}
</Text>
<div className='rounded-sm bg-white/20 px-[6px] py-[2px]'>
<Text size='xs'>{asset.symbol}</Text>
</div>
</div>
{props.depositCap && (
<div className='flex gap-1'>
<span className='text-left text-white/60 text-xs'>Cap Left: </span>
<DisplayCurrency
className='text-left text-white/60 text-xs'
coin={BNCoin.fromDenomAndBigNumber(props.depositCap.denom, props.depositCap.max)}
/>
</div>
)}
</div>
</div>
<DisplayCurrency

View File

@ -3,6 +3,8 @@ import classNames from 'classnames'
import { ChevronDown } from 'components/Icons'
import Text from 'components/Text'
import AssetItem from 'components/Trade/TradeModule/AssetSelector/AssetItem'
import useMarketAssets from 'hooks/useMarketAssets'
import { byDenom } from 'utils/array'
interface Props {
type: 'buy' | 'sell'
@ -13,6 +15,8 @@ interface Props {
}
export default function AssetList(props: Props) {
const { data: marketAssets } = useMarketAssets()
return (
<section>
<button
@ -34,6 +38,9 @@ export default function AssetList(props: Props) {
key={`${props.type}-${asset.symbol}`}
asset={asset}
onSelectAsset={props.onChangeAsset}
depositCap={
props.type === 'buy' ? marketAssets?.find(byDenom(asset.denom))?.cap : undefined
}
/>
))}
</ul>

View File

@ -4,8 +4,8 @@ import { InfoCircle } from 'components/Icons'
import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip'
import { ORDER_TYPE_TABS } from 'components/Trade/TradeModule/SwapForm/OrderTypeSelector/constants'
import ConditionalWrapper from 'hocs/ConditionalWrapper'
import { AvailableOrderType } from 'components/Trade/TradeModule/SwapForm/OrderTypeSelector/types'
import ConditionalWrapper from 'hocs/ConditionalWrapper'
interface Props {
selected: AvailableOrderType
@ -46,9 +46,9 @@ export default function OrderTypeSelector(props: Props) {
}
const className = {
wrapper: 'flex flex-1 flex-row px-3 pt-3',
wrapper: 'flex flex-1 flex-row px-3 pt-4',
tab: 'mr-4 pb-2 cursor-pointer select-none flex flex-row',
selectedTab: 'border-b-2 border-pink border-solid',
disabledTab: 'opacity-50 pointer-events-none',
disabledTab: 'opacity-20 pointer-events-none',
infoCircle: 'w-4 h-4 ml-2 mt-1',
}

View File

@ -2,6 +2,7 @@ import debounce from 'lodash.debounce'
import { useCallback, useEffect, useMemo, useState } from 'react'
import estimateExactIn from 'api/swap/estimateExactIn'
import DepositCapMessage from 'components/DepositCapMessage'
import Divider from 'components/Divider'
import RangeInput from 'components/RangeInput'
import AssetAmountInput from 'components/Trade/TradeModule/SwapForm/AssetAmountInput'
@ -16,6 +17,7 @@ import useAutoLendEnabledAccountIds from 'hooks/useAutoLendEnabledAccountIds'
import useCurrentAccount from 'hooks/useCurrentAccount'
import useHealthComputer from 'hooks/useHealthComputer'
import useLocalStorage from 'hooks/useLocalStorage'
import useMarketAssets from 'hooks/useMarketAssets'
import useMarketBorrowings from 'hooks/useMarketBorrowings'
import usePrices from 'hooks/usePrices'
import useToggle from 'hooks/useToggle'
@ -24,6 +26,7 @@ import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { byDenom } from 'utils/array'
import { defaultFee } from 'utils/constants'
import { getCapLeftWithBuffer } from 'utils/generic'
import { asyncThrottle, BN } from 'utils/helpers'
interface Props {
@ -39,6 +42,7 @@ export default function SwapForm(props: Props) {
const [slippage] = useLocalStorage(SLIPPAGE_KEY, DEFAULT_SETTINGS.slippage)
const { computeMaxSwapAmount } = useHealthComputer(account)
const { data: borrowAssets } = useMarketBorrowings()
const { data: marketAssets } = useMarketAssets()
const [isMarginChecked, setMarginChecked] = useToggle()
const [buyAssetAmount, setBuyAssetAmount] = useState(BN_ZERO)
@ -58,6 +62,19 @@ export default function SwapForm(props: Props) {
[borrowAssets, sellAsset.denom],
)
const depositCapReachedCoins: BNCoin[] = useMemo(() => {
const buyMarketAsset = marketAssets.find(byDenom(buyAsset.denom))
if (!buyMarketAsset) return []
const depositCapLeft = getCapLeftWithBuffer(buyMarketAsset.cap)
if (buyAssetAmount.isGreaterThan(depositCapLeft)) {
return [BNCoin.fromDenomAndBigNumber(buyAsset.denom, depositCapLeft)]
}
return []
}, [marketAssets, buyAsset.denom, buyAssetAmount])
const onChangeSellAmount = useCallback(
(amount: BigNumber) => {
setSellAssetAmount(amount)
@ -233,59 +250,59 @@ export default function SwapForm(props: Props) {
/>
<Divider />
<OrderTypeSelector selected={selectedOrderType} onChange={setSelectedOrderType} />
<AssetAmountInput
label='Buy'
max={maxBuyableAmountEstimation}
amount={buyAssetAmount}
setAmount={onChangeBuyAmount}
asset={buyAsset}
assetUSDValue={buyAssetValue}
maxButtonLabel='Max Amount:'
containerClassName='mx-3 my-6'
disabled={isConfirming}
/>
<div className='flex flex-col gap-6 px-3 mt-6'>
<AssetAmountInput
label='Buy'
max={maxBuyableAmountEstimation}
amount={buyAssetAmount}
setAmount={onChangeBuyAmount}
asset={buyAsset}
assetUSDValue={buyAssetValue}
maxButtonLabel='Max Amount:'
disabled={isConfirming}
/>
<RangeInput
wrapperClassName='p-4'
disabled={isConfirming || maxBuyableAmountEstimation.isZero()}
onChange={handleRangeInputChange}
value={buyAssetAmount.shiftedBy(-buyAsset.decimals).toNumber()}
max={maxBuyableAmountEstimation.shiftedBy(-buyAsset.decimals).toNumber()}
marginThreshold={
isMarginChecked
? buySideMarginThreshold.shiftedBy(-buyAsset.decimals).toNumber()
: undefined
}
/>
<RangeInput
disabled={isConfirming || maxBuyableAmountEstimation.isZero()}
onChange={handleRangeInputChange}
value={buyAssetAmount.shiftedBy(-buyAsset.decimals).toNumber()}
max={maxBuyableAmountEstimation.shiftedBy(-buyAsset.decimals).toNumber()}
marginThreshold={
isMarginChecked
? buySideMarginThreshold.shiftedBy(-buyAsset.decimals).toNumber()
: undefined
}
/>
<AssetAmountInput
label='Sell'
max={maxSellAmount}
amount={sellAssetAmount}
setAmount={onChangeSellAmount}
assetUSDValue={sellAssetValue}
asset={sellAsset}
maxButtonLabel='Balance:'
containerClassName='mx-3'
disabled={isConfirming}
/>
<DepositCapMessage action='buy' coins={depositCapReachedCoins} className='p-4 bg-white/5' />
<TradeSummary
containerClassName='m-3 mt-10'
buyAsset={buyAsset}
sellAsset={sellAsset}
borrowRate={borrowAsset?.borrowRate}
buyAction={handleBuyClick}
buyButtonDisabled={sellAssetAmount.isZero()}
showProgressIndicator={isConfirming}
isMargin={isMarginChecked}
borrowAmount={
sellAssetAmount.isGreaterThan(sellSideMarginThreshold)
? sellAssetAmount.minus(sellSideMarginThreshold)
: BN_ZERO
}
estimatedFee={estimatedFee}
/>
<AssetAmountInput
label='Sell'
max={maxSellAmount}
amount={sellAssetAmount}
setAmount={onChangeSellAmount}
assetUSDValue={sellAssetValue}
asset={sellAsset}
maxButtonLabel='Balance:'
disabled={isConfirming}
/>
<TradeSummary
buyAsset={buyAsset}
sellAsset={sellAsset}
borrowRate={borrowAsset?.borrowRate}
buyAction={handleBuyClick}
buyButtonDisabled={sellAssetAmount.isZero() || depositCapReachedCoins.length > 0}
showProgressIndicator={isConfirming}
isMargin={isMarginChecked}
borrowAmount={
sellAssetAmount.isGreaterThan(sellSideMarginThreshold)
? sellAssetAmount.minus(sellSideMarginThreshold)
: BN_ZERO
}
estimatedFee={estimatedFee}
/>
</div>
</>
)
}

View File

@ -0,0 +1,14 @@
import { ASSETS } from 'constants/assets'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { DISPLAY_CURRENCY_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
import { byDenom } from 'utils/array'
export default function useDisplayAsset() {
const [displayCurrency] = useLocalStorage<string>(
DISPLAY_CURRENCY_KEY,
DEFAULT_SETTINGS.displayCurrency,
)
return ASSETS.find(byDenom(displayCurrency)) ?? ASSETS[0]
}

View File

@ -15,6 +15,7 @@ import {
HealthComputer,
} from 'types/generated/mars-rover-health-computer/MarsRoverHealthComputer.types'
import { convertAccountToPositions } from 'utils/accounts'
import { LTV_BUFFER } from 'utils/constants'
import {
BorrowTarget,
compute_health_js,
@ -24,7 +25,6 @@ import {
SwapKind,
} from 'utils/health_computer'
import { BN } from 'utils/helpers'
import { LTV_BUFFER } from 'utils/constants'
export default function useHealthComputer(account?: Account) {
const { data: prices } = usePrices()

View File

@ -23,7 +23,7 @@ function useLendingMarketAssetsTableData(): {
const accountLentAssets: LendingMarketTableData[] = [],
availableAssets: LendingMarketTableData[] = []
markets.forEach(({ denom, depositCap, liquidityRate, liquidationThreshold, maxLtv }) => {
markets.forEach(({ denom, cap, liquidityRate, liquidationThreshold, maxLtv }) => {
const asset = getAssetByDenom(denom) as Asset
const marketDepositAmount = BN(marketDeposits.find(byDenom(denom))?.amount ?? 0)
const marketLiquidityAmount = BN(marketLiquidities.find(byDenom(denom))?.amount ?? 0)
@ -38,7 +38,7 @@ function useLendingMarketAssetsTableData(): {
accountLentValue,
accountLentAmount,
marketLiquidityAmount,
marketDepositCap: BN(depositCap),
marketDepositCap: cap.max,
marketLiquidityRate: liquidityRate,
marketLiquidationThreshold: liquidationThreshold,
marketMaxLtv: maxLtv,

View File

@ -49,10 +49,6 @@ interface PseudoAsset {
symbol: string
}
interface OtherAsset extends Omit<Asset, 'symbol'> {
symbol: 'MARS'
}
interface BorrowAsset extends Asset {
borrowRate: number | null
liquidity: {

View File

@ -5,7 +5,7 @@ interface Market {
collateralTotalScaled: string
depositEnabled: boolean
borrowEnabled: boolean
depositCap: string
cap: DepositCap
maxLtv: number
liquidityRate: number
liquidationThreshold: number

View File

@ -26,11 +26,7 @@ interface VaultInfo {
max: number
liq: number
}
cap: {
denom: string
used: BigNumber
max: BigNumber
}
cap: DepositCap
}
interface VaultConfig extends VaultMetaData, VaultInfo {}
@ -74,3 +70,9 @@ interface VaultPositionFlatAmounts {
unlocking: BigNumber
unlocked: BigNumber
}
interface DepositCap {
denom: string
used: BigNumber
max: BigNumber
}

View File

@ -11,3 +11,5 @@ export const defaultFee: StdFee = {
export const SECONDS_IN_A_YEAR = 31540000
export const LTV_BUFFER = 0.01
export const DEPOSIT_CAP_BUFFER = 0.999

5
src/utils/generic.ts Normal file
View File

@ -0,0 +1,5 @@
import { DEPOSIT_CAP_BUFFER } from 'utils/constants'
export function getCapLeftWithBuffer(cap: DepositCap) {
return cap.max.minus(cap.used).times(DEPOSIT_CAP_BUFFER).integerValue()
}

View File

@ -1,9 +1,14 @@
import {
AssetParamsBaseForAddr as AssetParams,
TotalDepositResponse,
} from 'types/generated/mars-params/MarsParams.types'
import { Market as RedBankMarket } from 'types/generated/mars-red-bank/MarsRedBank.types'
import { AssetParamsBaseForAddr as AssetParams } from 'types/generated/mars-params/MarsParams.types'
import { BN } from 'utils/helpers'
export function resolveMarketResponse(
marketResponse: RedBankMarket,
assetParamsResponse: AssetParams,
assetCapResponse: TotalDepositResponse,
): Market {
return {
denom: marketResponse.denom,
@ -12,7 +17,11 @@ export function resolveMarketResponse(
collateralTotalScaled: marketResponse.collateral_total_scaled,
depositEnabled: assetParamsResponse.red_bank.deposit_enabled,
borrowEnabled: assetParamsResponse.red_bank.borrow_enabled,
depositCap: assetParamsResponse.deposit_cap,
cap: {
denom: assetCapResponse.denom,
used: BN(assetCapResponse.amount),
max: BN(assetParamsResponse.deposit_cap),
},
maxLtv: Number(assetParamsResponse.max_loan_to_value),
liquidityRate: Number(marketResponse.liquidity_rate),
liquidationThreshold: Number(assetParamsResponse.liquidation_threshold),