finish adding liquidation price (#649)

* finish adding liquidation price

* fix minor issues

* v2.0.6 and use feature flag for auto repay (#650)

* 🐛incorrect deposit cap utilization (#647)

* Initialize perps (#648)

* setup routing

* add basic perps interface

* small fix

* feat: listed dydx and AKT (#652)

* Deposit Cap and Utilization Fix (#654)

* fix: fixed the deposit cap and total supplied / utilization rate

* fix: fixed build

* fix: fixed build

* fix: avoid deposit cap usage over 100%

* refactor market data apy/ltv

* fix: fixed the withdraw from vaults modal

* tidy: refactor

---------

Co-authored-by: Bob van der Helm <34470358+bobthebuidlr@users.noreply.github.com>

* stATOM and stOSMO TradingView support (#653)

* feat: listed dydx and AKT

* fix: removed theGraph support for now

* adjust liq price loading and null/0 values

---------

Co-authored-by: Linkie Link <linkielink.dev@gmail.com>
This commit is contained in:
Bob van der Helm 2023-11-27 12:07:00 +01:00 committed by GitHub
parent 1abaddd4e5
commit 88a7c27a28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 229 additions and 37 deletions

42
src/api/swap/getPools.ts Normal file
View File

@ -0,0 +1,42 @@
import { ENV } from 'constants/env'
const url = `${ENV.URL_REST}osmosis/gamm/v1beta1/pools/`
export default async function getPools(poolIds: string[]): Promise<Pool[]> {
const promises = poolIds.map((poolId) => fetch(url + poolId))
const responses = await Promise.all(promises)
return await Promise.all(responses.map(async (pool) => (await pool.json()).pool as Pool))
}
interface Pool {
'@type': string
address: string
future_pool_governor: string
id: string
pool_assets?: PoolAsset[]
pool_liquidity?: PoolLiquidity[]
pool_params: PoolParams
total_shares: TotalShares
total_weight: string
}
interface PoolAsset {
token: TotalShares
weight: string
}
interface PoolLiquidity {
amount: string
denom: string
}
interface TotalShares {
amount: string
denom: string
}
interface PoolParams {
exit_fee: string
smooth_weight_change_params: null
swap_fee: string
}

View File

@ -1,24 +1,38 @@
import classNames from 'classnames'
import { useMemo } from 'react'
import debounce from 'lodash.debounce'
import React, { useEffect, useMemo, useState } from 'react'
import ActionButton from 'components/Button/ActionButton'
import { CircularProgress } from 'components/CircularProgress'
import DisplayCurrency from 'components/DisplayCurrency'
import Divider from 'components/Divider'
import { FormattedNumber } from 'components/FormattedNumber'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import useLocalStorage from 'hooks/useLocalStorage'
import usePrice from 'hooks/usePrice'
import useSwapFee from 'hooks/useSwapFee'
import { BNCoin } from 'types/classes/BNCoin'
import { getAssetByDenom } from 'utils/assets'
import { formatAmountWithSymbol, formatPercent } from 'utils/formatters'
interface Props {
buyAsset: Asset
sellAsset: Asset
borrowAmount: BigNumber
borrowRate?: number | null
buyAction: () => void
buyAmount: BigNumber
buyAsset: Asset
buyButtonDisabled: boolean
containerClassName?: string
showProgressIndicator: boolean
isMargin?: boolean
borrowAmount: BigNumber
estimatedFee: StdFee
buyAction: () => void
isMargin?: boolean
liquidationPrice: number | null
route: Route[]
sellAmount: BigNumber
sellAsset: Asset
showProgressIndicator: boolean
}
const infoLineClasses = 'flex flex-row justify-between flex-1 mb-1 text-xs text-white'
export default function TradeSummary(props: Props) {
@ -34,7 +48,34 @@ export default function TradeSummary(props: Props) {
estimatedFee,
showProgressIndicator,
route,
sellAmount,
buyAmount,
} = props
const [slippage] = useLocalStorage<number>(LocalStorageKeys.SLIPPAGE, DEFAULT_SETTINGS.slippage)
const sellAssetPrice = usePrice(sellAsset.denom)
const swapFee = useSwapFee(route.map((r) => r.pool_id))
const [liquidationPrice, setLiquidationPrice] = useState<number | null>(null)
const [isUpdatingLiquidationPrice, setIsUpdatingLiquidationPrice] = useState(false)
const debouncedSetLiqPrice = useMemo(
() => debounce(setLiquidationPrice, 1000, { leading: false }),
[],
)
const minReceive = useMemo(() => {
return buyAmount.times(1 - swapFee).times(1 - slippage)
}, [buyAmount, slippage, swapFee])
useEffect(() => {
setIsUpdatingLiquidationPrice(true)
debouncedSetLiqPrice(props.liquidationPrice)
}, [debouncedSetLiqPrice, props.liquidationPrice])
useEffect(() => setIsUpdatingLiquidationPrice(false), [liquidationPrice])
const swapFeeValue = useMemo(() => {
return sellAssetPrice.times(swapFee).times(sellAmount)
}, [sellAmount, sellAssetPrice, swapFee])
const parsedRoutes = useMemo(() => {
if (!route.length) return '-'
@ -59,14 +100,9 @@ export default function TradeSummary(props: Props) {
>
<div className='flex flex-col flex-1 m-3'>
<span className='mb-2 text-xs font-bold'>Summary</span>
<div className={infoLineClasses}>
<span className='opacity-40'>Fees</span>
<span>{formatAmountWithSymbol(estimatedFee.amount[0])}</span>
</div>
{isMargin && (
<>
<div className={infoLineClasses}>
<span className='opacity-40'>Borrowing</span>
<SummaryLine label='Borrowing'>
<FormattedNumber
amount={borrowAmount.toNumber()}
options={{
@ -79,18 +115,45 @@ export default function TradeSummary(props: Props) {
}}
animate
/>
</div>
<div className={infoLineClasses}>
<span className='opacity-40'>Borrow Rate APY</span>
</SummaryLine>
<SummaryLine label='Borrow Rate APY'>
<span>{formatPercent(borrowRate || 0)}</span>
</div>
</SummaryLine>
<Divider className='my-2' />
</>
)}
<div className={infoLineClasses}>
<span className='opacity-40'>Route</span>
<span>{parsedRoutes}</span>
<>
<SummaryLine label='Liquidation Price'>
<div className='flex h-2'>
{isUpdatingLiquidationPrice ? (
<CircularProgress className='opacity-50' />
) : liquidationPrice === null || liquidationPrice === 0 ? (
'-'
) : (
<FormattedNumber
className='inline'
amount={liquidationPrice}
options={{ abbreviated: true, prefix: `${props.buyAsset.symbol} = $ ` }}
/>
)}
</div>
</SummaryLine>
<Divider className='my-2' />
</>
<SummaryLine label={`Swap fees (${(swapFee || 0.002) * 100}%)`}>
<DisplayCurrency coin={BNCoin.fromDenomAndBigNumber(sellAsset.denom, swapFeeValue)} />
</SummaryLine>
<SummaryLine label='Transaction fees'>
<span>{formatAmountWithSymbol(estimatedFee.amount[0])}</span>
</SummaryLine>
<SummaryLine label={`Min receive (${slippage * 100}% slippage)`}>
<FormattedNumber
amount={minReceive.toNumber()}
options={{ decimals: buyAsset.decimals, suffix: ` ${buyAsset.symbol}`, maxDecimals: 6 }}
/>
</SummaryLine>
<Divider className='my-2' />
<SummaryLine label='Route'>{parsedRoutes}</SummaryLine>
</div>
<ActionButton
disabled={buyButtonDisabled}
@ -104,3 +167,16 @@ export default function TradeSummary(props: Props) {
</div>
)
}
interface SummaryLineProps {
children: React.ReactNode
label: string
}
function SummaryLine(props: SummaryLineProps) {
return (
<div className={infoLineClasses}>
<span className='opacity-40'>{props.label}</span>
<span>{props.children}</span>
</div>
)
}

View File

@ -62,9 +62,9 @@ export default function SwapForm(props: Props) {
const { autoLendEnabledAccountIds } = useAutoLend()
const isAutoLendEnabled = account ? autoLendEnabledAccountIds.includes(account.id) : false
const modal = useStore<string | null>((s) => s.fundAndWithdrawModal)
const { simulateTrade, removedLends, updatedAccount } = useUpdatedAccount(account)
const throttledEstimateExactIn = useMemo(() => asyncThrottle(estimateExactIn, 250), [])
const { simulateTrade, removedLends } = useUpdatedAccount(account)
const { computeLiquidationPrice } = useHealthComputer(updatedAccount)
const borrowAsset = useMemo(
() => borrowAssets.find(byDenom(sellAsset.denom)),
@ -209,6 +209,11 @@ export default function SwapForm(props: Props) {
[isRepayable, setAutoRepayChecked],
)
const liquidationPrice = useMemo(
() => computeLiquidationPrice(props.buyAsset.denom),
[computeLiquidationPrice, props.buyAsset.denom],
)
useEffect(() => {
setBuyAssetAmount(BN_ZERO)
setSellAssetAmount(BN_ZERO)
@ -379,6 +384,9 @@ export default function SwapForm(props: Props) {
borrowAmount={borrowAmount}
estimatedFee={estimatedFee}
route={route}
liquidationPrice={liquidationPrice}
sellAmount={sellAssetAmount}
buyAmount={buyAssetAmount}
/>
</div>
</>

View File

@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { LocalStorageKeys } from 'constants/localStorageKeys'
import { BN_ZERO } from 'constants/math'
import { PRICE_ORACLE_DECIMALS } from 'constants/query'
import useAssetParams from 'hooks/useAssetParams'
import useLocalStorage from 'hooks/useLocalStorage'
import usePrices from 'hooks/usePrices'
@ -22,6 +23,7 @@ import { SWAP_FEE_BUFFER } from 'utils/constants'
import {
BorrowTarget,
compute_health_js,
liquidation_price_js,
max_borrow_estimate_js,
max_swap_estimate_js,
max_withdraw_estimate_js,
@ -192,6 +194,25 @@ export default function useHealthComputer(account?: Account) {
},
[healthComputer, slippage],
)
const computeLiquidationPrice = useCallback(
(denom: string) => {
if (!healthComputer) return null
try {
const asset = getAssetByDenom(denom)
if (!asset) return null
const decimalDiff = asset.decimals - PRICE_ORACLE_DECIMALS
return BN(liquidation_price_js(healthComputer, denom))
.shiftedBy(-VALUE_SCALE_FACTOR)
.shiftedBy(decimalDiff)
.toNumber()
} catch (e) {
return null
}
},
[healthComputer],
)
const health = useMemo(() => {
const convertedHealth = BN(Math.log(healthFactor))
.dividedBy(Math.log(3.5))
@ -211,5 +232,6 @@ export default function useHealthComputer(account?: Account) {
computeMaxBorrowAmount,
computeMaxWithdrawAmount,
computeMaxSwapAmount,
computeLiquidationPrice,
}
}

16
src/hooks/useSwapFee.ts Normal file
View File

@ -0,0 +1,16 @@
import useSWR from 'swr'
import getSwapFees from 'api/swap/getPools'
import { BN_ZERO } from 'constants/math'
const STANDARD_SWAP_FEE = 0.002
export default function useSwapFee(poolIds: string[]) {
const { data: pools } = useSWR(`swapFees/${poolIds.join(',')}`, () => getSwapFees(poolIds))
if (!pools?.length) return STANDARD_SWAP_FEE
return pools
.reduce((acc, pool) => acc.plus(pool?.pool_params?.swap_fee || STANDARD_SWAP_FEE), BN_ZERO)
.toNumber()
}

View File

@ -1,4 +1,3 @@
import BigNumber from 'bignumber.js'
import useSWR from 'swr'
import estimateExactIn from 'api/swap/estimateExactIn'
@ -7,26 +6,25 @@ import { BNCoin } from 'types/classes/BNCoin'
import { SWAP_FEE_BUFFER } from 'utils/constants'
import { BN } from 'utils/helpers'
const amountIn = 1000_000_000
export default function useSwapValueLoss(denomIn: string, denomOut: string) {
export default function useSwapValueLoss(
denomIn: string,
denomOut: string,
amount: number = 1_000_000,
) {
const denomInPrice = usePrice(denomIn)
const denomOutPrice = usePrice(denomOut)
return useSWR(
`swapValueLoss/${denomIn}/${denomOut}`,
`swapValueLoss/${denomIn}/${denomOut}/${amount}`,
async () => {
const valueIn = denomInPrice.times(amountIn)
const valueIn = denomInPrice.times(amount)
const amountOut = await estimateExactIn(
BNCoin.fromDenomAndBigNumber(denomIn, BN(amountIn)).toCoin(),
BNCoin.fromDenomAndBigNumber(denomIn, BN(amount)).toCoin(),
denomOut,
)
const valueOut = denomOutPrice.times(amountOut)
return BigNumber.max(
valueIn.minus(valueOut).dividedBy(valueIn).decimalPlaces(6),
0,
).toNumber()
return valueIn.minus(valueOut).dividedBy(valueIn).decimalPlaces(6).toNumber()
},
{
fallbackData: SWAP_FEE_BUFFER,

View File

@ -37,6 +37,12 @@ export function max_swap_estimate_js(
kind: SwapKind,
slippage: Slippage,
): string
/**
* @param {HealthComputer} c
* @param {string} denom
* @returns {string}
*/
export function liquidation_price_js(c: HealthComputer, denom: string): string
export interface HealthComputer {
kind: AccountKind
positions: Positions
@ -82,9 +88,9 @@ export interface InitOutput {
g: number,
h: number,
) => void
readonly liquidation_price_js: (a: number, b: number, c: number, d: number) => void
readonly allocate: (a: number) => number
readonly deallocate: (a: number) => void
readonly requires_stargate: () => void
readonly requires_iterator: () => void
readonly interface_version_8: () => void
readonly __wbindgen_malloc: (a: number, b: number) => number

View File

@ -230,6 +230,30 @@ export function max_swap_estimate_js(c, from_denom, to_denom, kind, slippage) {
}
}
/**
* @param {HealthComputer} c
* @param {string} denom
* @returns {string}
*/
export function liquidation_price_js(c, denom) {
let deferred2_0
let deferred2_1
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16)
const ptr0 = passStringToWasm0(denom, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc)
const len0 = WASM_VECTOR_LEN
wasm.liquidation_price_js(retptr, addHeapObject(c), ptr0, len0)
var r0 = getInt32Memory0()[retptr / 4 + 0]
var r1 = getInt32Memory0()[retptr / 4 + 1]
deferred2_0 = r0
deferred2_1 = r1
return getStringFromWasm0(r0, r1)
} finally {
wasm.__wbindgen_add_to_stack_pointer(16)
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1)
}
}
function handleError(f, args) {
try {
return f.apply(this, args)

View File

@ -14,9 +14,9 @@ export function max_swap_estimate_js(
g: number,
h: number,
): void
export function liquidation_price_js(a: number, b: number, c: number, d: number): void
export function allocate(a: number): number
export function deallocate(a: number): void
export function requires_stargate(): void
export function requires_iterator(): void
export function interface_version_8(): void
export function __wbindgen_malloc(a: number, b: number): number