diff --git a/src/api/swap/getPools.ts b/src/api/swap/getPools.ts new file mode 100644 index 00000000..27d49d4c --- /dev/null +++ b/src/api/swap/getPools.ts @@ -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 { + 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 +} diff --git a/src/components/Trade/TradeModule/SwapForm/TradeSummary.tsx b/src/components/Trade/TradeModule/SwapForm/TradeSummary.tsx index 8be477f1..188d3b03 100644 --- a/src/components/Trade/TradeModule/SwapForm/TradeSummary.tsx +++ b/src/components/Trade/TradeModule/SwapForm/TradeSummary.tsx @@ -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(LocalStorageKeys.SLIPPAGE, DEFAULT_SETTINGS.slippage) + + const sellAssetPrice = usePrice(sellAsset.denom) + const swapFee = useSwapFee(route.map((r) => r.pool_id)) + const [liquidationPrice, setLiquidationPrice] = useState(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) { >
Summary -
- Fees - {formatAmountWithSymbol(estimatedFee.amount[0])} -
{isMargin && ( <> -
- Borrowing + -
-
- Borrow Rate APY + + {formatPercent(borrowRate || 0)} -
+ + )} - -
- Route - {parsedRoutes} -
+ <> + +
+ {isUpdatingLiquidationPrice ? ( + + ) : liquidationPrice === null || liquidationPrice === 0 ? ( + '-' + ) : ( + + )} +
+
+ + + + + + + {formatAmountWithSymbol(estimatedFee.amount[0])} + + + + + + {parsedRoutes}
) } + +interface SummaryLineProps { + children: React.ReactNode + label: string +} +function SummaryLine(props: SummaryLineProps) { + return ( +
+ {props.label} + {props.children} +
+ ) +} diff --git a/src/components/Trade/TradeModule/SwapForm/index.tsx b/src/components/Trade/TradeModule/SwapForm/index.tsx index 3c62d04d..a98ef094 100644 --- a/src/components/Trade/TradeModule/SwapForm/index.tsx +++ b/src/components/Trade/TradeModule/SwapForm/index.tsx @@ -62,9 +62,9 @@ export default function SwapForm(props: Props) { const { autoLendEnabledAccountIds } = useAutoLend() const isAutoLendEnabled = account ? autoLendEnabledAccountIds.includes(account.id) : false const modal = useStore((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} /> diff --git a/src/hooks/useHealthComputer.tsx b/src/hooks/useHealthComputer.tsx index a47009aa..995ee3ae 100644 --- a/src/hooks/useHealthComputer.tsx +++ b/src/hooks/useHealthComputer.tsx @@ -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, } } diff --git a/src/hooks/useSwapFee.ts b/src/hooks/useSwapFee.ts new file mode 100644 index 00000000..15c7c35a --- /dev/null +++ b/src/hooks/useSwapFee.ts @@ -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() +} diff --git a/src/hooks/useSwapValueLoss.ts b/src/hooks/useSwapValueLoss.ts index f487e858..1e9f2fc0 100644 --- a/src/hooks/useSwapValueLoss.ts +++ b/src/hooks/useSwapValueLoss.ts @@ -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, diff --git a/src/utils/health_computer/index.d.ts b/src/utils/health_computer/index.d.ts index ea88a7b2..f6c5086c 100644 --- a/src/utils/health_computer/index.d.ts +++ b/src/utils/health_computer/index.d.ts @@ -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 diff --git a/src/utils/health_computer/index.js b/src/utils/health_computer/index.js index 0544f6f4..fe0f1cb6 100644 --- a/src/utils/health_computer/index.js +++ b/src/utils/health_computer/index.js @@ -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) diff --git a/src/utils/health_computer/index_bg.wasm b/src/utils/health_computer/index_bg.wasm index 55f248f4..7d916784 100644 Binary files a/src/utils/health_computer/index_bg.wasm and b/src/utils/health_computer/index_bg.wasm differ diff --git a/src/utils/health_computer/index_bg.wasm.d.ts b/src/utils/health_computer/index_bg.wasm.d.ts index ea2c344a..bcd48262 100644 --- a/src/utils/health_computer/index_bg.wasm.d.ts +++ b/src/utils/health_computer/index_bg.wasm.d.ts @@ -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